LoginSignup
1
1

More than 1 year has passed since last update.

minecraft serverのop権限をdiscord botのコマンド実行権限に対応させたい

Last updated at Posted at 2022-12-13

前提

これは,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へのコマンドを考えます.

使いそうなコマンドはlistopmsg/tellぐらいでしょうか.これらのコマンドは

  • list
    • 現在ログインしている人数と,誰がログインしているかの表示
  • op
    • serverに対するoperater権限を与える
    • 私のpapermcの環境だと,ops.jsonのlevelを変更してもlevel4相当の権限になりました
    • 公式の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」などと呼びます.競合するのはここだけなので見逃してください
コードが以下の通りです.

discordbot.py
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()部のコードが以下の通りです.

main.py
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()の関数のところを説明します.

まず,自分以外のユーザーに対しては登録できないように早期終了しています.
また,登録していないユーザーがいない,つまり権限を持っている人すべてが登録されていれば,その旨を伝え早期終了しています.

そのあとは登録作業のために,どの人が登録されていないかをドロップダウンリストで表示して選択させます.このドロップダウンリストの大本で選択されたときに何をするかを記述するために,UnregisteredNameSelectclassを作っています.Selectについてはとりあえずドキュメントを読んでください.とりあえず,選択されたときの処理を書いているんだなという認識で大丈夫です.
idを登録したらJSONファイルに書き込んでいます.

動作確認

実行してみましょう.

誰かを登録しようとしてもできなくて,自分を選択してみるとドロップダウンリストが操作できて,そこから選択すると登録できた表示がされました.つまりは期待した動作ができていると思います.
ただ,選択した後もドロップダウンリストが操作できるため,ここはあまり良くないでしょう.再びリストから選ぶと再登録されると思います.Selectにはdisabled というパラメータがあるので,選択されたらそこをTrueにすればよいでしょう.

次にdiscordのops.jsonが生成されるので中身を見てみましょう.

ops.json
[
  {
    "uuid": "uuid",
    "name": "itousagi",
    "id": 000000000000000000
  }
]

実際にはuuididに自分のidが入ります.

よって,minecraftのユーザーとdiscordのユーザーを紐づけることができたと思います.

以上となります.minecraft serverのop権限をdiscord botのコマンド実行権限に対応させたいという方針からいろいろ考えてきました.参考になれば幸いです.
お疲れ様でした.

  1. bypassesPlayerLimitについてはこちらのmax-playersの項目を見てください.

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1