discord.pyのBot Commands Frameworkを用いたBot開発

2019/04/13　追記
とりあえず、現在の仕様に合わせてみました。
何かの拍子で頭から仕様からすっぱ抜けて、あらぬことを書いているかもしれません。
エラーなどがありましたら、コメントください。

ついでにコグに対するイベントの追加も追記しました。

2019/02/25　追記
~~<font color="red">discord.pyのコグシステムに大幅な変更がありました。
そのため、最新のrewriteではこのコードは動きません。
時間があるときに書き直します。</font>~~

# はじめに
みなさんこんにちは。
今回は、discord.pyのBot Commands Frameworkを用いたBot開発ということで説明をしていきたいと思います。

この記事はどちらかというと、既にDiscord Bot開発を行っているという方向けの記事になります。
そのため、開発について最初（具体的にはdiscord appの作成とか）から説明するつもりはないのでご了承ください。

これから開発を始める、という方は、一度以下の記事をお読みになることをオススメします。

[Pythonで実用Discord bot(discord.py解説) - 1ntegrale9氏](https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f)

## 開発環境
- Python 3.7.1
- pip 18.1
- discord.py 1.0.0a
- git 2.17.1

## discord.pyのバージョンについて

~~`discord.py`については開発バージョンかつ最新バージョンである**rewrite (1.0.0a)**を使用します。
導入がまだの方は、仮想環境を作るか、旧バージョンをアンインストールして以下のコマンドでインストールを行ってください。~~
discord.pyの`rewrite`が正式リリースしました！
そのため現在では以下のコマンドでインストールが可能です。

仮想環境は、やはり作っておくことをオススメします。

```console
# Windows
$ py -3 -m pip install -U discord.py[voice]

# Mac and Linux(Ubuntu)
$ python3 -m pip install -U discord.py[voice]
```

~~<font color="red">※このコマンドの実行にはGitが必要です。
また、1.0.0aの正式リリース後はこのコマンドの使用は非推奨です。</font>~~

**rewrite**への移行に伴う変更点については、[公式ドキュメント](https://discordpy.readthedocs.io/ja/latest/migrating.html)を参照してください。

# とりあえずBotを書く

```mybot.py
from discord.ext import commands # Bot Commands Frameworkをインポート

# クラスの定義。ClientのサブクラスであるBotクラスを継承。
class MyBot(commands.Bot):

    # Botの準備完了時に呼び出されるイベント
    async def on_ready(self):
        print('-----')
        print(self.user.name)
        print(self.user.id)
        print('-----')


# MyBotのインスタンス化及び起動処理。
if __name__ == '__main__':
    bot = MyBot(command_prefix='!') # command_prefixはコマンドの最初の文字として使うもの。 e.g. !ping
    bot.run('Botのトークン') # Botのトークン
```

基礎となるBot部分はこのような感じになります。
とりあえず、一度起動だけしてみて、`on_ready`の処理が行われるかどうかだけ確認しましょう。
また、BotクラスにはデフォルトでHelpコマンドが実装されているので`!help`で確認してみてもいいかもしれません。

# コマンドを追加する
Botの基礎部分ができたら、次は早速コマンドを追加してみましょう。
しかし、このままBotに追加していくと、コードが見づらくなるので、コグとして分離します。

## コグを作る
discord.pyのBot Commands Frameworkにはコグというものが存在します。
簡単に言えば、コマンドや処理を分離するためのものです。
コード管理もやりやすくなるので、コマンドや処理の実装はコグで行いましょう。
では実際に書いてみます。

次のような階層構造で`testcog.py`を作成します。

```
root
 ├cogs
 │ └ testcog.py
 └ mybot.py
```

testcog.pyの中身はこんな感じになります。

```testcog.py
from discord.ext import commands # Bot Commands Frameworkのインポート

# コグとして用いるクラスを定義。
class TestCog(commands.Cog):

    # TestCogクラスのコンストラクタ。Botを受取り、インスタンス変数として保持。
    def __init__(self, bot):
        self.bot = bot

    # コマンドの作成。コマンドはcommandデコレータで必ず修飾する。
    @commands.command()
    async def ping(self, ctx):
        await ctx.send('pong!')

# Bot本体側からコグを読み込む際に呼び出される関数。
def setup(bot):
    bot.add_cog(TestCog(bot)) # TestCogにBotを渡してインスタンス化し、Botにコグとして登録する。
```

これでコグが完成しました。
コグは必ず`Cog`クラスを継承している必要があります。
コマンドはコグ内に非同期関数として定義し、`command`デコレータで修飾します。
非同期関数には、`self`の他にもう一つの引数が必須です。ここには[Context](https://discordpy.readthedocs.io/ja/latest/ext/commands/api.html#context)が渡されるため、一般的に`ctx`と命名します。
コマンドの名前は非同期関数の名前がそのまま使われるので、今回の場合だと`!ping`で実行が可能です。

では、このコグをBot側で読み込みます。

```mybot.py
from discord.ext import commands # Bot Commands Frameworkをインポート

import traceback # エラー表示のためにインポート

# 読み込むコグの名前を格納しておく。
INITIAL_EXTENSIONS = [
    'cogs.testcog'
]

# クラスの定義。ClientのサブクラスであるBotクラスを継承。
class MyBot(commands.Bot):

    # MyBotのコンストラクタ。
    def __init__(self, command_prefix):
        # スーパークラスのコンストラクタに値を渡して実行。
        super().__init__(command_prefix)

        # INITIAL_COGSに格納されている名前から、コグを読み込む。
        # エラーが発生した場合は、エラー内容を表示。
        for cog in INITIAL_EXTENSIONS:
            try:
                self.load_extension(cog)
            except Exception:
                traceback.print_exc()

    # Botの準備完了時に呼び出されるイベント
    async def on_ready(self):
        print('-----')
        print(self.user.name)
        print(self.user.id)
        print('-----')


# MyBotのインスタンス化及び起動処理。
if __name__ == '__main__':
    bot = MyBot(command_prefix='!') # command_prefixはコマンドの最初の文字として使うもの。 e.g. !ping
    bot.run('Botのトークン') # Botのトークン
```

これで、Botがコグを読み込むようになりました。
では、起動してみてDiscordで`!ping`コマンドを実行してみましょう。
pong!と返ってくれば成功です。

これで単純なコマンドは追加できました。
次に引数を必要とするコマンドを登録してみます。

```testcog.py
from discord.ext import commands # Bot Commands Frameworkのインポート

import discord # discord.pyをインポート

# コグとして用いるクラスを定義。
class TestCog(commands.Cog):

    # TestCogクラスのコンストラクタ。Botを受取り、インスタンス変数として保持。
    def __init__(self, bot):
        self.bot = bot

    # コマンドの作成。コマンドはcommandデコレータで必ず修飾する。
    @commands.command()
    async def ping(self, ctx):
        await ctx.send('pong!')

    @commands.command()
    async def what(self, ctx, what):
        await ctx.send(f'{what}とはなんですか？')

# Bot本体側からコグを読み込む際に呼び出される関数。
def setup(bot):
    bot.add_cog(TestCog(bot)) # TestCogにBotを渡してインスタンス化し、Botにコグとして登録する。
```

新しく`what`コマンドを追加しました。
引数を受け取って「＜値＞とはなんですか」と返すだけの簡単なコマンドです。
引数はいくつでも設定が可能であり、また、`discord.py`のコンバータを用いることで、受け取った値を任意の型に変換することができます。
たとえば、今回の`what`コマンドだと、

```py
@commands.command()
async def what(self, ctx, what: int):
    await ctx.send(f'{what}とはなんですか？')
```
のように、引数に関数アノテーションとして、変換したい型を指定することで、その型へと自動で変換してくれます。
上記のコードの場合は、`what`に渡された値が`int`型へ変換されます。
変換ができない値が渡された場合は例外が発生するので注意してください。
コンバータとして使えるもの、またはコンバータの作り方については[公式ドキュメント](https://discordpy.readthedocs.io/ja/latest/ext/commands/commands.html#converters)の参照をお願いします。

## サブコマンドを作る
さて、これでコマンドの作成方法と、それらをコグとして分離する方法がわかりました。
では次にコマンドをネストさせて、サブコマンドを作ってみましょう。
先程の`testcog.py`に追記していきます。

```testcog.py
from discord.ext import commands # Bot Commands Frameworkのインポート

import discord

# コグとして用いるクラスを定義。
class TestCog(commands.Cog):

    # TestCogクラスのコンストラクタ。Botを受取り、インスタンス変数として保持。
    def __init__(self, bot):
        self.bot = bot

    # コマンドの作成。コマンドはcommandデコレータで必ず修飾する。
    @commands.command()
    async def ping(self, ctx):
        await ctx.send('pong!')

    @commands.command()
    async def what(self, ctx, what):
        await ctx.send(f'{what}とはなんですか？')

    # メインとなるroleコマンド
    @commands.group()
    async def role(self, ctx):
        # サブコマンドが指定されていない場合、メッセージを送信する。
        if ctx.invoked_subcommand is None:
            await ctx.send('このコマンドにはサブコマンドが必要です。')

    # roleコマンドのサブコマンド
    # 指定したユーザーに指定した役職を付与する。
    @role.command()
    async def add(self, ctx, member: discord.Member, role: discord.Role):
        await member.add_roles(role)

    # roleコマンドのサブコマンド
    # 指定したユーザーから指定した役職を剥奪する。
    @role.command()
    async def remove(self, ctx, member: discord.Member, role: discord.Role):
        await member.remove_roles(role)

# Bot本体側からコグを読み込む際に呼び出される関数。
def setup(bot):
    bot.add_cog(TestCog(bot)) # TestCogにBotを渡してインスタンス化し、Botにコグとして登録する。
```

メインのコマンドとして`role`コマンドを追加し、そのサブコマンドとして`add`と`remove`を追加しました。
親となるコマンドは`command`デコレータではなく`group`デコレータで修飾します。
また、サブコマンドとなるコマンドは、`role`の子となるので`commands.command()`ではなく`role.command()`で修飾してください。

`role`コマンドは一回のネストですが、もちろん`group`で修飾したコマンドの子として、さらに`group`で修飾したコマンドを追加し、何層にもネストすることができます。

```py
@commands.group()
async def hoge(self, ctx):
    ...

@hoge.group()
async def foo(self, ctx):
    ...

@foo.command()
async def hogehoge(self, ctx):
    ...
```

## コグにイベントを追加
コグにはイベントを定義することもできます。
イベントとは`on_ready`や`on_message`のことです。

では先程のコグに「こんにちは」というメッセージに対して挨拶を返すイベントを追加してみましょう。

```testcog.py
from discord.ext import commands # Bot Commands Frameworkのインポート

import discord

# コグとして用いるクラスを定義。
class TestCog(commands.Cog):

    # TestCogクラスのコンストラクタ。Botを受取り、インスタンス変数として保持。
    def __init__(self, bot):
        self.bot = bot

    # コマンドの作成。コマンドはcommandデコレータで必ず修飾する。
    @commands.command()
    async def ping(self, ctx):
        await ctx.send('pong!')

    @commands.command()
    async def what(self, ctx, what):
        await ctx.send(f'{what}とはなんですか？')

    # メインとなるroleコマンド
    @commands.group()
    async def role(self, ctx):
        # サブコマンドが指定されていない場合、メッセージを送信する。
        if ctx.invoked_subcommand is None:
            await ctx.send('このコマンドにはサブコマンドが必要です。')

    # roleコマンドのサブコマンド
    # 指定したユーザーに指定した役職を付与する。
    @role.command()
    async def add(self, ctx, member: discord.Member, role: discord.Role):
        await member.add_roles(role)

    # roleコマンドのサブコマンド
    # 指定したユーザーから指定した役職を剥奪する。
    @role.command()
    async def remove(self, ctx, member: discord.Member, role: discord.Role):
        await member.remove_roles(role)

    @commands.Cog.listener()
    async def on_message(self, message):
        if message.author.bot:
            return

        if message.content == 'こんにちは':
            await message.channel.send('こんにちは')

# Bot本体側からコグを読み込む際に呼び出される関数。
def setup(bot):
    bot.add_cog(TestCog(bot)) # TestCogにBotを渡してインスタンス化し、Botにコグとして登録する。
```

コグにイベントを追加する場合は、イベントとなる非同期関数を`Cog.listener`デコレータで修飾する必要があります。
また、コグに追加されたイベントとBot本体に実装されている同名のイベントはそれぞれ別に実行されるので、コグのイベントでBot側のイベントが上書きされるようなことはありません。

そのため、イベントの記述を分割したい際などにも利用することができます。


# その他の機能
これでBot Commands Frameworkのおおよその使い方はわかったでしょうか。
しかし、できることはこれだけではありません。
ここではその機能の一部を紹介したいと思います。
コードはクラス内で定義されるものとし、第一引数には`self`を設定しています。

## コマンド名の明示

```py
@commands.command(name='list')
async def _list(self, ctx):
    ...
```
`command`デコレータに`name`として`str`を渡すと、コマンドの名前を明示的に設定することが出来ます。
設定したい名前が組み込み関数などと被ってしまった場合や、サブコマンドを実装する際に、同じ名前のコマンドが必要になったときなどに便利です。

## コマンドのエイリアス
```py
@commands.command(aliases=['hh'])
async def hogehoge(self, ctx):
    ...
```
`command`デコレータの`aliases`に`str`のイテラブルオブジェクトを渡すことで、コマンドのエイリアスを設定することができます。
上記のコマンドの場合、エイリアスとして`hh`が追加されているので、`!hogehoge`を`!hh`の入力だけで実行することが可能です。

## コマンド実行に必要な権限
キックやBanのコマンドを、だれでも使えたら困りますね。
そのため、実行に必要な権限を設定することができます。

```py
@commands.command()
@commands.has_permissions(manage_guild=True)
async def hogehoge(self, ctx):
    ...
```
`command`デコレータの下に`has_permissions`デコレータを追加することで、権限の設定が可能です。
コマンド内でわざわざ確認をしなくて済むのでとても便利です。
今回はサーバーの管理権限があるユーザーのみが実行できるように設定しました。
指定できる権限については[公式ドキュメント](https://discordpy.readthedocs.io/ja/latest/api.html#discord.Permissions.create_instant_invite)を参照してください。

## コマンドのエラーを拾う
コマンドのエラーに対して個別にエラーメッセージを表示したいということはないでしょうか。
やろうと思えば、コマンド内で`try~except`で実装が可能ですが、これも、Bot Commands Frameworkで簡単に実装ができるようになっています。

```py
@commands.command()
async def hogehoge(self, ctx, member: discord.Member):
    ...

@hogehoge.error
async def hogehoge_error(self, ctx, error):
    if isinstance(error, commands.BadArgument):
        await ctx.send('渡された値をサーバーメンバーとして認識できませんでした。')
```
`hogehoge`コマンドは実行時に引数として`member`を受け取ります。
これはコンバーターで`Member`へと変換されますが、変換ができない場合は例外として`BadArgument`が発生します。
これを`error`デコレータで修飾された`hogehoge_error`が拾うことになります。
`ctx`には`Context`が、`error`には発生した`Exception`が渡されます。
あとは内部で`error`が何であるかを確認し、それに対応するメッセージを返しています。

特に処理が長いコマンドでは、エラーを分離することでより見やすいコードが書けるようになるので大変便利です。

# さいごに
私の拙い記事をここまで読んでくださりありがとうございます。
記事を書くのは初めてで、間違ったりしていないかと内心ハラハラしている次第です。

時間があるときに「その他の機能」については随時追加できたらな、と考えております。

参考にした記事・サイト
公式ドキュメント - https://discordpy.readthedocs.io/ja/latest/index.html
Pythonで実用Discord bot(discord.py解説) - https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f