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を書く
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の中身はこんな感じになります。
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側で読み込みます。
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!と返ってくれば成功です。
これで単純なコマンドは追加できました。
次に引数を必要とするコマンドを登録してみます。
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
に追記していきます。
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
で修飾したコマンドを追加し、何層にもネストすることができます。
@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
のことです。
では先程のコグに「こんにちは」というメッセージに対して挨拶を返すイベントを追加してみましょう。
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
デコレータのaliases
にstr
のイテラブルオブジェクトを渡すことで、コマンドのエイリアスを設定することができます。
上記のコマンドの場合、エイリアスとして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