discord botにminecraftの座標を登録してtpコマンドを作ってもらう

# 前提

この記事は，discord botを使ってminecraft serverを走査したいアドベントカレンダーの記事です．

- WSL, Ubuntu 22.04.1
- discord.py             2.1.0
- Python 3.10.6

という環境で行っています．

## やりたいこと

minecraftで海底神殿などを見つけたとき，座標をdiscord botにコマンド経由で別のチャンネルにまとめておいてくれるようなものを作りたいです．
そして，それだけだと面白みが何もないので，とりあえずそこにテレポートできるようにtpコマンドをコピーできるようにしたいです．

というわけで作っていきましょう．

# 実装

まずはとりあえず座標を入力して表示するコマンドを作りましょう．

## コマンドの作成

```python
import os
import discord
from discord import app_commands, Client, Intents, Interaction
from discord.ui import View, Button
from discord.app_commands import CommandTree
from dotenv import load_dotenv


load_dotenv()


class MyClient(Client):
    def __init__(self, intents: Intents) -> None:
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await self.tree.sync()

    async def on_ready(self):
        print(f"login: {self.user.name} [{self.user.id}]")


intents = Intents.default()
client = MyClient(intents=intents)


@client.tree.command()
@app_commands.describe(what="what you found?")
async def set_locate(
    interaction: Interaction,
    what: str,
    x: int,
    y: int,
    z: int,
):
    message = f"{what} is located: x:{x}, y:{y}, z:{z}"
    await interaction.response.send_message(message)


client.run(os.getenv("TOKEN"))

```

実行してみると，とりあえず大丈夫そうですね．

次にやることは，このメッセージを別のチャンネルに送ることです．チャンネルのIDから`GuildChannel`を取得します．そのチャンネルに対してメッセージを`send`メソッドで送ります．

## 別チャンネルに送信

```diff_python
import os
import discord
from discord import app_commands, Client, Intents, Interaction
from discord.ui import View, Button
from discord.app_commands import CommandTree
from dotenv import load_dotenv


load_dotenv()


class MyClient(Client):
    def __init__(self, intents: Intents) -> None:
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await self.tree.sync()

    async def on_ready(self):
        print(f"login: {self.user.name} [{self.user.id}]")


intents = Intents.default()
client = MyClient(intents=intents)


@client.tree.command()
@app_commands.describe(what="what you found?")
async def set_locate(
    interaction: Interaction,
    what: str,
    x: int,
    y: int,
    z: int,
):
    message = f"{what} is located: x:{x}, y:{y}, z:{z}"

+   log_channel = client.get_channel(1052926772607463456)
+   await log_channel.send(message)

-   await interaction.response.send_message(message)
+   await interaction.response.send_message("add location!")


client.run(os.getenv("TOKEN"))

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/64f48ccc-76d5-47e3-a662-a5649bc57566.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/984327d4-7c45-3aeb-1600-861d8bf45e28.png)

別のチャンネルに送れました．

では続いて，別チャンネルに送信したメッセージにtpコマンドのコピーができるボタンを作ります．とりあえずコピーのところは置いておいて，ボタンを作りましょう．
[View](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#view)を継承したサブクラスに，[`discord.ui.button()`デコレータ](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.ui.button)を使用しサブクラスにボタンを追加します．この下に書いた処理が`callback`として扱われます．
注意点として，スーパークラスのコンストラクタの呼び出しを忘れないようにしましょう．1時間溶かすことになります(1敗)．


```diff_python
import os
import discord
from discord import app_commands, Client, Intents, Interaction
from discord.ui import View, Button
from discord.app_commands import CommandTree
from dotenv import load_dotenv


load_dotenv()


+ class TpCommandView(View):
+     def __init__(self):
+         super().__init__()
+ 
+     @discord.ui.button(label="get tp command")
+     async def getTpCmddButton(self, interaction: Interaction, button: Button):
+         await interaction.response.send_message(
+             "copy to your clipboard!", ephemeral=True
+         )


class MyClient(Client):
    def __init__(self, intents: Intents) -> None:
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await self.tree.sync()

    async def on_ready(self):
        print(f"login: {self.user.name} [{self.user.id}]")


intents = Intents.default()
client = MyClient(intents=intents)


@client.tree.command()
@app_commands.describe(what="what you found?")
async def set_locate(
    interaction: Interaction,
    what: str,
    x: int,
    y: int,
    z: int,
):
    message = f"{what} is located: x:{x}, y:{y}, z:{z}"

+   tp_cmd_view = TpCommandView()

    log_channel = client.get_channel(1052926772607463456)
-   await log_channel.send(message)
+   await log_channel.send(message, view=tp_cmd_view)

    await interaction.response.send_message("add location!")


client.run(os.getenv("TOKEN"))

```

実行してみると下の画像のような感じでボタンが表示され，クリックするとメッセージが送信されました．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/d125fff8-7856-2947-464c-3e6d8ebad0c9.png)


というわけで，実際にはコピーされるようにしましょう．

## tpコマンドのコピー

コピーされるというのは，ボタンを押した後Ctr+Vでコマンドが貼り付けられる，という意味です．自分でドラッグしてコピーは面倒なのでそうします．
そのためにはクリップボードに対して操作をしないといけません．どうやらPythonでは`pyperclip`を使うとそれができるみたいです．[公式ドキュメント](https://pyperclip.readthedocs.io/en/latest/#)を見てみると，`copy()`でコピーさせることができるみたいです．

この`pyperclip`を使うにはインストールが必要なのでしておきましょう．ドキュメントの方には書いていないですが，[pypi](https://pypi.org/project/pyperclip/)には書いてあります．

```bash: bash
$ pip install pyperclip
```

インストールできたらさっそく使ってみましょう．
`TpCommandView`に対してユーザーが入力した座標を渡すように変更します．その中でtpコマンドを作り，コピーさせます．ちなみにtpコマンドは，`/tp x y z`とすることで自分がその座標に飛びます．対象を選択することもできますが，ユーザーの選択は面倒そうなので止めておきます．

```diff_python
import os
import discord
from discord import app_commands, Client, Intents, Interaction
from discord.ui import View, Button
from discord.app_commands import CommandTree
+ import pyperclip
from dotenv import load_dotenv


load_dotenv()


class TpCommandView(View):
-   def __init__(self):
+   def __init__(self, locate: list):
        super().__init__()
+       self.locate = locate

    @discord.ui.button(label="get tp command")
    async def getTpCmddButton(self, interaction: Interaction, button: Button):
+       tp_cmd = f"tp {self.locate[0]} {self.locate[1]} {self.locate[2]}"
+       pyperclip.copy(tp_cmd)

        await interaction.response.send_message(
            "copy to your clipboard!", ephemeral=True
        )


class MyClient(Client):
    def __init__(self, intents: Intents) -> None:
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await self.tree.sync()

    async def on_ready(self):
        print(f"login: {self.user.name} [{self.user.id}]")


intents = Intents.default()
client = MyClient(intents=intents)


@client.tree.command()
@app_commands.describe(what="what you found?")
async def set_locate(
    interaction: Interaction,
    what: str,
    x: int,
    y: int,
    z: int,
):
    message = f"{what} is located: x:{x}, y:{y}, z:{z}"

-   tp_cmd_view = TpCommandView()
+   tp_cmd_view = TpCommandView([x, y, z])

    log_channel = client.get_channel(1052926772607463456)
    await log_channel.send(message, view=tp_cmd_view)

    await interaction.response.send_message("add location!")


client.run(os.getenv("TOKEN"))

```

引数3つ書くのが面倒だったので配列で受け取るようにしてみました．まあ，ネザー経由の道を作るときに値を1/8にする必要があるので配列でもいいんじゃないかなという妥協です．

![tpcmd.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/973f3add-b199-78dc-2f44-99b7f1dffffb.gif)

ちゃんとクリップボードにコピーしてくれました．discordはWindowsで動いていますので，他のOSだとどうなるかはちょっとわかりません．

ですが，概形は同じで大丈夫だと思います．

これでtpコマンドをコピーできたのでminecraftでコマンドを簡単に使えるようになりましたね．まあ，サバイバルでやってるならコマンド打てないんですけどね．

ともかく，discord botにminecraftの座標を登録してtpコマンドを作ってもらうことができました．
お疲れ様でした．