discord.py - commandsフレームワークへの移行

# はじめに
この記事は[discord.pyのcommandsフレームワーク](https://discordpy.readthedocs.io/ja/latest/ext/commands/index.html)に移行する方法を書いたものです。
執筆初心者なので、間違いを見つけたらどんどん指摘してください。

# 環境

```
- Python v3.8.3-final
- discord.py v1.6.0-final
- aiohttp v3.6.3
- system info: Windows 10 10.0.17134
```
（`python -m discord -v`の情報）


# 最初のコード

以下のコードから始めるモノとします：

```python
import discord

client = discord.Client()
RESPONSES = {
    "おは": "おはよう！",
    "おやすみ": "おやすみ！",
    "落ち": "おつかれ！"
}

@client.event
async def on_ready():
    print('We have logged in as {0.user}'.format(client))

@client.event
async def on_message(message):
    if message.author == client.user:
        return

    for rk, rv in RESPONSES.items():
        if rk in message.content:
            await message.reply(rv)
    
    if message.content.startswith('$ping'):
        await message.channel.send('Pong!')
    elif message.content.startswith('$greet'):
        await message.channel.send(f'Hello, `{message.content[7:]}`.')

client.run('Th1sIsN0tT0k3n.B3cause.1fiShowB0tWillG3tH4cked')
```
# 1. インポート

commandsフレームワークをインポートするには

```diff_python
import discord
+ from discord.ext import commands
```

のようにします。
普通のdiscordは残しておきます。（isinstanceでの判定やEmbedの作成に必要になります。が、今回は不必要です。）
<details><summary>詳細</summary><div>

実際、

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

でも動きはしますが、
毎回

```python
discord.ext.commands.Bot(command_prefix="!")
```
や

```python
@bot.command()
@discord.ext.commands.has_permissions(administrator=True)
```
と書く必要があり、面倒なためfrom~importを使っています。
</div></details>

#2. ClientをBotにする

ClientをBotにしていきます。
移行にはそんな心配は要らず、discord.Clientをcommands.Botに置き換えるだけで移行できます。
Botを使うにはプレフィックス（`/`や`!`など、コマンドの最初につける文字）を決める必要があります。
今回は`$`にしました。

```diff_python
- client = discord.Client()
+ bot = commands.Bot(command_prefix="$")
```

また、clientをbotにしたため、clientをbotに置き換えていきます。

```diff_python
- @client.event
+ @bot.event
async def on_ready():
-     print('We have logged in as {0.user}'.format(client))
+     print('We have logged in as {0.user}'.format(bot))

- @client.event
+ @bot.event
async def on_message(message):
-     if message.author == client.user:
+     if message.author == bot.user:
        return

    for rk, rv in RESPONSES.items():
        if rk in message.content:
            await message.channel.send(rv)
    
    if message.content.startswith('$ping'):
        await message.channel.send('Pong!')
    elif message.content.startswith('$greet'):
        await message.channel.send(f'Hello, `{message.content[7:]}`.')


- client.run('Th1sIsN0tT0k3n.B3cause.1fiShowB0tWillG3tH4cked')
+ bot.run('Th1sIsN0tT0k3n.B3cause.1fiShowB0tWillG3tH4cked')
```

#3. コマンドを定義する

「commands」フレームワークの名の通り、このフレームワークはコマンドを簡単に作ることができます。
コマンドを作るには[@bot.command](https://discordpy.readthedocs.io/ja/latest/ext/commands/api.html#discord.ext.commands.Bot.command)が一般的です。
関数名がそのままコマンド名になります。
<details><summary>詳細</summary><div>

@commands.commandで定義後、bot.add_commandで追加する方法もありますがそれほど使わないので割愛。

</div></datails>
また、@bot.commandには()が必要なので注意。

```python
@bot.command()
async def ping(ctx):
    await ctx.reply("Pong!")

@bot.command()
async def greet(ctx, name):
    await ctx.reply(f"Hello, `{name}`.")
```

<details><summary>覚えていると役立つこと</summary><div>

`list`のように関数名がかぶりそうなときはname=を引数に指定します。

```python
@bot.command(name="greet")
async def command_greet(ctx, name):
    await ctx.reply(f"Hello, `{name}`.")
```

エイリアスを追加するにはaliases=を指定します。

```python
@bot.command(aliases=["hi", "hello"])
async def greet(ctx, name):
    await ctx.reply(f"Hello, `{name}`.")
```

空白を無視して受け取りたいときは*,をつけます。

```python
@bot.command()
async def greet(ctx, *, name):
    await ctx.reply(f"Hello, `{name}`.")
```

これらは全て一緒に使うことが出来ます。

```python
@bot.command(name="greet", aliases=["hi", "hello"])
async def command_greet(ctx, *, name):
    await ctx.reply(f"Hello, `{name}`.")
```

</div></details>

コマンドを定義したため、on_messageのコマンドを削除します。

```diff_python
@bot.event
async def on_message(message):
    if message.author == bot.user:
        return

    for rk, rv in RESPONSES.items():
        if rk in message.content:
            await message.channel.send(rv)

-     if message.content.startswith('$ping'):
-         await message.channel.send('Pong!')
-     elif message.content.startswith('$greet'):
-         await message.channel.send(f'Hello, `{message.content[7:]}`.')
```


#4. コマンドを動作させる

on_messageを定義するとコマンドが実行されなくなります。
このようなときには[process_commands](https://discordpy.readthedocs.io/ja/latest/ext/commands/api.html#discord.ext.commands.Bot.process_commands)を使います。

```diff_python
@bot.event
async def on_message(message):
    if message.author == bot.user:
        return
    
    for rk, rv in RESPONSES.items():
        if rk in message.content:
            await message.channel.send(rv)
   
+     await bot.process_commands(message)
```

これで移行は終了です。

# 最終コード

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

bot = commands.Bot(command_prefix="$")
RESPONSES = {
    "おは": "おはよう！",
    "おやすみ": "おやすみ！",
    "落ち": "おつかれ！"
}

@bot.event
async def on_ready():
    print('We have logged in as {0.user}'.format(bot))

@bot.command()
async def ping(ctx):
    await ctx.reply("Pong!")

@bot.command()
async def greet(ctx, name):
    await ctx.reply(f"Hello, `{name}`.")

@bot.event
async def on_message(message):
    if message.author == bot.user:
        return

    for rk, rv in RESPONSES.items():
        if rk in message.content:
            await message.reply(rv)
    
    await bot.process_commands(message)


bot.run('Th1sIsN0tT0k3n.B3cause.1fiShowB0tWillG3tH4cked')
```


# 終わりに

この記事が役に立てば幸いです。
読んでいただきありがとうございました。