Pythonで実用Discord Bot(discordpy解説)

# はじめに

この記事は Python と discord.py を利用した Discord Bot 開発のチュートリアルです。

<font color="Red">Pythonの基礎知識がある方を対象読者としています。
（関数の定義と呼び出しができるレベルを想定しています）</font>

Python で Discord Bot を開発する場合、
Discord API ラッパー の [discord.py](https://github.com/Rapptz/discord.py) を利用するとお手軽なのですが、
そのためにはこちらの [公式ドキュメント](https://discordpy.readthedocs.io/en/stable) を根気よく読む必要があります。

この記事ではドキュメントの内容を簡単に噛み砕き、
Botを作成する手順とよく使う機能の実装方法を紹介します。

<font color="Red">技術ドキュメントを読み慣れている方はこの記事は不要です。</font>こちらをどうぞ。
- [はじめに — discord.py 1.4.0a ドキュメント](https://discordpy.readthedocs.io/ja/latest/intro.html)
- [クイックスタート — discord.py 1.4.0a ドキュメント](https://discordpy.readthedocs.io/ja/latest/quickstart.html)

<details><summary><font color="Red">プログラミング未経験の方へ（クリックで開く）</font></summary><div>
この記事の内容は、プログラミング未経験の方が実践するのは難しいです。
挑戦する場合は必ずこちらの2つのチュートリアルを履修してください。

- [コマンドラインを使ってみよう · Django Girls Tutorial](https://tutorial.djangogirls.org/ja/intro_to_command_line/)
- [Python入門 · Django Girls Tutorial](https://tutorial.djangogirls.org/ja/python_introduction/)

何もわからないけどとりあえずBotを動かしたい方はこちらがオススメ。
[Discord Bot 最速チュートリアル【Python&Heroku&GitHub】](https://qiita.com/1ntegrale9/items/aa4b373e8895273875a8)
</div></details>

<details><summary><font color="Red">Python未経験の方へ（クリックで開く）</font></summary><div>

この記事の内容は、Pythonの基礎知識がないと躓いてしまう可能性があります。
Python入門サイトは数多くありますが、以下のサイトがオススメです。

[Python-izm | Python の入門から応用までをサポートする学習サイト](https://www.python-izm.com/)
</div></details>

## 動作環境

- Python 3.8.2
- pip 20.0.2
- discord.py 1.3.3

# 初期設定

## Botアカウントの作成と登録

まずは [Discord Developer Portal](https://discordapp.com/developers/applications/) でBotのアカウントを作成し、
Discordサーバーに登録しましょう。
アクセストークンも必要なので取得してください。

詳細な手順はこちらの記事にて紹介しています。
[Discord Botアカウント初期設定ガイド for Developer - Qiita](https://qiita.com/1ntegrale9/items/cb285053f2fa5d0cccdf)

## Botプログラムの作成と起動

ここから Python によるコーディングが必要になります。

まずは [discord.py](https://github.com/Rapptz/discord.py) をインストールしましょう。

```shell:discord.pyのインストール(Windows以外の場合)
$ python3 -m pip install -U "discord.py[voice]"
```

```shell:discord.pyのインストール(Windowsの場合)
$ py -3 -m pip install -U discord.py[voice]
```

そして以下のコードを `discordbot.py` という名前で保存します。

```python:discordbot.py
# インストールした discord.py を読み込む
import discord

# 自分のBotのアクセストークンに置き換えてください
TOKEN = 'THi5IsDuMMyaCCesSTOK3n00.Cl2FMQ.ThIsi5DUMMyAcc3s5ToKen0000'

# 接続に必要なオブジェクトを生成
client = discord.Client()

# 起動時に動作する処理
@client.event
async def on_ready():
    # 起動したらターミナルにログイン通知が表示される
    print('ログインしました')

# メッセージ受信時に動作する処理
@client.event
async def on_message(message):
    # メッセージ送信者がBotだった場合は無視する
    if message.author.bot:
        return
    # 「/neko」と発言したら「にゃーん」が返る処理
    if message.content == '/neko':
        await message.channel.send('にゃーん')

# Botの起動とDiscordサーバーへの接続
client.run(TOKEN)
```

Bot を起動します。

```shell:Botの起動
$ python3 discordbot.py
```

Bot が参加している Discord サーバーのテキストチャンネルで、
`/neko` と発言すると Bot が `にゃーん` と返してくれます。

<img width=40% alt="Screenshot 2018-07-25 16.33.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/118830/1e613f8d-418a-d0a7-6440-db1901c9ad7d.png">

# 機能の追加

基本的に、Botのコードには
「トリガーとなる操作(イベント)」と「イベント発生時の処理」
の2つを記述していくことになります。

両者をまとめたものをイベントハンドラと呼びます。

```python
"""メッセージ受信時に実行されるイベントハンドラ"""
@client.event # イベントを受信するための構文（デコレータ）
async def on_message(message): # イベントに対応する関数と受け取る引数
    ... # 処理いろいろ
```

```python
"""Bot起動時に実行されるイベントハンドラ"""
@client.event
async def on_ready():
    ...
```

```python
"""リアクション追加時に実行されるイベントハンドラ"""
@client.event
async def on_reaction_add(reaction, user):
    ...
```

```python
"""新規メンバー参加時に実行されるイベントハンドラ"""
@client.event
async def on_member_join(member):
    ...
```

```python
"""メンバーのボイスチャンネル出入り時に実行されるイベントハンドラ"""
@client.event
async def on_voice_state_update(member, before, after):
    ...
```

イベントハンドラには `@client.event` と `async def` の記述が必須です。
それぞれ `デコレータ` と `コルーチン関数` というものですが、
難しいのでとりあえず書いておけば大丈夫だと思ってください。

また、1つのイベントに対して複数のイベントハンドラを定義するとエラーになります。
（`on_message` を2つ以上記述するとエラー）

イベントのリストおよび詳細はこちら。
[Event Reference (discord.py documentation)](https://discordpy.readthedocs.io/en/stable/api.html#event-reference)

</div></details>

## e.g. 話しかけた人に返信する

[discord.Message.mentions](https://discordpy.readthedocs.io/en/stable/api.html#discord.Message.mentions)
[discord.Message.author](https://discordpy.readthedocs.io/en/stable/api.html#discord.Message.author)
[discord.Member.mention](https://discordpy.readthedocs.io/en/stable/api.html#discord.Member.mention)
を利用します。

```python:話しかけた人に返信する
# 返信する非同期関数を定義
async def reply(message):
    reply = f'{message.author.mention} 呼んだ？' # 返信メッセージの作成
    await message.channel.send(reply) # 返信メッセージを送信
   
# 発言時に実行されるイベントハンドラを定義
@client.event
async def on_message(message):
    if client.user in message.mentions: # 話しかけられたかの判定
        await reply(message) # 返信する非同期関数を実行
```

この例ではBotにメンションを送ると、
Botからメンション付きメッセージが返ってきます。

## e.g. 起動時に任意のチャンネルで発言する

[discord.Client.get_channel](https://discordpy.readthedocs.io/en/stable/api.html#) を利用します。

```python:起動時に任意のチャンネルで挨拶する
CHANNEL_ID = 987654321987654321 # 任意のチャンネルID(int)

# 任意のチャンネルで挨拶する非同期関数を定義
async def greet():
    channel = client.get_channel(CHANNEL_ID)
    await channel.send('おはよう！')

# bot起動時に実行されるイベントハンドラを定義
@client.event
async def on_ready():
    await greet() # 挨拶する非同期関数を実行
```

この例ではBotの起動時に指定したIDのチャンネルに挨拶してくれます。

ここで必要になるチャンネルのIDは、
`ユーザ設定`->`テーマ`->`詳細設定`の`開発者モード`をONにし、
任意のチャンネル名を右クリックして「IDをコピー」から取得できます。
詳細：[ユーザー/サーバー/メッセージIDはどこで見つけられる？](https://support.discordapp.com/hc/ja/articles/206346498-%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC-%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC-%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8ID%E3%81%AF%E3%81%A9%E3%81%93%E3%81%A7%E8%A6%8B%E3%81%A4%E3%81%91%E3%82%89%E3%82%8C%E3%82%8B-)

## e.g. サーバー内の様々なリストを取得

[discord.Guild.members](https://discordpy.readthedocs.io/en/stable/api.html#discord.Guild.members)
[discord.Guild.roles](https://discordpy.readthedocs.io/en/stable/api.html#discord.Guild.roles)
[discord.Guild.text_channels](https://discordpy.readthedocs.io/en/stable/api.html#discord.Guild.text_channels)
[discord.Guild.voice_channels](https://discordpy.readthedocs.io/en/stable/api.html#discord.Guild.voice_channels)
[discord.Guild.categories](https://discordpy.readthedocs.io/en/stable/api.html#discord.Guild.categories)
を利用します。

discord.py ではサーバーを Guild と表記することに注意。

```python:発言者がいるサーバー内の様々なリストの取得と表示

# コマンドに対応するリストデータを取得する関数を定義
def get_data(message):
    command = message.content
    data_table = {
        '/members': message.guild.members, # メンバーのリスト
        '/roles': message.guild.roles, # 役職のリスト
        '/text_channels': message.guild.text_channels, # テキストチャンネルのリスト
        '/voice_channels': message.guild.voice_channels, # ボイスチャンネルのリスト
        '/category_channels': message.guild.categories, # カテゴリチャンネルのリスト
    }
    return data_table.get(command, '無効なコマンドです')

# 発言時に実行されるイベントハンドラを定義
@client.event
async def on_message(message):
    # コマンドに対応するデータを取得して表示
    print(get_data(message))
```

この例では各コマンドに対応するリストを取得して表示するだけですが、
これらのリストを利用することで以下の操作などが可能になります。

- メンバー全員に役職を付与する
- 管理者権限のある役職を全て削除する
- テキストチャンネルを任意のカテゴリに一括で移動する

`get_data` 関数内での辞書の使い方についてはこちら。
[Pythonの辞書のgetメソッドでキーから値を取得（存在しないキーでもOK） | note.nkmk.me](https://note.nkmk.me/python-dict-get/)

## e.g. カテゴリ内にチャンネルを作成する

[discord.CategoryChannel.create_text_channel](https://discordpy.readthedocs.io/en/stable/api.html#discord.CategoryChannel.create_text_channel) を利用します。

```python:同一カテゴリ内にnewチャンネルを作成

# 発言したチャンネルのカテゴリ内にチャンネルを作成する非同期関数
async def create_channel(message, channel_name):
    category_id = message.channel.category_id
    category = message.guild.get_channel(category_id)
    new_channel = await category.create_text_channel(name=channel_name)
    return new_channel

# 発言時に実行されるイベントハンドラを定義
@client.event
async def on_message(message):
    if message.content.startswith('/mkch'):
        # チャンネルを作成する非同期関数を実行して Channel オブジェクトを取得
        new_channel = await create_channel(message, channel_name='new')

        # チャンネルのリンクと作成メッセージを送信
        text = f'{new_channel.mention} を作成しました'
        await message.channel.send(text)
```

この例では `/mkch` と打つと、
そのテキストチャンネルが属しているカテゴリに
新たに `new` という名前のテキストチャンネルを作成します。

## e.g. 管理者がテキストチャンネルのログを全削除する

[discord.Permissions.administrator](https://discordpy.readthedocs.io/en/latest/api.html#discord.Permissions.administrator)
[discord.TextChannel.purge](https://discordpy.readthedocs.io/en/stable/api.html#discord.TextChannel.purge)
を利用します。

```python:ログの全削除
# 発言時に実行されるイベントハンドラを定義
@client.event
async def on_message(message):
    if message.content == '/cleanup':
        if message.author.guild_permissions.administrator:
            await message.channel.purge()
            await message.channel.send('塵一つ残らないね！')
        else:
            await message.channel.send('何様のつもり？')
```

この例では `/cleanup` と打つと、
そのテキストチャンネル内のログが全て消えます。
管理者以外が打つと怒られます。

## e.g. リアクションで役職を付与する

[discord.Member.add_roles](https://discordpy.readthedocs.io/en/stable/api.html#discord.Member.add_roles) を利用します。

```python:役職の付与
ID_CHANNEL_WELCOME = 987654321987654321 # 入室用チャンネルのID(int)
ID_ROLE_WELCOME = 987654321987654321 # 付けたい役職のID(int)
EMOJI_WELCOME = '✅' # 対応する絵文字

# 役職を付与する非同期関数を定義
async def grant_role(payload):
    # 絵文字が異なる場合は処理を打ち切る
    if payload.emoji.name != EMOJI_WELCOME: 
        return

    # チャンネルが異なる場合は処理を打ち切る
    if payload.channel_id != ID_CHANNEL_WELCOME:
        return

    # Member オブジェクトと Role オブジェクトを取得して役職を付与
    member = payload.member
    role = guild.get_role(ID_ROLE_WELCOME)
    await member.add_roles(role)
    return member

# リアクション追加時に実行されるイベントハンドラを定義
@client.event
async def on_raw_reaction_add(payload):
    # 役職を付与する非同期関数を実行して Optional[Member] オブジェクトを取得
    member = await grant_role(payload)
    if member is not None: # 役職を付与したメンバーがいる時
        text = f'{member.mention} ようこそ！'
        await client.get_channel(ID_CHANNEL_WELCOME).send(text)
```

この例ではIDで指定したチャンネルで ✅ というリアクションを付けると、
そのメンバーにIDで指定した役職を付与します。

デフォルトで閲覧権限を無効にし、役職に閲覧権限を付けることで、
利用規約を読むまではチャンネルの閲覧制限を掛けておく、
というようなシステムも実現できます。

# Botを24時間365日稼働させる

<font color="Red">※コマンドラインやgitの基本的な使い方を習得している必要があります。</font>

自前のPCで24時間365日運用し続けるのは無理があるので、
リモートサーバーを借りて Bot を運用しましょう。
無料で簡単に使える [Heroku](https://www.heroku.com/home) へのデプロイ方法を紹介します。

## Herokuのセットアップ

まずはHerokuアカウントを作成しましょう。
[Heroku | Sign up](https://signup.heroku.com/login)

次にアプリを作成しましょう。
[Create New App | Heroku](https://dashboard.heroku.com/new-app)

`App name` は `discordbot-1ntegrale9` のような感じで適当に付けましょう。
名前を入力したら `Create App` をクリックしましょう。
`Choose a region` と `Add to pipeline` はそのままで大丈夫です。

## デプロイに必要なファイルの作成

追加で以下の3つのファイルを用意してください。
これらをbotのプログラムがあるディレクトリ直下に置いてください。

よく分からなければ、こちらのテンプレートリポジトリを参考にしてください。
https://github.com/DiscordBotPortalJP/discordpy-startup

### runtime.txt

実行環境を記載します。

```:runtime.txt
python-3.8.2
```

[Specifying a Python Runtime | Heroku Dev Center](https://devcenter.heroku.com/articles/python-runtimes)

### requirements.txt

インストールする外部モジュールを記載します。

```:requirements.txt
discord.py
```

[Python, pipでrequirements.txtを使ってパッケージ一括インストール | note.nkmk.me](https://note.nkmk.me/python-pip-install-requirements/)

### Procfile

プロセスと実行するコマンドを `プロセス名: コマンド` の形で記載します。

```:Procfile
discordbot: python discordbot.py
```

プロセス名は `discordbot` 以外でも問題ありませんが、
`web` や `worker` などの特殊な意味を持つプロセス名に注意してください。

[procfile · herokaijp/devcenter Wiki](https://github.com/herokaijp/devcenter/wiki/procfile)

## デプロイ方法

以下の2通りの方法があります。お好きな方をお選びください。
- GitHubリポジトリと連携する場合（オススメ）
- Heroku CLIを利用する場合

### GitHubリポジトリと連携する場合

<font color="Red">※トークンを直接コードに書いている場合は、必ずプライベートリポジトリにしてください</font>

まずGitHubアカウントを連携する必要があります。

Heroku の右上のメニューから `Account settings` を選び、
Manage Account の Third-party Services から設定します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/44be772a-dfe4-35cf-e083-27a7884deafe.png">

Dashboard の Deploy を開き、Deployment method を GitHub にします。
下の Connect to GitHub で自分のアカウントを指定し、
リポジトリ名を Search して Connect します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/5e69c9d4-087d-07f6-9299-09b43d260472.png">

`Automatic deploys` の `Enable Automatic Deploys` をクリックすると、
GitHubリポジトリにpushする度にデプロイされます。

`Manual deploy` の `Deploy Branch` の方は即時デプロイされます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/ee3ddc2d-bd67-f817-2ba0-96f724a0cc9f.png">

### Heroku CLIを利用する場合

[Heroku CLIをインストール](https://devcenter.heroku.com/articles/getting-started-with-python#set-up) した後、
`Deploy using Heroku Git` に従ってデプロイします。

```:叩くコマンド(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
```

## DynosをONにする
デプロイ後、DynosというのをONにするとBotが稼働します。
デフォルトではOFFになっているので、`Resources` タブから設定します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/22dee6d7-96aa-b2ee-2b64-b5722f5d3c62.png">

右の鉛筆マークをクリックして、
スライドバーをクリックして、
`Confirm` をクリックします。
（表示されている$0.00は稼働で発生する料金=無料です。）

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/da61488f-723a-60d6-17bc-c10d201c832f.png">

`Overview` タブでも Dynos の ON/OFF が確認できます。

<img width=90% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/0352c366-55cb-f400-5663-3d40378cdc66.png">

## Herokuの仕様について
HerokuではBotがエラーで落ちた場合に自動で再起動します。
また、24時間稼働すると再起動します。

Heroku の無料プランでは30分動作しないWebアプリケーションはスリープしますが、
紹介した手順ではwebプロセスを使用していないため、問題なく常時稼働します。

参考：[HerokuでWebアプリ開発を始めるなら知っておきたいこと(1) 無料のスペック - アインシュタインの電話番号](http://blog.ruedap.com/2011/04/29/ruby-heroku-web-app-development-tips-1)

## Heroku で DB を使う

公式で PostgreSQL と Redis のアドオンが提供されています。

[Postgres - SQL データベース・サービス | Heroku](https://jp.heroku.com/postgres)
[キーバリュー型データストア Redis をクラウドで | Heroku](https://jp.heroku.com/redis)

こちらも参考にしてください。
[Heroku×Redis×Python で始める NoSQL DB 入門 - Qiita](https://qiita.com/1ntegrale9/items/5115eaee1ee2f10bbf06)

# 補足事項

## FAQ: 実行時に SSLError が発生します

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

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

```console:console
$ /Applications/Python\ 3.7/Install\ Certificates.command
```

を実行することで解消されると思います。

参考：[macOS用公式インストーラーのPython 3.6でCERTIFICATE_VERIFY_FAILEDとなる問題 - Qiita](https://qiita.com/orangain/items/0a641d980019fd7e0c52)

## FAQ: git push heroku master時にエラーになる

```
! [remote rejected] master -> master (pre-receive hook declined) 
```
というエラーであれば、こちらをお読みください。
[【Python&Heroku】push時にエラーになる時の対処法 - GraffitiNote](https://1ntegrale9.qrunch.io/entries/NznO1i6FudUR7QdL)


## FAQ: discord.py を使わないと Bot は作れませんか？
直接 Discord の API を利用する方法もあります。
しかし、以下に示すスキルセットが必要です。

- [公式のAPIドキュメント](https://discordapp.com/developers/docs) が読める
- REST API に関する知識がある
- Python で HTTP通信 および 非同期処理 を行うコードが書ける

因みに、[disco](https://github.com/b1naryth1ef/disco) というライブラリもありますが、
こちらを採用するメリットはあまり見受けられません。

## FAQ: Python 以外の言語で Bot は作れますか？
もちろん可能です。
Discord公式でも多くのライブラリが紹介されています。
[Community Resources (Discord Developer Portal - Documentation)](https://discordapp.com/developers/docs/topics/community-resources)

特に以下は日本でよく利用されている印象です。

- JavaScript ( [discord.js](https://github.com/discordjs/discord.js) / [Eris](https://github.com/abalabahaha/eris) )
- Ruby ( [discordrb](https://github.com/meew0/discordrb) )
- Java ( [Discord4J](https://github.com/Discord4J/Discord4J) ) 

# 終わりに

discord.py には記事にまとめきれない膨大な機能があります。
そのため [公式リファレンス](https://discordpy.readthedocs.io/en/latest/) が読めることが必須です。

しかし読み方を覚えるのも難しく、内容を全て網羅することも難しいので、
DiscordBotやその周辺技術に詳しい人が多く、質問もできるコミュニティを紹介します。

眺めているだけでも有益ですので、とりあえず入っておくのがオススメです。

- [Discord Bot Portal JP](https://discord.gg/FWw6VqQ)
    - Discord Bot の利用者/開発者が集まるサーバー
    - <font color=red>この記事で分からないことがあればこちらでご質問ください。</font>
- [Python.jp](https://www.python.jp/pages/pythonjp_discord.html)
    - Python.jp の公式サーバー
    - Python の初心者から上級者まで集まる大手コミュニティです。
    - 利用規約があるので必ずお読みください。
- [discord.py](https://discord.gg/nXzj3dg)
    - discord.py の公式サーバー（英語&日本語）
    - Python初心者向けではないので注意。
    - 高度な開発を行う場合には頼りになります。
- [インフラ勉強会](https://discord.gg/mGBTKea)
    - 殆ど毎日インフラに関する勉強会が行われているサーバー
    - [インフラ勉強会のご紹介 - Qiita](https://qiita.com/yutakakn/items/86a32f2a3798f31a5471)
    - [インフラ勉強会について – インフラ勉強会](https://wp.infra-workshop.tech/about/)

# 関連記事
- [[discord.py] Discord botの作り方 ユーザ作成からコーディング・動作確認までの手順を紹介 – cod-log](https://cod-sushi.com/discord-py-warmup/)
- [discord.pyのBot Commands Frameworkを用いたBot開発 - Qiita](https://qiita.com/Lazialize/items/81f1430d9cd57fbd82fb)
- [discord.py(Rewrite版)のCogを使ってみる - Qiita](https://qiita.com/kymybk/items/14d5d10f33315c8a9aee#_reference-32487e5e4e00581efbb4)
- [discord.pyを仮想環境(pipenv)で使う
](https://qiita.com/1ntegrale9/items/7917ff81165e0161b4af)
- [PythonでDiscordボットを作る時のFAQ - Qiita](https://qiita.com/Karaf/items/7d52c95c94268627b618#_reference-f998e0815fc62f34a4be)