LoginSignup
22
22

More than 3 years have passed since last update.

discord.pyでbotに一定時間ごとに発言させる【async版】

Last updated at Posted at 2018-11-07

初投稿です。ご意見大歓迎です。質問等ありましたらコメント頂ければお受けできる範囲でお受けします。

※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
また、筆者はasyncとかawaitとかに疎く、なんかつけないといけないやつ程度の認識しかないことをご承知ください。(勉強したくはありますが難しい……)

この記事でできるようになること

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に投稿する」こともできる。

main.py
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が「おやすみ」と言ってくれることはない。

main_bad.py
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ライブラリを用いて以下のようなコードになっていた。

main_ex.py
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は受けない。
そこでこのコードを参考にできないかと考え以下のコードを書いた。

main_bad1.py
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_messageawaitなしでは動かないようになっているからだ。

その2

それでは、と、以下のように書き換える。これもうまく動かない。(この辺りのコードはいろいろ試行錯誤していてうろ覚えなので、間違っているかもしれない)

main_bad2.py
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)以下に記述したコードは基本実行されないと心得るべき。

main_bad3.py
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以下に記述すればログイン直後にコードが確実に実行されることに気づき以下のコードを書く。

main_bad4.py
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)に直すことで事なきを得た。


  1. discord.pyとbotの導入については、こちらとかが詳しい 

  2. 記事タイトルよりも範囲が広いですが、具体的なワードにしたほうが困っている人の役に立つと思い敢えてタイトルはこうしています 

  3. 逆に以下が気にならない方であれば、こちらのIFTTTとWebhookを用いて連携する記事が参考になるかもしれません 

  4. 率直に言って、この辺りは理解が不十分である。こちらを見る限りでは、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だけで動くのも理解できる気はする) 

  5. これは私の読解力が低いだけで、存在するのかもしれない。存在しないほうがおかしいとも思う。 

22
22
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
22