Python
Heroku
python3
bot
discord

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

はじめに

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

ということで pythonjp コミュニティは Slack から Discord に移行中です。
pythonjp Discord サーバー招待URL

Slack でできることは大抵 Discord でできますし、
Slackはログが1万件までの制限があるのに対してDiscordは無制限であり、
ボイスチャットやチャンネルのカテゴリ分けなど機能が充実しているので、
是非 Slack から Discord へ移行していってほしいですね。

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

しかし discord.py の解説記事を見ると「とりあえず動きました!終わり」というものが多いため、
もう少し実用的な例を含めた紹介をしてみたいと思います。


こんな感じで動くイメージです

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

まずはdiscordのbotアカウントを作成し、サーバーに登録しましょう。
細かい手順は偉大な先人たちの記事を参考にするとよいです。
Pythonで簡単なDiscord Botの作り方 - Qiita
discordで使える白猫テニス用レート計算botを作ったよ! - Qiita

botの作成と起動

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

console
$ pip install discord

そして以下の公式チュートリアルのサンプルコードを discordbot.py として保存します。

discordbot.py
# APIラッパと非同期I/Oモジュールの読み込み
import discord
import asyncio

# クライアント接続オブジェクト
client = discord.Client()

# 起動時の処理
@client.event
async def on_ready():
    print('Logged in as')
    print(client.user.name)
    print(client.user.id)
    print('------')

# 誰かが発言した時の処理
@client.event
async def on_message(message):
    if message.content.startswith('!test'):
        counter = 0
        tmp = await client.send_message(message.channel, 'Calculating messages...')
        async for log in client.logs_from(message.channel, limit=100):
            if log.author == message.author:
                counter += 1

        await client.edit_message(tmp, 'You have {} messages.'.format(counter))
    elif message.content.startswith('!sleep'):
        await asyncio.sleep(5)
        await client.send_message(message.channel, 'Done sleeping')

# botの接続と起動 (tokenはbotアカウントのアクセストークン)
client.run('token')

python discordbot.py でbotを起動し、
登録したサーバーの任意のテキストチャンネルで !test!sleep! と発言すると、
自分の発言数を教えてくれたり5秒寝てから反応をくれたりします。

基本的には on_message() で発言(コマンド)を受け取り、
応答なり操作なりをするという形になります。

実行時に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

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

Discord における返信は
@USERNAME を指定して行いますが、
内部的には <@USERID> という文字列になっています。
message.contentの中身を覗くと分かると思います)

bot のユーザIDは client.user.id で、
発言者のユーザIDは message.author で取得できるので、
if client.user.id in message.content が返信された判定となり、
message.author.mention を含んだ発言が返事となります。

返事をする
if client.user.id in message.content:
    await client.send_message(message.channel, '{} 呼んだ?'.format(message.author.mention))

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

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

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

チャンネル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, '{} チャンネルを作成しました'.format(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, '{} 君もPythonistaのフレンズなんだね!'.format(message.author.mention))        

Herokuへのデプロイ

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

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

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

Procfile
bot: python discordbot.py
runtime.txt
python-3.6.5
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リファレンス を読んでみてください。

参考