はじめに
この記事では,数回にわたってゼロから録音機能を備えたDiscord BotをPythonで作成します.
discord.pyはDiscordのAPIのラッパーライブラリです.本記事では数回に分けてdiscord.pyでのBotの作成から自作の機能の追加まで一通り行います.順序としては環境構築から始めて,一通りの機能を実装したのち録音機能を実装します.
色々なものに関する説明を加えていくうちにとんでもなく長い記事になってしまったため分割して投稿することにしました.
全7回を予定しており現在5記事まで執筆を終えています.
- Pythonで始める録音機能付きDiscord Bot: (1) 入門 discord.py
- Pythonで始める録音機能付きDiscord Bot: (2) 便利機能(Bot拡張,Cog,Embed)
- Pythonで始める録音機能付きDiscord Bot: (3) Databaseとの連携
- Pythonで始める録音機能付きDiscord Bot: (4) 音楽ファイルを再生する
- Pythonで始める録音機能付きDiscord Bot: (5) Discord APIを直接操作する
なお,この記事での実装方法には誤りがあるかもしれません.その際にはコメント欄でどうぞご指摘ください.
この記事を書くに至った経緯
discord.pyには痒い所に手が届く機能は一通り実装されています.例えば音楽再生Botを作りたいといった際には,本来は「WebSocketで認証を行う→Voice Channel用のWebSocketに接続し音声を送信する準備を行う→UDP接続を行いRTPでOpusエンコードされた20msの音楽データを暗号化し送信する」1などといった煩雑な処理を行わなければなりませんが,このdiscord.pyではそういった機能が一通り実装されており容易に利用できます.
しかしながら録音機能(音声データの受信)に関しては,
このIssueにも挙がっている通り数年前から実装要望の声が上がっておりますがいまだ本家へのPRはおこなわれておりません.一応,録音機能自体はJS版のラッパーであるDiscord.jsでは実装されているようです.不可能でないなら自力で実装してみたので,それの紹介とともにdiscord.pyの入門記事も書いてしまおうと思い書き始めました.
注意事項
- Python自体の初歩的な解説はしません.
- 動作環境
- Ubuntu 20.04
- Python 3.8.2
- Docker 19.03
- docker-compose 1.26
- discord.py 1.4.1
- VSCode 1.47.3
- Pipenv 2018.11.26
- Python,Pipenv,Docker,docker-compose,VSCodeのインストールについては説明しません.
Pipenvに関してはこちらをご覧ください.
docker-composeに関してはこちらをご覧ください
環境構築
Botの作成
まずはDiscordの開発者向けポータルからBotを作成します.New Applicationから適当に名付けて作成します.(Botの名前はここではDBotとします)
作成が終わるとこのような画面になります.アイコン画像の変更も行うことができます.
次に,Botを作成する際にあたって必要となるトークンを発行します.このトークンは流出してしまうと例えば管理者権限を持つボットであればそのトークンが流出した際,ボットを追加したすべてのサーバに関して第三者がその管理者権限でサーバーを操作することが可能となってしまいます.決して流出しないように細心の注意をもって管理することが必要です.
トークンの発行はBotメニューから[Add Bot]を選択します.
BotのトークンはBot画面作成後の画面から以下のボタンをクリックします.
これでBotの作成は完了ですが,Botをサーバに追加するための認証URLが発行されていません.認証URLの発行はOAuth2メニューから行えます.
SCOPEではbotを選択し,BOT PERMISSIONSではBotの権限を選択します.開発段階では「Administrator」権限を入れておけば問題ないです.既存のBotでもAdministratorを要求するものが多く存在しますが,どうしても管理者権限を要求するBotというだけで少し危険な印象を受ける場合があるので一般公開する際には見直すべきかもしれません.
画面中央に出てきたURLをクリックすることでBotをサーバーに招待できます.Botを導入するためには,導入させたいサーバーの管理権限を有している必要があります.2
ディレクトリ構成
本プロジェクトでは以下のようなディレクトリ構成をもとに開発していくことにします.順番に作成していくのでまだ作成する必要はありません.
.
|- src # サービスに関するソースコードすべてのルート
| |- app # アプリケーションに関するソースコード
| | |- [bot name] # Botの名前はお好きに決めてください.
| | | |- cogs # discord.pyに機能を追加するためのファイルをこのフォルダにまとめる
| | | |- core # Bot自体の定義や補助関数,拡張機能などはここにまとめる
| | | |- __init__.py # モジュールとして扱います
| | | |- __main__.py # 起動用のファイル
| | |
| | |- config.ini # さまざまな設定はモジュール外で行います
| | |- Pipfile # (自動生成されるので作らなくてOK)
| | |- entrypoint.dev.sh
| |
| |- dev.dockerfile
|
|- docker-compose.dev.yml
単に動かすだけであればファイル一つで済みますが,ここではより規模の大きなサービスを動かす場合を想定して少し複雑なディレクトリ構成にしています.
まずはPythonを動かします.Pipenvで仮想環境を作り,その環境でBotを動かします.
$ mkdir src
$ cd src
$ mkdir app
$ cd app
$ mkdir dbot
$ echo 'print("Yay")' > ./dbot/__main__.py
$ pipenv --python 3.8.2
これでPythonの環境は完成しました.__main__.py
という特殊なファイル名を使っていますが,このファイルにPythonのコードを書くことで__main__.py
の呼び出しが
# もともとPythonがPCに入っている場合
$ python -m dbot
Yay
# 作成したPipenv環境で走らせる場合
$ pipenv run python -m dbot
で行うことができます.自作モジュールの実行などの場合はこっちの方がすっきりしていて良さげです.
Botを動かそう!
Pythonのコードを動かせるようになったので次はdiscord.pyを実際にインストールしてBotを機能させるようにします.
前節で細かくディレクトリを分けましたが,本記事では動作を確認することが目的ですのですべて__main__.py
にコードを書いていくことにします.
$ pipenv install discord.py[voice]
インストールが終わったらエディタで__main__.py
を編集していきます.
import discord
# こんな感じの文字列がトークンです(下のトークンは適当です)
TOKEN = "MTE0NTE0MzY0MzY0ODEwOTMx.Adiade.Oosak0_Majide_Hampana1tteee"
# Botを動かすためのオブジェクトの作成
client = discord.Client()
@client.event
async def on_ready():
# この関数はBotの起動準備が終わった際に呼び出されます
print("起動しました")
@client.event
async def on_message(message):
# この関数はメッセージが送信された際に表示されます
# Messageにはユーザから送られてきたメッセージに関する様々な情報が格納されています
# Botからのメッセージには応答しない
if message.author.bot:
return
print("メッセージが送られました")
print("送信者", message.author.display_name)
print("内容", message.content)
# Yayという内容の文が送られたら...
if message.author != client and message.content == 'Yay':
# メッセージが送られたチャンネルにメッセージを送り返す
await message.channel.send("You're on discord.py!")
client.run(TOKEN)
ここまで入力できたら保存し,再度実行します.
起動し,コンソールに起動しました
と表示されたら,実際にBotを導入したサーバーにYayという文字を打ってみましょう.
コンソールには以下のように表示されます.
$ pipenv run python -m dbot
起動しました
メッセージが送られました
送信者 Admin
内容 Yay
Yay! You're on discord.py!
ごく短いコードですが,これだけで簡単なBotが動くようになりました.以下いくつか補足です.
このコードではasync def on_なんちゃら
という関数3の上に@client.event
というものがくっついている個所がいくつかあります.これらはPythonの機能を利用したものであり,それぞれ
-
async
~await
- 非同期処理を書く際に使う文法
-
@client.event
-
@
はデコレータと呼ばれる機能 - これを事前に書くことでその下の関数がBotに対するイベントが起こった際に発火されます
-
という機能/意味があります.初学者でしたら,いまのところとりあえずこう書いておけばいいという認識で大丈夫です.
on_ready
,on_message
のようにon_
から始まる関数に@client.event
デコレータを付けることで特定のイベントが起きた際に特定の処理をするといったことが簡単に記述できます.対応しているイベントの一覧はAPIリファレンスページにあります(結構な量があります).
また,ところどころ関数の呼び出しにawait
を使用している個所がありますが,await
を必要とする関数かどうかの判別方法はリファレンスページの関数に関する説明の中で,
This function is a coroutine.
と書かれている個所があったらその関数はawait
を前につける必要があります.
また,discord.pyでは,メッセージの内容や,メッセージの送信者などといった情報をクラスのプロパティとして取得可能です.なので,コードの実態が分からなくともコードを見ただけでどのような処理が行われているかが分かりやすくなります.その反面,慣れるまではどのクラスにどのプロパティやメソッドがあるか逐一リファレンスとにらめっこする必要があります.(一応補足にてdiscord.pyのクラスの一部を紹介します.)
まあ,要するに困ったらリファレンスを見ましょう.答えは95割そこにあります.RTFM!!!
また,ここではdiscord.Client
を直接インスタンス化してイベントの定義をしましたがdiscord.Client
を継承した独自のクラスを定義しても同等の処理が可能です.(リファレンスページにはこちらの書き方が使われています.)
import discord
TOKEN = "..."
class Bot(discord.Client):
def __init__(self):
super().__init__()
async def on_ready(self):
print("起動しました")
async def on_message(self, message):
if message.author.bot:
return
print("メッセージが送られました")
print("送信者", message.author.display_name)
print("内容", message.content)
if message.content == 'Yay':
await message.channel.send("You're on discord.py!")
Bot().run(TOKEN)
ひとまず,Botの機能追加は後程やるとして,このサービスをDockerコンテナ上で動かしてみましょう.
Dockerコンテナの作成
冒頭のディレクトリ構成例でみたようにDocker関係のファイルを3種類作成します.dev
と名前の付いているように開発時と本番実行時に環境を分けてることを視野に入れています.
- dev.dockerfile
- ベースとなるDockerイメージの作成
- entrypoint.dev.sh
- 起動時に動かすスクリプト
- docker-compose.dev.yml
- Dockerコンテナを容易に制御できるようにするための設計書
Dockerfile
の書き方はこちらの記事に倣って,builderでPythonパッケージのインストールを行った後に実行用のイメージを作成します.
FROM python:3.8 as builder
WORKDIR /bot
RUN apt update -y && \
apt upgrade -y
COPY ./app/Pipfile ./app/Pipfile.lock /bot/
RUN pip install pipenv && \
pipenv install --system
FROM python:3.8-slim
WORKDIR /bot
RUN apt update -y && \
apt upgrade -y && \
apt install -y nodejs npm curl && \
npm install -g n && \
n stable && \
apt purge -y nodejs npm && \
apt install -y ffmpeg && \
apt autoremove -y
RUN npm install -g nodemon
ENV PYTHONBUFFERED=1
COPY --from=builder /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages
COPY . /bot
実行用のイメージのビルド部分ではnodemonとffmpegのインストールを途中で行っています.nodemonは開発時にわざわざBotを止めて再起動しなくとも,ファイルの変更を感知して再起動するために使用し,ffmpegは音楽の再生等メディアに関する処理を行うために使用します.
entrypoint.dev.sh
はnodemonを利用して起動を行います.
nodemon --signal SIGINT -e py,ini --exec python -m dbot
最後にdocker-compose.dev.yml
を以下のように書きます
version: "3.8"
services:
dbot:
build:
context: ./src
dockerfile: dev.dockerfile
tty: true
working_dir: /bot/app
entrypoint: bash ./entrypoint.dev.sh
volumes:
- ./src:/bot
これらの設定によりsrc
以下がコンテナのbot
にマウントされ,ホスト上でスクリプトを更新した際にその変更を感知しBotの再起動が行われるようになります.docker環境にすることで,後にデータベースやWebフロントなどを追加したいといった際に容易に拡張が可能となります.
ここまで書き終えたら,プロジェクトルートに戻って以下のコマンドを実行します.
$ chmod +x ./src/app/entrypoint.dev.sh
$ docker-compose -f docker-compose.dev.yml -p dev build # イメージの作成
$ docker-compose -f docker-compose.dev.yml -p dev up # 起動
docker-compose -f docker-compose.dev.yml -p dev
と長々と付いていますが環境分けしているためこのように記述する必要があります.面倒であれば以下のようなスクリプトを作っておけば幸せになれるでしょう
#!/bin/bash
cmd="docker-compose -f docker-compose.$1.yml -p $1 ${@:2}"
echo $cmd
eval $cmd
$ ./run.sh dev build
$ ./run.sh dev up
起動を確認した後,試しに__main__.py
を編集してみましょう.
dbot_1 | [nodemon] 2.0.4
dbot_1 | [nodemon] to restart at any time, enter `rs`
dbot_1 | [nodemon] watching path(s): *.*
dbot_1 | [nodemon] watching extensions: py,ini
dbot_1 | [nodemon] starting `python -m dbot`
dbot_1 | 起動しました
dbot_1 | [nodemon] restarting due to changes...
dbot_1 | [nodemon] starting `python -m dbot`
dbot_1 | 起動しましたよ
保存を感知して,Botの再起動が行われました.これで開発効率を大幅に上げることが可能となります.
終わりに
この記事では基本的なBotの構築と開発に適した環境の設定を行いました.
次回では,より規模の大きいBotを作るための設計や,この記事で紹介していない埋め込み(Embed)要素などの説明を行います.
補足
1. よく使うクラス
-
記法
- List[type]
- 要素の型がtypeであるリスト
- Optional[type]
- 型はtypeもしくはNone
- Union[type1, type2, ...]
- 型はtype1, type2, ...のいずれか
- *args
- 可変引数
- List[type]
関数の引数はすべて記載しているわけではなくよく使うものだけピックアップしています.
discord.Message
クリックして展開
https://discordpy.readthedocs.io/ja/latest/api.html#message
プロパティ名 | 型 | 説明 |
---|---|---|
id | int | 一意な識別子 |
author | discord.Member | メッセージを投稿した人 |
content | str | メッセージの内容 |
guild | discord.Guild | メッセージが投稿されたサーバー(ギルド) |
channel | サーバー内だったらdiscord.TextChannel | メッセージが投稿されたチャンネル(DMなどはまた別のクラスとなるがここでは述べない) |
mentions | List[discord.Member] | メンションを飛ばした相手のリスト |
reactions | List[discord.Reaction] | メッセージに対する絵文字のリアクション |
created_at | datetime.datetime | 投稿日時 |
edited_at | Optional[datetime.datetime] | 編集日時(未編集であればNone ) |
jump_url | str | そのメッセージに飛ぶためのリンク |
await delete()
- メッセージの削除.
- Botがメッセージ管理の権限を思っている必要あり.
await edit(content: str, ...)
- メッセージの編集.
- キーワード付きで
edit(content="hoge")
のように指定する. - Bot自身の投稿したメッセージでないといけない.
await add_reaction(emoji: strなど)
- メッセージにリアクションを追加
- Unicodeの絵文字であればそのまま入力可能
await remove_reaction(emoji: strなど, member: discord.Member)
- 指定したメンバーの指定した絵文字リアクションを削除
await clear_reaction(emoji: strなど)
- 指定した絵文字リアクションを削除
discord.Member
クリックして展開
プロパティ名 | 型 | 説明 |
---|---|---|
id | int | 一意な識別子 |
name | str | ユーザー名 |
nick | Optional[str] | サーバーで設定している渾名 |
display_name | str | nickがあればnick,なければname |
mention | str | メンバーにメンションするための文字列 |
guild | discord.Guild | メンバーの属するサーバー |
roles | List[discord.Role] | メンバーの全ロールのリスト |
top_role | discord.Role | メンバーの最高ロール |
avatar_url | str | アバター画像のURL |
joined_at | datetime.datetime | メンバーがサーバーに参加した日時 |
created_at | datetime.datetime | メンバーがDiscordアカウントを登録した日 |
以下のメソッドのreasonは監査ログに表示されます.
await ban(reason: Optional[str], ...)
- メンバーをサーバーからBANします.
- BAN権限が必要で,そのユーザーはunbanするまでサーバーに参加できなくなります
await unban(reason: Optional[str], ...)
- サーバーからBANしたメンバーのBANを解除します.
- BAN権限が必要で,その処理を実行後ユーザーはサーバーに参加可能になります
await kick(reason: Optional[str], ...)
- ユーザーをサーバーから追放します
- Kick権限が必要です.
await add_roles(*roles: discord.Role)
- ユーザーにロールを与えます
- 付与できるロールは以下の制約があります.
- 1. ロール管理の権限がない場合は実行できません
- 2. BotやServer Booster等のロールは付与できません
- 3. Botが有しているロールのうち最高ロール以上の位置にあるロールはつけることができません
await remove_roles(*roles: discord.Role)
- ユーザーからロールをはく奪します
discord.TextChannel
クリックして展開
プロパティ名 | 型 | 説明 |
---|---|---|
id | int | 一意な識別子 |
name | str | チャンネル名 |
guild | discord.Guild | チャンネルがあるサーバー |
members | List[discord.Member] | チャンネル閲覧権限のあるメンバーのリスト |
mention | str | チャンネルにメンションするための文字列 |
created_at | datetime.datetime | 作成日時 |
await send(content: str, embed: discord.Embed, file: discord.File)
- メッセージの送信を行います
await purge(limit: int, check)
- メッセージの削除を行います
- limit個のメッセージを探索し,checkに該当するメッセージを削除します
- 引数checkはNoneの場合には問答無用で全メッセージを削除しますが,discord.Messageを引数にとり,削除対象のメッセージに対してTrueを返す関数を与えることで条件を指定して削除できます.
history(limit: int = 100) -> AsyncIterator
- メッセージの履歴を取得します
- 文法が独特であるため注意が必要です
以下のようにasync for ... in ~~
という呼び出し方をします.
async for message in channel.history():
print(messane.author.name)
discord.Guild
クリックして展開
プロパティ名 | 型 | 説明 |
---|---|---|
id | int | 一意な識別子 |
name | str | サーバー名 |
icon_url | str | サーバーアイコンのURL |
owner | discord.Member | サーバーオーナー |
member_count | int | メンバー数 |
text_channels | List[discord.TextChannel] | サーバー内の全テキストチャンネル |
members | List[discord.Member] | サーバー内の全メンバー |
roles | List[discord.Role] | サーバー内の全ロール |
emojis | List[discord.Emoji] | サーバーで独自に作成した絵文字のリスト |
created_at | datetime.datetime | 作成日時 |
メソッドは様々ありますが追々紹介することにします.