Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

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

はじめに

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

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

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

Pythonで実用Discord bot(discord.py解説) - 1ntegrale9氏

開発環境

  • 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が正式リリースしました!
そのため現在では以下のコマンドでインストールが可能です。

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

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

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

※このコマンドの実行にはGitが必要です。
また、1.0.0aの正式リリース後はこのコマンドの使用は非推奨です。

rewriteへの移行に伴う変更点については、公式ドキュメントを参照してください。

とりあえず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が渡されるため、一般的に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コマンドだと、

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

のように、引数に関数アノテーションとして、変換したい型を指定することで、その型へと自動で変換してくれます。
上記のコードの場合は、whatに渡された値がint型へ変換されます。
変換ができない値が渡された場合は例外が発生するので注意してください。
コンバータとして使えるもの、またはコンバータの作り方については公式ドキュメントの参照をお願いします。

サブコマンドを作る

さて、これでコマンドの作成方法と、それらをコグとして分離する方法がわかりました。
では次にコマンドをネストさせて、サブコマンドを作ってみましょう。
先程の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コマンドを追加し、そのサブコマンドとしてaddremoveを追加しました。
親となるコマンドはcommandデコレータではなくgroupデコレータで修飾します。
また、サブコマンドとなるコマンドは、roleの子となるのでcommands.command()ではなくrole.command()で修飾してください。

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

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

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

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

コグにイベントを追加

コグにはイベントを定義することもできます。
イベントとはon_readyon_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を設定しています。

コマンド名の明示

@commands.command(name='list')
async def _list(self, ctx):
    ...

commandデコレータにnameとしてstrを渡すと、コマンドの名前を明示的に設定することが出来ます。
設定したい名前が組み込み関数などと被ってしまった場合や、サブコマンドを実装する際に、同じ名前のコマンドが必要になったときなどに便利です。

コマンドのエイリアス

@commands.command(aliases=['hh'])
async def hogehoge(self, ctx):
    ...

commandデコレータのaliasesstrのイテラブルオブジェクトを渡すことで、コマンドのエイリアスを設定することができます。
上記のコマンドの場合、エイリアスとしてhhが追加されているので、!hogehoge!hhの入力だけで実行することが可能です。

コマンド実行に必要な権限

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

@commands.command()
@commands.has_permissions(manage_guild=True)
async def hogehoge(self, ctx):
    ...

commandデコレータの下にhas_permissionsデコレータを追加することで、権限の設定が可能です。
コマンド内でわざわざ確認をしなくて済むのでとても便利です。
今回はサーバーの管理権限があるユーザーのみが実行できるように設定しました。
指定できる権限については公式ドキュメントを参照してください。

コマンドのエラーを拾う

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

@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

Lazialize
プログラマを目指す学生です。 Python、Java、C#の経験があります。 最近は主にPythonでの開発をしていて、稀にUnityでC#を使っています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした