DiscordからPalworldのサーバ状況を確認して操作できるbotを作る

## はじめに
　最近 Palworld 流行っていますね。私事ですが、最近自宅鯖を使って色々と勉強していました。そんな中サーバと建てて色々と遊べるゲームということで、勉強がてら友人間で遊ぶためのサーバを立てました。しかし色々と問題が出ています。

 ## 課題
 * サーバを再起動しないとメモリリークが発生する
[Dedicated Servers: Bug found! :: Palworld / パルワールド 総合掲示板](https://steamcommunity.com/app/1623730/discussions/0/4132683013930082136)
* 再起動をするとチャンクが読み込まれず、拠点の中心にパルが固まり餓死や病気になる

## 課題に対する方策
### (NG)定期再起動を仕込む
　これでメモリリークの問題は解決することができますが、少人数(3～5人)のサーバですので、しばらくログインしないパターンもあります。そのため、2つ目の問題が回避できませんでした。
### (NG)使うときに連絡してもらって起動する
　それ誰かがホストしたら良くない？
### (NG)めっちゃメモリ積む
　そもそも推奨メモリでやりなさいよ(スペックは下記)って話ではありますが、少人数なので許してください。
　今回は自宅鯖ですが、VPSだったらすごい値段になるので、他の手を考えてみたい。
### (採用)discordからサーバ起動/停止をすることができるようにする
　せや！Discordからコマンドを打って、サーバの起動状況の確認、再起動ができるようにしたらええやん！

 ## 実際の動作
 * Status
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277121/cd2f5b96-40b9-8603-bf73-c5b4304460c1.png)
* Start
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277121/849b2e7c-f586-45ba-f137-08002596432a.png)
* Stop
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277121/8918d6b5-2efe-74f4-47d8-6ec76c99638a.png)
* Restart
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277121/0dda383c-c81d-5119-f7b5-1d86dad156e1.png)

 ## スペック
 | スペック | 値 | 備考 |
| ---- | ---- | ---- |
| OS | Ubuntu23.04 | Proxmox上で起動 |
| CPU | Intel N100(4 vCPU) |  |
| Memory | 12 GB | SWAP 16GB |

## 前提条件
* 下記 Qiita 記事を参考に Palworld の起動シェルを systemd へ登録できている
* Discord Bot の登録が出来ている
* Discord Bot にコマンドが使える権限が与えられている
* sudo を -A オプションを使って no password で実行できるようにしている
[sudoコマンドのパスワード入力の省略](https://qiita.com/hoshianaaa/items/bf80a26225e329f35928)
visudo で書いても良いです。別の方法を試してみたくて使いました。
visudo の場合は以下をご参照ください。
[特定のコマンドをパスワードなしでsudo する設定](https://lukesilvia.hatenablog.com/entry/20080716/p1)

## 簡単な仕様説明
* discord のコマンド機能で実装しています。コマンドの実装方法は以下の記事の #2 を参考にしました。
 [Discord Botを作ってみよう #01 - Hello, Pycord!](https://qiita.com/ukwhatn/items/c3abfbbd447f5802afa3)
 [Discord Botを作ってみよう #02 - スラッシュコマンドを使ってみよう](https://qiita.com/ukwhatn/items/48953e30ed67dee6fe38)
* 本サーバは以下の記事を参考に構築しており、systemdで操作できるようにしています。
 [最大32人 パルワールド Linux 専用サーバの建て方](https://qiita.com/naoya-i/items/e907a6b949e5da36d532)
* そのため、bot経由で投げるコマンドも systemctl を使っています。

## お願い
:::note warn
急いで作った + pythonが書けないため、エラー処理など甘いです。
よりスマートなコードをお待ちしております。
:::
:::note warn
多人数のサーバを操作する場合は誰かが勝手に落としてしまうなどのいたずらがある可能性も考えられます。
十分に注意して実装ください。
:::

## ソース
``` python
import discord
from discord.ext import commands
import subprocess

# init
TOKEN = '[BOTのトークン]'
Intents = discord.Intents.default()
Intents.message_content = True

bot = discord.Bot(intents=Intents)

#
# main
#
class StopView(discord.ui.View):
    def __init__(self, timeout=10):
        super().__init__(timeout=timeout)
    
    @discord.ui.button(label="YES", style=discord.ButtonStyle.success)
    async def ok(self, button: discord.ui.Button, interaction: discord.Interaction):
        command = 'sudo -A systemctl stop palworld-dedicated.service'
        result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, text=True)
        await interaction.response.send_message(f"{interaction.user.mention} Server is stoped.")
class RestartView(discord.ui.View):
    def __init__(self, timeout=10):
        super().__init__(timeout=timeout)
    
    @discord.ui.button(label="YES", style=discord.ButtonStyle.success)
    async def ok(self, button: discord.ui.Button, interaction: discord.Interaction):
        command = 'sudo -A systemctl restart palworld-dedicated.service'
        result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, text=True)
        await interaction.response.send_message(f"{interaction.user.mention} Server is restarted.")

@bot.event
async def on_ready():
    print("on_ready")
    print(discord.__version__)

@bot.command(name="status", description="Check PalWorld Status")
async def status(ctx: discord.ApplicationContext):
    await ctx.response.defer()
    command = 'sudo -A systemctl status palworld-dedicated.service  | grep Active:'
    result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, text=True)
    await ctx.followup.send(result.stdout)
 
@bot.command(name="start", description="Start PalWorld Server")
async def start(ctx: discord.ApplicationContext):
    await ctx.response.defer()
    command = 'sudo -A systemctl start palworld-dedicated.service'
    result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, text=True)
    await ctx.followup.send("Start PalWorld Server.")
 
@bot.command(name="stop", description="Stop PalWorld Server")
async def restart(ctx: discord.ApplicationContext):
    await ctx.response.defer()
    await ctx.followup.send('Do you really want to stop Palworld server?[y/n]')
    view = StopView(timeout=None)
    await ctx.channel.send(view=view)
 
@bot.command(name="restart", description="Restart PalWorld Server")
async def restart(ctx: discord.ApplicationContext):
    await ctx.response.defer()
    await ctx.followup.send('Do you really want to restart Palworld server?[y/n]')
    view = RestartView(timeout=None)
    await ctx.channel.send(view=view)
    
bot.run(TOKEN)
```
## まとめ
* 現状のメモリリークとチャンクの読み込みの仕様をクリアするために、少人数のサーバをdiscord経由で利用者が確認、起動/終了できるようにしました。
* 現状3人しか使わないので目的は達成できて満足ですが、ソースの改修や追加機能の提案などお待ちしています。