Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
10
Help us understand the problem. What is going on with this article?
@Shirataki2

Pythonで始める録音機能付きDiscord Bot: (1) 入門 discord.py

はじめに

この記事では,数回にわたってゼロから録音機能を備えたDiscord BotをPythonで作成します.

discord.pyはDiscordのAPIのラッパーライブラリです.本記事では数回に分けてdiscord.pyでのBotの作成から自作の機能の追加まで一通り行います.順序としては環境構築から始めて,一通りの機能を実装したのち録音機能を実装します.

色々なものに関する説明を加えていくうちにとんでもなく長い記事になってしまったため分割して投稿することにしました.

全7回を予定しており現在5記事まで執筆を終えています.

  1. Pythonで始める録音機能付きDiscord Bot: (1) 入門 discord.py
  2. Pythonで始める録音機能付きDiscord Bot: (2) 便利機能(Bot拡張,Cog,Embed)
  3. Pythonで始める録音機能付きDiscord Bot: (3) Databaseとの連携
  4. Pythonで始める録音機能付きDiscord Bot: (4) 音楽ファイルを再生する
  5. 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とします)

Image from Gyazo

作成が終わるとこのような画面になります.アイコン画像の変更も行うことができます.

次に,Botを作成する際にあたって必要となるトークンを発行します.このトークンは流出してしまうと例えば管理者権限を持つボットであればそのトークンが流出した際,ボットを追加したすべてのサーバに関して第三者がその管理者権限でサーバーを操作することが可能となってしまいます.決して流出しないように細心の注意をもって管理することが必要です.

トークンの発行はBotメニューから[Add Bot]を選択します.

Image from Gyazo

BotのトークンはBot画面作成後の画面から以下のボタンをクリックします.

Image from Gyazo

これでBotの作成は完了ですが,Botをサーバに追加するための認証URLが発行されていません.認証URLの発行はOAuth2メニューから行えます.

Image from Gyazo

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を編集していきます.

__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という文字を打ってみましょう.

Image from Gyazo

コンソールには以下のように表示されます.

$ 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を継承した独自のクラスを定義しても同等の処理が可能です.(リファレンスページにはこちらの書き方が使われています.)

__main__.py
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パッケージのインストールを行った後に実行用のイメージを作成します.

dev.dockerfile
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を利用して起動を行います.

entrypoint.dev.sh
nodemon --signal SIGINT -e py,ini --exec python -m dbot

最後にdocker-compose.dev.ymlを以下のように書きます

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と長々と付いていますが環境分けしているためこのように記述する必要があります.面倒であれば以下のようなスクリプトを作っておけば幸せになれるでしょう

run.sh
#!/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
      • 可変引数

関数の引数はすべて記載しているわけではなくよく使うものだけピックアップしています.

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の絵文字であればそのまま入力可能:ramen:
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 作成日時

メソッドは様々ありますが追々紹介することにします.



  1. この鍵括弧の中は今は読まなくていいです.難しいことやってるんですねくらいの認識でOKです. 

  2. サーバーのオーナーまたは「管理者」,「サーバーの管理者」のいずれかの権限があるロールを持ったユーザー 

  3. 厳密にはasync def ~~はコルーチン関数です 

10
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
10
Help us understand the problem. What is going on with this article?