パルワールドサーバー の Discord Bot を作ってみた

## 概要

パルワールドサーバー を Discord のチャット経由 で操作できるように、
Discord Bot (rcon-py) を作成しました

https://github.com/teityura/rcon-py

この記事は、上記リポジトリの使い方や仕組みについて
ざっくりまとめた備忘録となります

## 前提

この記事は、パルワールドサーバーの構築とデーモン化
の作業が前提になっています

- パルワールドサーバーの構築とデーモン化
  - [https://qiita.com/teityura/items/48a39c4f584fbe3dc419](https://qiita.com/teityura/items/48a39c4f584fbe3dc419)

ディレクトリ構成も、以下のように
/home/steam/dedicated-server/palworld になっていることを想定しているので、
別のディレクトリ構成で実行させる場合、適宜修正してください

``` py
# base directories
SRC_DIR = os.path.dirname(__file__)
HOME_DIR = "/home/steam"
INSTALL_DIR = f"{HOME_DIR}/dedicated-server/palworld"

# logging settings
LOG_PATH = f"{SRC_DIR}/logs/app.log"
DEBUG_LOG_PATH = f"{SRC_DIR}/logs/debug-app.log"

# server settings
BANLIST_PATH = f"{INSTALL_DIR}/Pal/Saved/SaveGames/banlist.txt"
CONFIG_PATH = f"{INSTALL_DIR}/Pal/Saved/Config/LinuxServer/PalWorldSettings.ini"
DEFAULT_CONFIG_PATH = f"{INSTALL_DIR}/DefaultPalWorldSettings.ini"
```

## rcon-py とは何者か

Discord のチャット経由で
サーバ運用に必要な色々なコマンドを投入できる Bot となっています

![restart.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190129/76c95dde-d65e-6dce-bfd0-16e8e4b424b3.jpeg)

## help コマンド (どんなコマンドが使えるか)

諸々のセットアップが済んだ状態で、
/help と打つと、コマンドの一覧が表示されます

![help.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190129/1fb0abf1-02c3-a9b9-1a39-ceaea643ef52.jpeg)

### info コマンド

/info で サーバー情報(バージョン等) が表示されます

![info.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190129/b2c53b1c-1d3d-47ec-62bd-44a8264fcd20.jpeg)

上記で rcon-py が何者かなんとなく理解いただけたと思うので、

- rcon-py の仕組み
- Discord Bot を作るための準備

について掘り下げていきます

## rcon-py の仕組み

主に下記の2つで構成されます

- discord.py
  - python で実装された Discord用のAPIラッパー です
- rcon(gorcon/rcon)
  - go で実装された rcon cli ツールです

## Discord Bot を作るための準備

今回は Bot と Webhook の両方を利用するので、下記などを参考に、
それぞれ作成して、認証に必要な情報をメモしておきます

- Botアカウント作成
  - [https://discordpy.readthedocs.io/ja/latest/discord.html](https://discordpy.readthedocs.io/ja/latest/discord.html)
    - DISCORD_BOT_TOKEN
- Webhook作成
  - [https://support.discord.com/hc/ja/articles/228383668](https://support.discord.com/hc/ja/articles/228383668)
    - DISCORD_WEBHOOK_URL

### Botアカウント作成

OAuth2 URL Generator から、Bot の権限を付与した URL を生成します

- MESSAGE CONTENT INTENT が有効になっていて、
- Bot Permissions
  - Send Messages
  - Read Message History
  - Read Messages/View Channels

になっていることを確認してください
トークンを後で利用するので、メモしておきます

![bot-permission.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190129/1f065be5-4f66-cbbd-fd2c-7ecd469b8e4d.jpeg)

### Webhook作成

Webhookは、チャンネルに対して作成されます

ウェブフックURLを後で利用するので、メモしておきます

![webhook-setting.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190129/77aefaa1-0ea6-968d-a197-e5f04f3353d4.jpeg)

## rcon-py 構築前準備1

.env に 上記でメモした情報を記載します

``` .env
ALLOWED_USERS="user1,user2,user3"
ALLOWED_ROLES="Admin,ServerAdmin"
DISCORD_BOT_TOKEN="xxxxx"
DISCORD_WEBHOOK_ID='xxxxx'
DISCORD_WEBHOOK_URL='https://discord.com/api/webhooks/xxxxx/yyyyy'
```

### rcon-py の権限について補足

このチャットボットを実行できるユーザを制限しています

下記3つのいずれかの条件を満たす場合、コマンドを実行できます

- 特定のユーザ(ALLOWED_USERS) であるか
- 特定のロール(ALLOWED_ROLES) に属しているか
- 特定のウェブフック(DISCORD_WEBHOOK_ID) であるか

## rcon-py 構築前準備2

rcon.yaml に RCON の認証情報を定義しておきます

PalWorldSettings.ini を
RCONEnabled: TRUE
RCONPort: 25575
に設定している場合、以下のようになります

``` yaml
palworld:
  address: "localhost:25575"
  password: "hoge"
```

## rcon-py 構築

- 構築

``` sh
make
```

- 破棄

``` sh
make clean
```

### Makefile

Makefile に
all(venv, unit, cron, rcon, start), clean を定義しています

make で実行することで、それぞれの処理を実行できます

#### venv

既存の python を汚したくないので、
python3 の venv 環境を作成して venv に対して、
requirements.txt に記載されたパッケージをインストールします

### unit

etc/systemd/system/rcon-py.service を
/etc/systemd/system/rcon-py.service にコピーします

### cron

etc/cron.d/rcon-py を /etc/cron.d/rcon-py にコピーします

### rcon

gorcon/rcon リポジトリから rcon cli を入手し、
/usr/local/bin/rcon にコピーします

## src/app.py

Bot の本体です
上記の info コマンドは、以下のように実装されています

is_valid_request で
構築前準備1, 構築前準備2 で設定した情報を元に、
許可されたリクエストかどうかを判定しています

``` py
@bot.command(name="info", brief="サーバーの情報を表示します")
@is_valid_request()
async def info(ctx):
    result = run_rcon_command("Info")
    if result.returncode == 0:
        await ctx.send(f":ok: Info に成功しました\n```{result.stdout}```")
    else:
        await ctx.send(f":warning: Info に失敗しました\n```stdout: {result.stdout}\nstderr: {result.stderr}```")
```

### status コマンド

/status で サーバーの状態を表示します
ローカルホストで systemctl status palworld を直接実行しています
/stop, /start, /restart なども同様に実装されています

``` py
@bot.command(name="status", brief="サーバーの状態を表示します")
@is_valid_request()
async def status(ctx):
    result = subprocess.run(["systemctl", "status", "palworld"], capture_output=True, text=True)
    msg = truncate_message(result.stdout)
    await ctx.send(f"```# systemctl status palworld\n{msg}```")
```

![status.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190129/4862d0ae-69a3-0eb1-4401-ecb8ccba63e6.jpeg)

## src/hook.py

webhook 実行用のスクリプトです

ウェブフック経由で /info を実行する場合、以下のようになります

``` log
[root@game-server 17:48:49] rcon-py # pwd
/root/repos/rcon-py

[root@game-server 17:48:50] rcon-py # source venv/bin/activate

(venv) [root@game-server 17:48:56] rcon-py # src/hook.py info
```

### app.py の logger

app.py では logging で app.log に出力しています

hook.py 実行時も何も出ませんでしたが、チャンネルへの出力と、
src/logs/app.log のログが出力されています

``` log
[root@game-server 17:48:36] logs # tail -f src/logs/app.log
...

[2024-02-25 17:49:03] [INFO    ] app.py: [on_message] message.content: /info
[2024-02-25 17:49:03] [INFO    ] app.py: [on_message] message.webhook_id: 1200200034006780000
[2024-02-25 17:49:03] [INFO    ] app.py: [logging_command] cmd: 'info' args: []
[2024-02-25 17:49:03] [INFO    ] app.py: [on_message] message.content: :ok: Info に成功しました
\```Welcome to Pal Server[v0.1.4.1] Teityura's Palworld Server
\```
[2024-02-25 17:49:03] [INFO    ] app.py: [on_message] message.webhook_id: None
```

![info2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190129/165755e1-2ad3-cd1a-3e83-51e2d4be5460.jpeg)

### 毎朝 6:55 に再起動させる cron ジョブ

etc/cron.d/rcon-py を下記のように書くと、
毎日 am 6:55 に /restart を実行するようになります

```
55 6 * * * @@CUR_USER@@ @@CURDIR@@/venv/bin/python @@CURDIR@@/src/hook.py restart 300
```

## その他メモ

### コンテナで環境を分離したい

RCON で 対応したコマンドを実行するだけのツールにしたかったのですが、
「デーモンの再起動や、banlist.txt の編集等が必要になるな」と思い、
「じゃあ、paramikoでSSH経由でコマンドを流す感じになるかな？」と思い、
「実装とかエラー処理が面倒そうだな。。」と思い、コンテナでの実装を断念しました

### でも構築は楽にしたい

docker-compose up -d とかで環境立ち上がるのが好きです

venv の構築とか、
unit, cron の設定とかが、
ワンコマンドでできたら嬉しいけど、
シェル書くのもなんかアレやなーと思いました

なので、Makefile でそれらを実行できるようにしてみました
make コマンドってなんか気持ちいいですね

### gorcon/rcon を使わせてもらってる理由

python のモジュールで RCON を使えるものをいくつか試したのですが、
自分の環境では、コマンドの実行はできるが、
タイムアウトが発生したりして、うまくいかなかったので、
じゃあ rcon cli 使えばいいかな？と思い、
よさげな記事が見つかったので、同じ感じで実装してみました

~~多分、やりませんが、~~ 余力があれば、
RCONの部分も自分で実装してみたいなーと思ってます

### 他のBotも実装したい

ARK, Minecraft, 7d2d など、
他の色々なゲームサーバーのBotで運用できるようになったらいいなー、と頭の片隅に置いておきつつ、
結果、パルワールドだけの実装になってます

rcon cli は
rcon -e palworld のようにプロファイルを切り替えれるので、
app.py を ゲームごとに分離できれば、統合とかもできるかなーと思ったりします

全てのゲームサーバのBotを統合するべきかどうかは、
また今度考えることにしますが、
パルワールド以外のBotを作る場合も、多分同じ感じで作れると作れるかなと思います

### コマンドの定期実行について

hook.py で ウェブフックユーザとして、チャットをして、
そのチャット内容に対して、コマンドを実行するようにしています

app.py に定義したコマンドを実行する方法について調べたところ、
asyncio で特定時間になれば、
特定のメソッドを定期実行する方法などが見つかりました

app.py は
チャンネルの監視, 対応したコマンドの実行 だけをやらせたかったので、
DISCORD_WEBHOOK_ID かどうかで判定する方式にしてみました

上記のように、ちょっと変わった実装になってるので、
もし、このツールを使ってみようと思っていただけた場合、
コマンドの実行を正しく制限できてるか、注意してみてください

## 参考

- Discord botからEC2で建てたPalWorldサーバーを管理する
  - https://zenn.dev/geny7i/articles/7259b1b53f445a
    - BOTのトークンや実装のベースの参考にしました
- gorcon/rcon
  - https://github.com/gorcon/rcon
    - これを /usr/local/bin/rcon に設置します
- rcon-py
  - https://github.com/teityura/rcon-py
    - 今回の成果物です