Discord.pyをつかったBotづくり

# はじめに
本記事では、discord.pyを利用して実用的なBotの作り方について解説していきます。

#### 対象読者は以下のような方を想定しています。
- [Progate](https://prog-8.com)のPythonコースをすべてクリアできる
- [Botのトークン](https://cod-sushi.com/discord-py-token/)が用意できている
- Pythonのインストールや、テキストエディタの準備、作業ディレクトリの準備など開発環境が整っている
- コーディングに[VisualStudioCode](https://code.visualstudio.com/)または、[PyCharm](https://www.jetbrains.com/ja-jp/pycharm/)を利用している

#### 筆者の環境
- Windows 11
- Python 3.12.1
- discord.py 2.3.2

#### 本記事で作るBot
本記事では、最終的に以下のような機能のBotを作成します。
- 指定したユーザーのアイコンを取得する

# 下準備

### 仮想環境の作成
Pythonにはvenvという仮想環境を作成するための機能があります。
今回はそれを利用して開発のための仮想環境を作成します。
PyCharmを利用している方は既に作成されているはずなので、飛ばしていただいて構いません。
まず、エディターでターミナルを開き作業ディレクトリに移動して以下のコマンドを実行します。

```shell
python -m venv venv
```

しばらく待ったあと何事もなく`venv`というフォルダーが作成されたら成功です。
次にターミナルで以下のコマンドを実行します。

```shell
venv/Scripts/activate.ps1
```

そうするとターミナルのディレクトリの左側に`(venv)`という表示が出たら完了です。

### discord.pyのインストール
ターミナルで以下のコマンドを実行します。
```shell
python.exe -m pip install --upgrade pip
```
しばらく待ちます。

```shell
Successfully installed pip-xx.x.x
```
というログが出たら次に、以下のコマンドを実行します。

```shell
pip install -U discord.py
```

しばらく待ちます。
```shell
Installing collected packages: multidict, idna, frozenlist, attrs, yarl, aiosignal, aiohttp, discord.py
Successfully installed aiohttp-x.x.x aiosignal-x.x.x attrs-xx.x.x discord.py-x.x.x frozenlist-x.x.x idna-x.x multidict-x.x.x yarl-x.x.x
```
というログが出たら完了です。
`x`はバージョンなので環境や時期で対応した数字に変わります。

### ファイルの用意
作業ディレクトリに`main.py`というファイルを作ってください。

# 書いていく

### 基本のコード

```py
import discord as dc
from discord import app_commands as ac


# ここにコマンドなどを書く


class Client(dc.Client):
    def __init__(self):
        super().__init__(intents=dc.Intents.all())
        self.tree = ac.CommandTree(client=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}]')


client = Client()

client.run(token='Your-token-here')

```

##### 解説
- self.tree = ac.CommandTree(client=self)
    -
    スラッシュコマンドを実装するためのメソッドを作成しています。
    スラッシュコマンドを使う際は基本的に必要です。

- super().__init__(intents=dc.Intents.all())
    -
    botの[インテント](https://discordpy.readthedocs.io/ja/latest/api.html?highlight=intents#discord.Intents)を設定しています。
    インテントとは、botが受け取れるイベントを制限するための機能です。
- async def setup_hook(self) -> None:
    -
    botがdiscordにログインする(オンライン状態になる)前に呼び出される関数です。
    コマンドを追加する処理などは基本的にここに書きます。
- await self.tree.sync()
    -
    スラッシュコマンドをdiscord側と同期するための関数です。
    短時間に何回も実行すると、トークンが5分ほど使えなくなるので、注意してください。
- async def on_ready(self):
    -
    botがログインしたら呼び出される関数です。基本的に一度しか使えません。
    場合によってはコマンドの追加をこちらに書くこともありますが、ほぼないです。

### 指定したユーザーのアイコンを取得するコマンドを作っていく

```py
@ac.command(
    name='get_avatar',
    description='ユーザーのアイコンを取得します。'
)
@ac.rename(
    member='メンバー'
)
@ac.describe(
    member='アイコンを取得したいユーザーを指定します。'
)
async def get_user_avatar(
        inter: dc.Interaction,
        member: dc.Member
):
    embed = dc.Embed(
        title=f'{member.display_name} さんのアイコン',
        colour=dc.Colour.teal(),
        type='image'
    )
    embed.set_image(
        url=member.display_avatar.url
    )
    await inter.response.send_message(
        embed=embed
    )
    return
```

##### 解説
- @ac.command()
    -
    スラッシュコマンドを定義するためのデコレーターです。
    nameはコマンドを、descriptionはコマンドを説明を設定しています。
- @ac.rename()
    -
    コマンドの引数を改名します。引数は英語でしか定義できないため、
    日本語にするためには、こちらも使う必要があります。
- @ac.describe()
    -
    コマンドの引数に説明を追加します。
    日本語でも大丈夫です。
- async def get_user_avatar(inter: dc.Interaction, member: dc.Member):
    -
    コマンドの処理を記載するための関数を定義しています。discord.pyでは処理は関数またはクラス単位で定義します。
    [inter](https://discordpy.readthedocs.io/ja/latest/interactions/api.html?highlight=discord%20interaction#discord.Interaction)引数はコマンドの実行に関する情報が入っている引数です。スラッシュコマンドなどインタラクティブされるものには必須の引数です。
    [member](https://discordpy.readthedocs.io/ja/latest/api.html?highlight=member#discord.Member)引数ではどのメンバーのアイコンを取得するか指定するための引数になっており、スラッシュコマンドを実行する際、ユーザー側が設定します。
- embed
    -
    埋め込みと呼ばれるもので、今回画像を返す際に見やすいようにするために利用します。
    実際にコマンドを実行するとこのような埋め込みが返ってきます。
    ![スクリーンショット 2023-12-11 201208.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589135/6288fa9f-c0f7-d41a-2226-6aa0724be276.png)
- await inter.response.send_message(embed=embed):
    -
    インタラクションに対してメッセージを返す関数です。この関数はコールチン関数のため、awaitをつける必要があります。
    インタラクションに対するレスポンスは必ず一度返す必要があります。ですが、逆に一度しか返すことはできません。

### 最終的に

```py
import discord as dc
from discord import app_commands as ac


@ac.command(
    name='get_avatar',
    description='ユーザーのアイコンを取得します。'
)
@ac.rename(
    member='メンバー'
)
@ac.describe(
    member='アイコンを取得したいユーザーを指定します。'
)
async def get_user_avatar(
        inter: dc.Interaction,
        member: dc.Member
):
    embed = dc.Embed(
        title=f'{member.display_name} さんのアイコン',
        colour=dc.Colour.teal(),
        type='image'
    )
    embed.set_image(
        url=member.display_avatar.url
    )
    await inter.response.send_message(
        embed=embed
    )
    return


class Client(dc.Client):
    def __init__(self):
        super().__init__(intents=dc.Intents.all())
        self.tree = ac.CommandTree(client=self)

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

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


client = Client()

client.run(token='your-token-here')

```
##### 追加の解説
- Clientクラス内のsetup_hook関数内
    -
    `self.tree.add_command(get_user_avatar)` という関数が追加されていると思います。
    これは、コマンドをbotに追加するための関数で、コマンドを増やすたびに必要です。

# 最後に
いかがだったでしょうか？記事を書くこと自体が初めてなので、だいぶ不格好で分かりづらい記事になってしまったかもしれません。
わからないことがあったらコメントなどで、気軽に質問してください。
ここまでご覧いただきありがとうございました。