Discord.pyで、あれこれをやりたいためのサンプルコード集

# 2022/04/07 追記

Discord.pyですが、開発が再開されたとのことです。

https://github.com/Rapptz/discord.py/discussions/7848

開発を再開されたことを受け、必要に応じてこの記事の更新を再開する予定です。

Qiitaの編集リクエストをお送りいただき、ありがとうございます。この場を借りて御礼申し上げます。

# まえがき

この記事はDiscord.pyを使用してBOTを作る際に役立つかもしれないサンプルソースコードを掲載しています。

いずれもベストプラクティスであるとは限りませんが、DiscordのBOT作りの参考になればいいなという思いで、記事にしました。

## 環境

- Python 3.7以降
- discord.py 1.5.1 以降
    - サンプルコードは[Discord.pyのBotフレームワーク](https://discordpy.readthedocs.io/ja/latest/ext/commands/index.html)（`discord.ext.commands`）を使用しています。

# 1.5からの仕様変更

***(2020/11/21 追記)***

Discord APIの仕様変更により、Discord.py 1.5から一部機能を使うにはintentsが必要になりました。

特に、メンバーのステータスや情報を取得する場合は、[Discordデベロッパーポータル](https://discord.com/developers/applications)で設定が必要になります。
この設定をしないとBOTが起動しなかったり、意図しない動作が発生してしまうことがあります。

![Intentの設定例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111880/4a6c4fb9-a6c7-8a21-3df2-e59826497e37.png)

規模の大きいBOTを運用する場合（参加サーバーが100を超えるBOT）は別途申請が必要になります。

## intentsを設定するには

Botインスタンス作成時に`intents`という引数に`Intents`インスタンスを代入します。

```python
import discord
from discord.ext import commands

# discord.Intents.all()を使用するとpresencesやmembersも有効な状態になります。
discord_intents = discord.Intents.all()
# 用途に応じて以下のように無効化することもできます。設定できる項目は、以下のURLを参照してみてください。
# https://discordpy.readthedocs.io/ja/latest/api.html#discord.Intents
# discord_intents.bans = False # on_member_banやon_member_unbanのイベントを無効化

bot = commands.Bot(
    command_prefix="!",
    intents=discord_intents
)

```

※intentsが指定されなかった場合、自動的にintentsが設定されます。

# BOTが反応する頭文字を変えたい

Botインスタンス作成時に`command_prefix`という引数に頭文字を指定します。

```python
from discord.ext import commands

bot = commands.Bot(
    command_prefix="!"
)
```

## メンションにも反応させたい

`commands.when_mentioned_or`を使用するとメンションでも、頭文字でも反応するようになります。

```python
from discord.ext import commands

bot = commands.Bot(
    command_prefix=commands.when_mentioned_or("!")
)
```

# コマンドの大文字小文字を気にしないようにしたい

Botインスタンス作成時に`case_insensitive`を`True`にすると大文字小文字の区別をせず、反応するようになります。

```python
from discord.ext import commands

bot = commands.Bot(
    command_prefix="!",
    case_insensitive=True
)
```

# 標準搭載のヘルプ機能を無効にしたい

Botインスタンス作成時に`help_command`という引数を`None`にします。

```python
from discord.ext import commands

bot = commands.Bot(
    command_prefix="!",
    help_command=None
)
```

# Botアカウントに「～をプレイ中」と表示したい

`commands.Bot`インスタンスを作成時に`activity`引数に`discord.Game`インスタンスを代入します。

```python
import discord
from discord.ext import commands

bot = commands.Bot(
    command_prefix="!",
    activity=discord.Game("Qiita") # Qiitaをプレイ中
)
```

もしくは、`discord.Game`インスタンスを作成し、Botの接続完了時に`change_presence`で変更します。

```python
import discord
from discord.ext import commands

bot = commands.Bot(
    command_prefix="!"
)

presence = discord.Game("Qiita") # Qiitaをプレイ中

@bot.event
async def on_ready():
    await bot.change_presence(activity=presence)
```

# 特定のリアクションをしたらロールを付与、解除したら剥奪する

**(2020/11/21 追記)** `discord.Intents.reactions`のintentを有効にする必要があります。（[ドキュメント](https://discordpy.readthedocs.io/ja/latest/api.html#discord.Intents.reactions)）

イベントハンドラの`on_raw_reaction_add`と`on_raw_reaction_remove`を使用してロールの付与・剥奪を処理します。

`on_raw_reaction_remove`では、`member`が代入されないので、
`get_guild`を使ってギルド（サーバー）情報を取得してから、`get_member`でユーザー情報を取得する必要があります。

```python
from discord.ext import commands

class RoleCog(commands.Cog):
    def __init__(self, bot):
        self.bot = bot # commands.Botインスタンスを代入

    @commands.Cog.listener()
    async def on_raw_reaction_add(self, payload):
        if payload.member.bot: # BOTアカウントは無視する
            return

        if payload.channel_id != 123456789123: # 特定のチャンネル以外でリアクションした場合は無視する
            return

        if payload.emoji.name == "👍": # 特定の絵文字
            await payload.member.add_roles(
                payload.member.guild.get_role(123456789123) # ロールID
            )

    @commands.Cog.listener()
    async def on_raw_reaction_remove(self, payload):
        guild = self.bot.get_guild(payload.guild_id)
        member = guild.get_member(payload.user_id)
        if guild is None or member is None: # サーバーやメンバー情報が読めなかったら無視
            return

        if member.bot: # BOTアカウントは無視する
            return

        if payload.channel_id != 123456789123: # 特定のチャンネル以外でリアクションを解除した場合は無視する
            return

        if payload.emoji.name == "👍": # 特定の絵文字
            await payload.member.remove_roles(
                payload.member.guild.get_role(123456789123) # ロールID
            )

```

# 「～が入力中」と表示させる

`async with ctx.typing():`(`ctx`はテキストチャンネル)を使用することで入力中のサインを表示できます。

```python
import asyncio
from discord.ext import commands

class TypingCog(commands.Cog):
    @commands.command()
    async def delay_send(self, ctx):
        async with ctx.typing(): # 送られてきたチャンネルで入力中と表示させる
            await asyncio.sleep(5) # 重い処理をwith内で書く。（一例）
            await ctx.send("処理完了")
```

# カテゴリを取得したい

テキストチャンネル等と同様に、`get_channel`関数でカテゴリを取得することができます。

```python
from discord.ext import commands

bot = commands.Bot()

@bot.event
async def on_ready():
    category = bot.get_channel(123456789123) # カテゴリのID
```

# コマンドにDiscordのメンバーが予め入ったものを使用したい

メンバーを入れる引数の後に`discord.Member`を指定すると、`Member`インスタンスとして代入されます。
なお、メンバーが見つからなかった場合は例外`commands.BadArgument`が送出されます。
エラーハンドリングは後述にて説明します。

```python
import discord
from discord.ext import commands

class CommandArgCog(commands.Cog):
    @commands.command()
    async def say_hello(self, ctx, target: discord.Member): # targetにメンバー指定する
        await ctx.send(f"Hello, {target.mention}!")
```

# 各コマンドのエラーハンドリングを行いたい

`@{コマンドの関数名}.error`というデコレータ付け、引数に`ctx`と`error`を定義します。
ちなみに、コマンド処理内でエラーが発生すると、`commands.CommandInvokeError`が送出されます。

```python
import discord
from discord.ext import commands

class ErrorHandlingCog(commands.Cog):
    @commands.command()
    async def say_hello(self, ctx, target: discord.Member):
        await ctx.send(f"Hello, {target.mention}!")

    @say_hello.error
    async def say_hello_error(self, ctx, error):
        if isinstance(error, commands.BadArgument): # errorはBadArgumentか
            await ctx.send(f"{ctx.message.author.mention} ターゲットを指定して下さい。もしくはユーザーが見つかりませんでした。")
            return
```

# 実行しているBotが所属しているどのギルド（サーバー）に入ってないユーザー情報を取得したい

`fetch_user`を使うとBotが所属しているギルドに入ってないユーザー情報を取得することができます。

`fetch_user`はユーザーが見つからなかった場合に例外が出されるため、tryで囲むべきです。

```python
import discord
from discord.ext import commands

class FetchUser(commands.Cog):
    def __init__(self, bot):
        self.bot = bot # commands.Botインスタンスを代入

    @commands.command()
    async def fetch_user(self, target_id: int):
        try:
            target = await self.bot.fetch_user(target_id)
        except discord.NotFound:
            # ユーザーが見つからなかった場合の処理（以下は一例）
            await ctx.send("ユーザーが見つかりませんでした。")
            return

        await ctx.send(str(target)) # Username#0000
```

# あとがき

紹介したのは一例でしたが、BOT製作の役に立てたら幸いです。