前提
これは,discord botを使ってminecraft serverを操作したいアドベントカレンダーの記事です.
そしてこの記事は,minecraft serverのop権限をdiscord botのコマンド実行権限に対応させることを考える記事です.対応のためのプログラムは実装していますが,その通りにすればいいというわけではありません.予めご了承ください.
また,環境は
- WSL, Ubuntu 22.04.1
- minecraft server 1.19.2
- Python 3.10.6
となっています.
botから使用するコマンドの想定
まず,discord botから実行するminecraft sererへのコマンドを考えます.
使いそうなコマンドはlist
やop
,msg
/tell
ぐらいでしょうか.これらのコマンドは
-
list
- 現在ログインしている人数と,誰がログインしているかの表示
-
op
- serverに対するoperater権限を与える
- 私のpapermcの環境だと,ops.jsonのlevelを変更してもlevel4相当の権限になりました
- ほかに設定があるかもしれませんが,papermcのドキュメントそれっぽいやつがなかったので諦めました
- 公式のjarファイルだと
ops.json
を変更しserverを再起動することで権限が更新されます
-
msg
/tell
- 誰かにメッセージを送る
という感じのものです.これらのコマンドで実行するのに権限が必要なものはop
だけなので必要性が正直ないかもしれません.ですがそもそもstop()
についても本来であれば権限が必要なのでそこには適用できるでしょう.
minecraftのユーザ名とdiscordの名前を一致させる.
次の話です.
discord botにからminecraft serverの権限を確認するには,serverがあるディレクトリ内のops.json
を確認するのがいいと思います.ops.json
には,ユーザーID,ユーザー名とそれに対応した権限レベルなどが格納されています1.しかし,ops.json
に記述されているユーザー名はminecraftで遊ぶ時のユーザー名ですので,discord上の名前と一致するとは限りません.そのため,この対応をさせます.
ここで,ops.json
は初期状態だと何も入っていないので,最初は手動でop
コマンドを実行する必要があります.そのため,とりあえずop
コマンドを実行しておいてください.
どうするか
具体的に何をするかを考えたのですが,この記事では,ops.json
に記述されているような形式で,minecraftのユーザーID,minecraftのユーザー名,discordのユーザーIDをもつJSONの配列でファイルを作りましょう.
また,そのJSONファイルの更新は,context_menu()
を使ってdiscord上で自分を右クリックしたときのアプリを実行したときにします.context_menu()
については少しだけ説明した記事を書いているので見ていただけると嬉しいです.
プログラム
ops.jsonの内容を取得
まずは,minecraft serverのあるディレクトリのops.json
の中身を取得して,自分が使うJSONに直してファイルに保存する部分の説明です.ここで自分が使うJSONファイルをops.json
としています.区別のため,「discordのops.json
」などと呼びます.競合するのはここだけなので見逃してください
コードが以下の通りです.
from MCServer import MCServer
from MinecraftCommand import MinecraftCmd
from discord import Intents, Client
from discord.app_commands import CommandTree
import json
import os
class MCClient(Client):
def __init__(self, intents: Intents) -> None:
super().__init__(intents=intents)
self.tree = CommandTree(self)
self.server = MCServer()
self.ops: dict = self.get_ops()
async def setup_hook(self) -> None:
self.tree.add_command(MinecraftCmd(self.server, self))
await self.tree.sync()
async def on_ready(self):
print(f"login: {self.user.name} [{self.user.id}]")
self.channel = self.get_channel(1047700283712622643)
def get_ops(self) -> dict:
json_file_name = "ops.json"
if os.path.exists(f"./{json_file_name}"):
ops = {}
with open(json_file_name, "r") as file:
ops = json.load(file)
return ops
ops = []
ops_json = None
with open("../mcserver/ops.json") as file:
ops_json = json.load(file)
for user in ops_json:
ops.append(
{"uuid": user["uuid"],
"name": user["name"],
"id": None}
)
with open("ops.json", "w") as write_file:
print("dump")
json.dump(ops, write_file, indent=2)
return ops
内容を取得する箇所はget_ops()
メソッドです.
そもそもすでにdiscordのops.json
が存在するなら,すでにminecraftのops.json
は読み込まれているとして早期リターンしています.理由としては,そのあとのコードでidを初期化しているからです.すでにidの値が存在しているときは何もしないという条件分岐の方がよかったなと今になって思いました.見逃してください
その後は単純にファイルの中身を読んで,自分が使う形に変形し,JSONとして出力し,discordのops.json
を返しているだけです.
context_menu()
部
続いてcontext_menu()
部のコードが以下の通りです.
import os
import json
from discord import Intents, Client, Interaction, Member
from discord.ui import Select, View
from discordbot import MCClient
from dotenv import load_dotenv
load_dotenv()
class UnregisteredNameSelect(Select):
def __init__(self, *, placeholder: str = None, client: Client) -> None:
super().__init__(placeholder=placeholder)
self.client = client
async def callback(self, interaction: Interaction):
self.client.ops[int(self.values[0])]["id"] = interaction.user.id
with open("ops.json", "w") as write_file:
print("dump")
json.dump(self.client.ops, write_file, indent=2)
await interaction.response.send_message("registered!", ephemeral=True)
intents = Intents.default()
client = MCClient(intents=intents)
@client.tree.context_menu(name="register name")
async def registerName(interaction: Interaction, member: Member):
if not interaction.user == member:
await interaction.response.send_message("sorry. please select yourself", ephemeral=True)
return
unregisteredName = []
for index, user in enumerate(client.ops):
if user["id"] is None:
unregisteredName.append([user["name"], index])
if not unregisteredName:
await interaction.response.send_message("all user registered", ephemeral=True)
return
unregisteredNameSelector = UnregisteredNameSelect(client=client)
for item in unregisteredName:
unregisteredNameSelector.add_option(
label=item[0], value=item[1]
)
select_view = View()
select_view.add_item(unregisteredNameSelector)
await interaction.response.send_message(
f"hi, {member.mention}. select your name.", ephemeral=True, view=select_view
)
client.run(os.getenv("TOKEN"))
classのところは置いといて,context_menu()
の関数のところを説明します.
まず,自分以外のユーザーに対しては登録できないように早期終了しています.
また,登録していないユーザーがいない,つまり権限を持っている人すべてが登録されていれば,その旨を伝え早期終了しています.
そのあとは登録作業のために,どの人が登録されていないかをドロップダウンリストで表示して選択させます.このドロップダウンリストの大本で選択されたときに何をするかを記述するために,UnregisteredNameSelect
classを作っています.Select
についてはとりあえずドキュメントを読んでください.とりあえず,選択されたときの処理を書いているんだなという認識で大丈夫です.
idを登録したらJSONファイルに書き込んでいます.
動作確認
実行してみましょう.
誰かを登録しようとしてもできなくて,自分を選択してみるとドロップダウンリストが操作できて,そこから選択すると登録できた表示がされました.つまりは期待した動作ができていると思います.
ただ,選択した後もドロップダウンリストが操作できるため,ここはあまり良くないでしょう.再びリストから選ぶと再登録されると思います.Select
にはdisabled
というパラメータがあるので,選択されたらそこをTrueにすればよいでしょう.
次にdiscordのops.json
が生成されるので中身を見てみましょう.
[
{
"uuid": "uuid",
"name": "itousagi",
"id": 000000000000000000
}
]
実際にはuuid
やid
に自分のidが入ります.
よって,minecraftのユーザーとdiscordのユーザーを紐づけることができたと思います.
以上となります.minecraft serverのop権限をdiscord botのコマンド実行権限に対応させたいという方針からいろいろ考えてきました.参考になれば幸いです.
お疲れ様でした.
-
bypassesPlayerLimit
についてはこちらのmax-players
の項目を見てください. ↩