Discordで動くLinuxターミナルを作る

[長野高専 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nnct) 22日目の記事になります．皆さんすごい記事書いてる...
この記事では，Discord上で動くLinuxターミナル...風のbotを作ってみました．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/b14b244d-17cf-337d-d56f-ad1f73541e4d.png)
こんな感じのターミナルを作りました．

https://github.com/BonyChops/discord-terminal

:::note warn
任意コマンド実行を他人に許可することにより，予期せぬ問題が起こる可能性があります．使う際は必ず信頼できるメンバー間でのみにしましょう．
:::

(画像にあるF-langについては以下)

https://github.com/NNCt18J/F-Lang


# きっかけ
黒板があれば学生が数式を落書きのように書いていく...ああいうノリのTerminal版がほしいなと思ったので作りました． 
弊クラスにはDiscord鯖があるので，そこに#Terminalチャンネルを作って遊んでもらうことにしました．

# 仕組み
せっかくならroot権限を付与して徹底的に破壊してもらいたいものです．ですが，それだけで毎回鯖を復旧させるのは大変なので，Dockerでイメージ，そこからコンテナを作り，必要であればそれを壊してイメージからコンテナを再構築することで，`sudo rm /* -rf` [^1]を実行されてもすぐに復活する環境を用意します．

# 実装
作りましょう．

## コンテナづくり
早速コンテナを作りましょう．とりあえず自分が一番慣れているUbuntuで作ります．

### SSH導入
Discord Botとのコマンドの受け渡しはSSHで行います．

## ユーザー名
コンテナ内のユーザー名は，DiscordのIDに準ずるものにします．ただし，人によってはLinuxのユーザー名に使えない記号を使っていたり，日本語名になっている人もいます．そういう人のためにも，どんな名前が来てもできるような仕組みを作ります．

`✝ほねつきにく✝`の場合

1. ローマ字に変換(✝honetsukiniku✝)
1. `[a-z0-9_]`以外を消す(honetsukiniku)


## LANへのアクセスを無効化
宅鯖などでこいつを運用する場合，家にいる人は当然家のLANにアクセスできることになります．流石にそれは困るので，iptablesで無効化させます．
Dockerコンテナのネットワークルールは`DOCKER-USER`チェインをiptablesにより設定することで反映させられます[^3]．(わりと最低限しか設定していないため，もしかしたら漏れがあるかも)

|優先度|対象|理由|
|-----|----|---|
|1|デフォルトゲートウェイのTCP/80を却下|デフォルトゲートウェイの端末設定を開けなくするため|
|2|デフォルトゲートウェイを許可(192.168.1.1)|ここを許可しないとグローバルに出れないため|
|3|LAN全体をブロック(192.168.1.0/24)|LAN内の別の端末のアクセスを防ぐため

```sh
sudo iptables -I DOCKER-USER -s 192.168.1.0/24 -j DROP
sudo iptables -I DOCKER-USER -s 192.168.1.1 -j ACCEPT
sudo iptables -I DOCKER-USER -s 192.168.1.1 -p tcp --dport 80 -j DROP
```


## コンテナに対応する設定ファイル
`config.json`に定義できるようにすることで，とりあえずsshが通るコンテナであれば使えるようにします．

```typescript
{
 "containers": Array<{
        "name": string,
        "port": number,
        "image": string,
        "dockerConfig": Object<dockerConfig>, //The option to create container. See https://docs.docker.com/engine/api/v1.37/#operation/ContainerCreate
        "discordIcon": string //Example: "<:windows:762682464966803476>"
        "authMethod": {
            "type": string,
            "user": string,
            "password": string
        }
    }>
}
```

## リセットボタン
Discordに便利な機能が追加されました[^2]．これでリアクションで頑張る必要はなさそうですね．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/5b247d3f-0f01-e99b-4993-c75633d6b87a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/11092545-5046-7cbb-3f2c-3bd3108de8e9.png)
クリックされた後はコンテナの停止，削除，イメージからのコンテナ再作成を行います．

## ドラッグ&ドロップでアップロード
送信されたファイルをコンテナにSCPで転送します．
送信されたファイルをBufferで格納したら，node-scp2のLow Level APIであるwriteを使って，そのBufferを直接転送します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/a332fe31-85f8-5b99-4ee0-643225c207a6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/d77334a1-48cb-c984-ab83-a6719592098e.png)

## ファイルのダウンロード
コンテナ内のファイルをDiscord側にSCPで転送します．
アップロードと同じく，SCPでBufferを受け取ってそのままDiscordに転送しようと思ったのですが，どうやらnode-scp2には直接Bufferで受け取るメソッドがない[^4]ようなので，Low Level APIであるreadを追加したnode-scp2パッケージを作成しました[^5]．

https://github.com/BonyChops/node-scp2

```diff_json:package.json
 "dependencies": {
-    "scp2": "^0.5.0",
+    "scp2": "github:BonyChops/node-scp2",
  }
```

## Workflow
任意チャンネルで`%ワークフロー名 引数...`を打つと発動できるWorkflowという機能もつけてみました．これで会話も盛り上がること間違いなし！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/ff587569-60b4-b751-885e-862b471aa958.png)
設定はこんな感じにします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/a80ecc70-addc-114d-df1f-45de792d4056.png)

使える引数は以下です

|引数|説明|
|---|----|
|$n|指定された文字列引数(n=1,2,3,...)|
|$FILEn|アップロードされたファイル(のパス)(コードブロックも含む)(n=1,2,3,...)|
また，ファイルを`/home/workflow/files/`に出力すると，出力結果として返ってきます


<details><summary>例: Gnuplot</summary><div>

```sh:workflow.sh
PLOTCONFIG=$FILE2
if [ -z $FILE1 ]; then
    echo "プロットデータが必要です"
    exit
fi

if [ -z "$PLOTCONFIG" ]; then
    PLOTCONFIG=config.gnuplot
    echo "set terminal png\nset output \"/home/workflow/files/out.png\"\nplot inputFile with lines notitle" > config.gnuplot
fi
gnuplot -e "inputFile='$FILE1'" "$PLOTCONFIG"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/a5af73b8-466a-8b60-234f-31e0d8736fc8.png)

</div></details>



# 実際の評判
クラス内で運用してしばらく経ちますが，その際のユースケースを紹介します

- コマンド確認
表示結果をさくっと確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/249f7e78-89e6-2503-b029-cc310ae7124b.png)

- Gnuplot実行
学校課題で必要なグラフもWorkflowで実行
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/963bddda-862b-a0fd-bda0-6fb73f4e8c30.png)



# 今後やりたいこと
ちょっと忙しいのでできる見通しがたっていませんが，やりたいことがあります．

- 表示結果を見やすくする
  textimgを使って出力を画像として出すことにより，より見やすい結果を表示したいなと思っています．画像はとりあえずお試しでやってみたものなのでバグり散らかしてます[^6]．
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/94a0ba2a-57f8-f94a-dd4d-6c64e5f1ac01.png)
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/210c16b6-4855-4e84-5003-85d8eb45a152.png)
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/d0f78a1a-f8cb-9fdb-b1c7-b3f34bb5eb41.png)[^7]


- OS(コンテナ)切り替え
Twitterの方ではできるようになりました！！とか言ってましたが，まだ完璧ではないです...安定してできるようにしたいですね
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601799/fdbc30c7-ac1c-f3b2-511f-81eeb06dfea5.png)

- Readme.mdの充実化
面倒臭がって適当なことしか書いてないので，いつかしっかりとした事を書いておきたいです．

- 汚ソースのリファクタリング
ソースが汚すぎるのできれいにしたいです(Dockerfile含む)...



[^1]: `--no-preserve-root` は`/`ではなく`/*`のときは必要ないです
[^2]: https://support.discord.com/hc/en-us/articles/1500012250861-Bots-Buttons
[^3]: https://docs.docker.com/network/iptables/
[^4]: もしかしたら見逃しているかもしれません．もしそうでしたらご指摘ください...＞＜
[^5]: そのままプルリクも投げようかなーとも思いましたが，最後のプルリクCloseが2018でしたのでやめておきました...
[^6]: ちゃんとフォント入れれば直る...はずです
[^7]: 雰囲気ちょっとPortal 2の色違いWheatleyっぽい...？そんなことないか