Python
Heroku
python3
bot
discord

Pythonで実用Discord bot(discord.py解説)

はじめに

-> pythonjp のコミュニティはどこにある? Discord にあるのさ!

Python に興味がある方は是非ご参加ください。
Discord bot に関する意見交換も行っています。
Python.jp Discordサーバ - python.jp

また3000人規模のインフラ勉強会サーバーもオススメです。
インフラ勉強会のご紹介


Discord は Slack とよく似たコミュニケーションツールです。
Slack でできることは大抵 Discord でできます。更に、

  • ログが無制限!(Slackは1万件まで)
  • ボイスチャット搭載
  • チャンネルのカテゴリ分けが可能

などなど機能が充実しているので、
是非 Discord でのコミュニティ運営を検討してみてください。

さて、 Discord でも Slack と同様に bot を動かすことができます。
Python で Discord botを作成する場合、
Discord API ラッパーの discord.py を利用するとお手軽です。

この記事ではよくある「botが動きました!終わり!」ではなく、
実用的な例を含めて紹介をしてみたいと思います。

動作環境

  • python 3.6.6
  • pip 18.0
  • discord.py 0.16.12

※ 最新の python 3.7 では動作しないので注意!

macOS環境のPython3のインストール - python.jp
Windows環境のPython3のインストール - python.jp

botアカウントの作成と登録

まずはbotアカウントを作成し、サーバーに登録しましょう。

DEVELOPER PORTAL にアクセスしてログインします。

「Create an application」をクリックします。

「APP ICON」と「NAME」に Bot のアイコンと名前を設定します。
左の「SETTINGS」から「Bot」をクリックします。

右の「Add Bot」クリックします。

「ADD A BOT TO THIS APP?」というモーダルダイアログが表示されるので、
「Yes, do it!」をクリックします。

「ICON」と「USERNAME」に先程設定したものと同じアイコンと名前を設定します。
そして、「TOKEN」の下の「COPY」をクリックしてアクセストークンを取得します。

アクセストークンは必ず誰にも見えない場所に保存してください。
うっかり知られるとbotが乗っ取られます。

左の「SETTINGS」から「OAuth2」をクリックします。

下にスクロールします。

「SCOPES」のチェックボックスの中から「bot」にチェックを入れます。
「BOT PERMISSIONS」が出現するので、「Administrator」にチェックを入れます。
「SCOPES」の右の「COPY」をクリックして、認証URLを取得します。

取得した認証URLにアクセスします。

追加したいサーバーを選択し、「認証」をクリックします。
※自分が管理者になっているサーバーのみ追加できます。

「I'm a not robot」のチェックボックスをクリックします。

「認証しました」と表示されれば、サーバーへのbotの登録は完了です。

botプログラムの作成と起動

まずは discord.py をインストールしましょう。

console
# Mac
$ python3 -m pip install -U discord.py

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

そして以下のコードを discordbot.py として保存します。

discordbot.py
import discord # インストールした discord.py

client = discord.Client() # 接続に使用するオブジェクト

# 起動時に通知してくれる処理
@client.event
async def on_ready():
    print('ログインしました')

# 「/neko」と発言したら「にゃーん」が返る処理
@client.event
async def on_message(message):
    if message.content.startswith('/neko'):
        reply = 'にゃーん'
        await client.send_message(message.channel, reply)

# botの接続と起動
# (tokenにはbotアカウントのアクセストークンを入れてください)
client.run('token')

botを起動します。

console
# Mac
$ python3 discordbot.py

# Windows
$ py -3 discordbot.py

テキストチャンネルで /neko と発言すると、
botが「にゃーん」と返してくれます。

Screenshot 2018-07-25 16.33.15.png

基本的には

@client.event # イベントを受信するための構文(デコレータ)
async def on_message(message): # 発言を受信したら処理をする関数

の下に色々処理を書いていくことになります。

e.g. 話しかけられたら返事をする

返事をする
if client.user.id in message.content: # 話しかけられたかの判定
    reply = f'{message.author.mention} 呼んだ?' # 返信文の作成
    await client.send_message(message.channel, reply) # 返信を送る

Discord では @USERNAME を指定すると返信になりますが、
この @USERNAME は内部的には <@USERID> という文字列になっています。
なのでbotのID = client.user.id が含まれているかで判定をします。

話しかけた人への <@USERID>message.author.mention で取得できるので、
返信文は f'{message.author.mention} 返事の内容' となります。

因みに f'{string}' という記法は python3.6 から追加されたものです。
f-strings の使用例 - Qiita

e.g. 任意のチャンネルで発言する

client.get_channel() を利用します。

任意のチャンネルで発言
channel = client.get_channel('任意のチャンネルID')
await client.send_message(channel, '勝手に喋るよ')

チャンネルIDを取得するには、
ユーザ設定->テーマ->詳細設定の開発者モードをONにし、
任意のチャンネル名を右クリックして「IDをコピー」を選択します。

e.g. ユーザーやチャンネルのリストを取得

ユーザーのオブジェクトリストは client.get_all_members() で、
チャンネルのオブジェクトリストは client.get_all_channels() で取得できます。

これを利用して以下のようなリストを作成することができます。

ユーザーの表示名のリスト
[member.display_name for member in client.get_all_members()]
チャンネルのIDのリスト
[channel.id for channel in client.get_all_channels()]

e.g. 指定名のチャンネルの作成と削除

client.create_channel と client.delete_channel を利用します。

チャンネル作成
if message.content.startswith('!mkch'):
    channel_name = message.content.split()[1]
    await client.create_channel(message.server, channel_name, type=discord.ChannelType.text)
    await client.create_channel(message.server, channel_name, type=discord.ChannelType.voice)
    await client.send_message(message.channel, f'{channel.name} チャンネルを作成しました')

上記のコードでは、!mkch hogefuga と発言すると、
hogefugaという名前のテキストチャンネルとボイスチャンネルが作成されます。

チャンネル削除
if message.content.startswith('!delch'):
    compatible_channel = [c for c in message.server.channels if message.channel.name == c.name and c.type == discord.ChannelType.voice][0]
    await client.delete_channel(message.channel)
    await client.delete_channel(compatible_channel)

上記のコードでは、!delch と発言すると、
発言したテキストチャンネルおよび同名のボイスチャンネルが削除されます。

e.g. チャンネル内発言の全削除

client.logs_from() および client.delete_messages() を利用します。

ログ削除
if message.content.startswith('!clean'):
    clean_flag = True
    while (clean_flag):
        msgs = [msg async for msg in client.logs_from(message.channel)]
        if len(msgs) > 1: # 1発言以下でdelete_messagesするとエラーになる
            await client.delete_messages(msgs)
        else:
            clean_flag = False
            await client.send_message(message.channel, 'ログの全削除が完了しました')

上記のコードでは、!clean と発言すると、
発言したチャンネル内の発言が全削除されます。

ループさせているのは client.logs_from() で取得できる件数に制限があるためです。

また、async for 内包構文を利用していますので、
あまり馴染みがない方はこちらをご参照ください。
Python3.6 から追加された文法機能 - Qiita

e.g. 役職を付与する

client.add_roles() を利用します。

役職には様々な用途があると思いますが、
サーバーに参加した状態では全てのチャンネルの閲覧権限がなく、
!join と発言することで閲覧権限のある役職が割り振られる、
というような定番の操作が可能です。

役職の付与
if message.content.startswith('!join'):
    role = discord.utils.get(message.author.server.roles, name="pythonista")
    await client.add_roles(message.author, role)
    await client.send_message(message.channel, f'{message.author.mention} 君もPythonistaのフレンズなんだね!')        

Herokuへのデプロイ

自前のPCで24時間運用し続けるのは無理があるので、
Heroku で bot を運用しましょう。

まずは Heroku のアカウントとアプリを作成し、
CLIをインストール してください。

デプロイには追加で以下の3つのファイルが必要になります。

Procfile
bot: python discordbot.py
runtime.txt
python-3.6.6
requirements.txt
discord.py==0.16.12

bot プログラムと合わせて heroku に push しましょう。

叩くコマンド(DashboardのDeployタブ参照)
$ heroku login
$ cd <botを作成したディレクトリ>
$ git init
$ heroku git:remote -a <herokuのアプリ名>
$ git add .
$ git commit -am "make it better"
$ git push heroku master

デプロイ時のログ
$ git push heroku master
Counting objects: 6, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (6/6), 1.01 KiB | 1.01 MiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Python app detected
remote: -----> Installing python-3.6.5
remote: -----> Installing pip
remote: -----> Installing requirements with pip
remote:        Collecting discord.py==0.16.12 (from -r /tmp/build_1b3a5f9e18b0c8439c67bd2177010ef3/requirements.txt (line 1))
remote:          Downloading https://files.pythonhosted.org/packages/97/3c/2a97b47fd8839f8863241857bbd6a3998d1de1662b788c8d9322e5a40901/discord.py-0.16.12.tar.gz (414kB)
remote:        Collecting aiohttp<1.1.0,>=1.0.0 (from discord.py==0.16.12->-r /tmp/build_1b3a5f9e18b0c8439c67bd2177010ef3/requirements.txt (line 1))
remote:          Downloading https://files.pythonhosted.org/packages/09/5a/7b81ea8729d41f44c6fe6a116e466c8fb884950a0061aa3768dbd6bee2f8/aiohttp-1.0.5.tar.gz (499kB)
remote:        Collecting websockets<4.0,>=3.1 (from discord.py==0.16.12->-r /tmp/build_1b3a5f9e18b0c8439c67bd2177010ef3/requirements.txt (line 1))
remote:          Downloading https://files.pythonhosted.org/packages/4f/3a/2c3a5b2c65179851e80d4acae30cffb2610a8740a8edb2afbeaa564283f8/websockets-3.4-cp36-cp36m-manylinux1_x86_64.whl (54kB)
remote:        Collecting chardet (from aiohttp<1.1.0,>=1.0.0->discord.py==0.16.12->-r /tmp/build_1b3a5f9e18b0c8439c67bd2177010ef3/requirements.txt (line 1))
remote:          Downloading https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl (133kB)
remote:        Collecting multidict>=2.0 (from aiohttp<1.1.0,>=1.0.0->discord.py==0.16.12->-r /tmp/build_1b3a5f9e18b0c8439c67bd2177010ef3/requirements.txt(line 1))
remote:          Downloading https://files.pythonhosted.org/packages/cc/30/508a22a28dfb50cf9079cd9d0cf9b0d7dbae5afdf9823977351cbd548897/multidict-4.3.1-cp36-cp36m-manylinux1_x86_64.whl (476kB)
remote:        Collecting async_timeout (from aiohttp<1.1.0,>=1.0.0->discord.py==0.16.12->-r /tmp/build_1b3a5f9e18b0c8439c67bd2177010ef3/requirements.txt (line 1))
remote:          Downloading https://files.pythonhosted.org/packages/96/0f/e6357458c87fb4ed8f3df215773f3caad40968f10e05552cbd8bd28415e4/async_timeout-3.0.0-py3-none-any.whl
remote:        Installing collected packages: chardet, multidict, async-timeout, aiohttp, websockets, discord.py
remote:          Running setup.py install for aiohttp: started
remote:            Running setup.py install for aiohttp: finished with status 'done'
remote:          Running setup.py install for discord.py: started
remote:            Running setup.py install for discord.py: finished with status 'done'
remote:        Successfully installed aiohttp-1.0.5 async-timeout-3.0.0 chardet-3.0.4 discord.py-0.16.12 multidict-4.3.1 websockets-3.4
remote:
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 43.6M
remote: -----> Launching...
remote:        Released v3
remote:        https://discord-py-19.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/discord-py-19.git
 * [new branch]      master -> master

Heroku の無料プランでは30分動作しないアプリケーションはスリープしますが、
discord.py を利用した bot はイベントループが常時走っているので問題ありません。

因みに複数のbotを動かしたい場合には少し工夫が必要です。
Herokuの無料プランで複数のPython製botを動かす方法 - Qiita

おわりに

以上のことを把握していれば、大抵のことは実現できると思います。
あとの細かい部分は APIリファレンス を読んでみてください。

参考リンク

FAQ:実行時にSSLErrorが発生する場合

console
ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed

というエラーが実行時に発生する場合があります。その場合は、

console
$ /Applications/Python\ 3.6/Install\ Certificates.command

を実行することで解消されると思います。
参考:macOS用公式インストーラーのPython 3.6でCERTIFICATE_VERIFY_FAILEDとなる問題 - Qiita