初投稿です。ご意見大歓迎です。質問等ありましたらコメント頂ければお受けできる範囲でお受けします。
※2019年12月12日追記
本記事は
discord.py ver0.16.12
を前提に書いています。
現行のdiscord.py
の最新版はver 1.3.0
とメジャーアップデートがなされており、大きく仕様が異なります。
最新版でのコードについては、以下の記事に追記予定です。
【最新版】discord.pyでbotに一定時間ごとに発言させる
前提・この記事でできるようになること
前提
- discord.py 0.16.12
- Python 3.6
discord.pyはDiscordのAPIを簡単に叩けるようにするためのPythonのライブラリ。discord.pyを利用したbotの導入は既に済んでいるものとする1。
この記事でできるようになること
discord.pyを利用して、botに一定時間ごとに特定の処理を行わせることができるようになる2。
動機
元々やりたかったのは、ミリオンライブシアターデイズのTwitter公式アカウントのツイートを、プレイヤーが集まるDiscordのサーバーに連携すること。
Google検索すると**「Webhook使ってIFTTTで連携すれば簡単」**という記事で溢れている。しかし私の場合、以下の2つの事情からその手法はとりづらかった3。
- 私自身に権限がなかったため、WebhookのURLを取得するには管理者さんにお願いする必要がある
- Webhookを利用する場合それ専用のBotが生成されるが、連携には既に導入しているbotを利用したい
こうした事情から既存のdiscord.pyを用いて作成しているbotにコードを追加して、TwitterとDiscordを連携する手段を模索することにした。
方法としては、「TwitterのAPIを叩きミリシタ公式アカウントのツイートを取得し、新しいツイートがあればそれをチャンネルに発言する」というのを数秒ごとに繰り返すことになる。この中で、数秒ごとに繰り返す処理で躓いたので、以下にまとめます。
動くコード例
いろいろすっ飛ばして、10秒毎に「おはよう」と発言するコードは以下。「『おはよう』と発言する」部分を適当なコードに置き換えれば、例えば上記のような「数秒ごとにTwitterのAPIを叩き差分をDiscordに投稿する」こともできる。
import discord
import asyncio
client = discord.Client()
@client.event
async def on_ready():
asyncio.ensure_future(greeting_gm())
async def greeting_gm():
await client.send_message(channel, 'おはよう')
await asyncio.sleep(10)
client.run(token)
注意点としては、処理をasync def on_ready():
内に記述することと、time.sleep(10)
ではなくawait asyncio.sleep(10)
と書くこと、asyncio.ensure_future
でTask化(?)すること。
1点目は、async def on_ready():
内に記述したコードはbotがサーバでオンラインになった直後に自動的に実行されるため、botに定期的に行わせたい処理とマッチする。
2点目は、discord.pyのv1.0.0のリファレンスにもあることだが、単にtime.sleep(10)
とするとその10秒間は全てのコードの実行が停止する(?)ため。
3点目は、このようにせずに単にawait greeting('おはよう', 3)
などとすると、それ以下の記述が実行されないため。例えば以下のようなコードでは、botが「おやすみ」と言ってくれることはない。
import discord
import asyncio
client = discord.Client()
@client.event
async def on_ready():
await greeting_gm() #この中のwhile Trueを延々とループするために
await greeting_gn() #この行に到達できない
async def greeting_gm():
while True:
await client.send_message(channel, 'おはよう')
await client.sleep(10)
async def greeting_gn():
while True:
await client.send_message(channel, 'おやすみ')
await client.sleep(10)
client.run(token)
起動後からずっと複数の処理を定期的に行わせたいならasyncio.ensure_future
を用いて書く必要がある4。
動かなかったコード
以下は特に読む必要はなし?躓きを記録することで何か意見がもらえるかもしれないし、他の躓いている人の何か助けになるかもしれないので、上記コードに至る失敗例をいくつか。
その1
元々このbotには、botを起動するmain.pyと同じ階層に"kill"という名前のファイルが生成されるとbotを終了する処理が記述されていた。これは、1秒毎にディレクトリを見て、もし"kill"という名前のファイルがあれば終了処理を行うという形で記述されている。まさに、今やろうとしている処理と近い。このメソッドの名前を仮にwatcher
とすると、threading
ライブラリを用いて以下のようなコードになっていた。
import threading
import discord
client = discord.Client()
# (ログイン後のbotの処理を記述するコードもろもろ)
def watcher():
while True:
if find_kill: # 'kill'というファイルがあれば
break
else:
time.sleep(1)
terminate_bot()
t = threading.Thread(target=watcher)
t.start()
client.run(token)
並列処理することで、ディレクトリの監視とbotの処理を分けている。そのため、watcher
内のtime.sleep
の影響をbotは受けない。
そこでこのコードを参考にできないかと考え以下のコードを書いた。
import threading
import discord
client = discord.Client()
# (ログイン後のbotの処理を記述するコードもろもろ)
def watcher():
foo()
def greeting_routine():
while True:
client.send_message(channel, 'おはよう')
time.sleep(10)
t = threading.Thread(target=watcher)
t.start()
t2 = threading.Thread(target=greeting_routine)
t2.start()
client.run(token)
これはうまくいかない。そもそもsend_message
はawait
なしでは動かないようになっているからだ。
その2
それでは、と、以下のように書き換える。これもうまく動かない。(この辺りのコードはいろいろ試行錯誤していてうろ覚えなので、間違っているかもしれない)
import threading
import discord
client = discord.Client()
# (ログイン後のbotの処理を記述するコードもろもろ)
def watcher():
foo()
async def greeting_routine():
while True:
await client.send_message(channel, 'おはよう')
time.sleep(10)
t = threading.(target=watcher)
t.start()
t2 = threading.(target=greeting_routine)
t2.start()
client.run(token)
これもうまくいかない。time.sleep(10)
では全体の処理が止まってしまうとか以前に、client.run(token)
以前にgreeting_routine()
が実行されるため、当然ながら発言先チャンネルの情報が取得できずエラーになる。
これを回避するために、サーバにログイン済みかどうかを調べるメソッドを用いて分岐させることも考えたが、リファレンスを読んでもそうしたメソッドが見当たらなかった上5に、そもそもclient.run(token)
より前で、サーバにログインしていることを前提としたコードを実行するのはセンスがなさすぎると思い他の実装を模索する。
その3
client.run(token)
以下で実行しようとする。client.run(token)
はbotが落ちるまで抜けないので、当然所望の動作は得られない。
※これに限らずclient.run(token)
以下に記述したコードは基本実行されないと心得るべき。
import threading
import discord
client = discord.Client()
# (ログイン後のbotの処理を記述するコードもろもろ)
def watcher():
foo()
async def greeting_routine():
while True:
await client.send_message(channel, 'おはよう')
time.sleep(10)
t = threading.Thread(target=watcher)
t.start()
client.run(token)
t2 = threading.Thread(target=greeting_routine)
t2.start()
その4
この辺りで、botに何かさせるのだから、client.run(token)
したあとに実行される部分にコードを追加するしかないと悟る。
リファレンスを読み直すうちに、on_ready
以下に記述すればログイン直後にコードが確実に実行されることに気づき以下のコードを書く。
import threading
import discord
client = discord.Client()
@client.event
async def on_ready():
while True:
await client.send_message(channel, 'おはよう')
time.sleep(10)
client.run(token)
無事「おはよう」が10秒毎に発言されるようになったものの、今度は他のコマンドを受け付けない事態になった。これは先にも触れたtime.sleep(10)
が原因。await asyncio.sleep(10)
に直すことで事なきを得た。
-
discord.pyとbotの導入については、こちらとかが詳しい
また、筆者はasync
とかawait
とかに疎く、なんかつけないといけないやつ程度の認識しかないことをご承知ください。(勉強したくはありますが難しい……) ↩ -
記事タイトルよりも範囲が広いですが、具体的なワードにしたほうが困っている人の役に立つと思い敢えてタイトルはこうしています ↩
-
率直に言って、この辺りは理解が不十分である。こちらを見る限りでは、
loop=asyncio.get_event_loop()
とloop.run_forever()
とかloop.run_until_complete(asyncio.gather(greeting_gm(), greeting_gn()))
みたいなのを書いてやる必要がありそうなのだが、そのように記述すると This eventloop is already running. というエラーが出る。どうもdiscord.pyの中でイベントループを生成しているということのようだが、discord.pyを読むには至っていない。(仮にdiscord.pyの方でイベントループを生成しているなら、asyncio.ensure_future
だけで動くのも理解できる気はする) ↩ -
これは私の読解力が低いだけで、存在するのかもしれない。存在しないほうがおかしいとも思う。 ↩