2018年11月8日 | ブログ記事

Discordのbotを書いている話【アドベントカレンダー2018 15日目】

ko-cn

この記事はtraP Advent Calender 2018 15日目の記事です。


はじめに

自己紹介

ブログでは初めまして。現在絶賛幽霊部員中の こーちゃん です。
アイドルマスターシンデレラガールズのオタクです。担当は橘ありすちゃんです。
天使
かわいいですね。
今週末(11/10,11)にメットライフドームで6thライブが行われます。現地で私と握手!

閑話休題

記事について

私が(ある程度)真面目にプログラミングの勉強を始めてから2ヶ月ほど経ちました。その間に、PythonでDiscordのbotを制作しました。それについて多少書きました。
ちなみに、実は既にこのような記事があるのですが、言語が違ったり目的が違ったりするので許してください。
また、私はプログラミング初心者なので処理が汚かったり知識不足が目立つ点が多々あると思います。なにか気づいた点がありましたら教えていただけると嬉しいです。

下準備

作成したbot概要

モバマス(mobage版アイドルマスターシンデレラガールズ)のイベントに関するリマインダーを実装しました。今回作成したbotの目玉機能です。というかそのために作りました。
このイベントですが、参加できる時間が決まっていたり、効率が上がる時間が決まっていたりと時間に関して意識することが多いです。忘れていて大損をすることが多いので、それを減らすためにリマインダーを作りました。
機能一覧やソースコードへのリンクはこちらに記載しています。

環境について

開発に使用したOSは Windows10、Pythonのバージョンは 3.6.6。使用したパッケージであるdiscord.pyのバージョンは 0.16.12です。

環境構築

環境構築はこちらのページを参考にさせていただきました。

discord.pyとは

discord.pyは、PythonでdiscordのAPIを簡単に利用できるようにしてくれるパッケージです。公式のドキュメントはこちらにあります。また、こちらこちらも必要に応じて参考にしました。(情報が微妙に違う?)

bot本体について

コードの骨組み

コード全体としては基本的に以下のような構成になります。

import discord # discord.py をインポート

client = discord.Client() # bot自体を表すオブジェクト

# 起動時に実行される処理
@client.event
async def on_ready():
    処理

# メッセージ送信時に読み込まれる処理
@client.event
async def on_message(message):
    処理

# botの接続と起動 tokenはbotのトークン
client.run('token')

コマンド実装概説

discordのbotで一般的なのは、テキストチャンネルで/command!commandといったようなメッセージを送信することで対応した動作を引き起こすというタイプのコマンドです。
このように、特定のメッセージに反応させたい場合は

@client.event
async def on_message(message):
    処理

というブロックの中に処理を記述していきます。受け取ったメッセージの文面のstrはmessage.content、送信されたチャンネルを表すオブジェクトはmessage.channel......といったように、様々な情報が属性として取得できます。
/nekoというメッセージに反応する処理を追加したい場合は

@client.event
async def on_message(message):
    if message.content.startswith('/neko'):
        処理

などと書けば実装できます。

botに発言させる

先程のコードでおわかりかと思いますが、discord.pyはasyncioを多用しています。発言はclient.send_message(送信先チャンネル, 内容)というメソッドを用いるのですが、async defで定義された処理では

await client.send_message()

というようにすることでメッセージを送信できます。

@client.event
async def on_message(message):
    if message.content.startswith('/neko'):
        await client.send_message(message.channel, 'にゃーん')

とすれば、/nekoというメッセージが送信されたら、botがそのチャンネルににゃーんと発言してくれます。同様にして

    if 'プニキ' in message.content:
       await client.send_message(message.channel, 'https://kids.yahoo.co.jp/games/sports/013.html')

などもできますね。ヤランデイイ

マルチスレッドでリマインダーを作る

私が一番苦労したのがここでした。指定時刻にリマインドをしたい場合はschedパッケージを使うのが一般的です。指定時刻に指定の処理を実行してくれるのですが、この際に実行まで待機時間が発生してしまいます。
リマインダーが実行されるまで他のコマンドが使えないというのはあまりに不便なので、メインの処理をしているのとは別のスレッドを作ってリマインダーを動かすことにしました。
恐らくasyncioをきちんと使えば別スレッドを作る必要はないと思うのですが、どうしてもうまく行かなかったのでこちらの方針にしました。(近いうちにもっと詳しく調べます。)
例えば、/remindの受信から5分後にリマインダーですという発言を標準出力にする、というコマンドを実装するなら、

import discord
import threading
import time
import sched
import asyncio
client = discord.Client()

def remind():
    scheduler = sched.scheduler(time.time, time.sleep)
    scheduler.enter(60*5, 1, print('リマインダーです'))
    scheduler.run()

@client.event
async def on_message(message):
    if message.content.startswith('/remind'):
        thread = threading.Thread(target=remind)
        thread.start()

client.run('token')

とすれば良いですね。しかし、これをdiscordで発言させようと思うとなかなか難しいです。

import discord
import threading
import time
import sched
import asyncio
client = discord.Client()

def send_remind(channel):
    await client.send_message(channel, 'リマインダーです')

def remind(channel):
    scheduler = sched.scheduler(time.time, time.sleep)
    scheduler.enter(60*5, 1, send_remind, args=(channel,))
    scheduler.run()

@client.event
async def on_message(message):
    if message.content.startswith('/remind'):
        thread = threading.Thread(target=remind, args=(message.channel,))
        thread.start()

client.run('token')

と書きたいところですが、send_remindasyncではないのでこれはうまく動きません。修正しようと色々試してみた結果、解決方法として以下の物を見つけました。

import discord
import threading
import time
import sched
import asyncio
client = discord.Client()
loop = asyncio.get_event_loop() # イベントループを取得

def send_remind(channel):
    asyncio.ensure_future(client.send_message(channel, 'リマインダーです'), loop=loop) # 実行するイベントループを指定

def remind(channel):
    scheduler = sched.scheduler(time.time, time.sleep)
    scheduler.enter(60*5, 1, send_remind, args=(channel,))
    scheduler.run()

@client.event
async def on_message(message):
    if message.content.startswith('/remind'):
        thread = threading.Thread(target=remind, args=(message.channel,))
        thread.start()

client.run('token')

発言をawaitを用いたものではなくasyncio.ensure_futureを用いたものに差し替えました。その際、引数としてイベントループを渡してあげると別スレッドでうまく動いてくれます。

このように処理を変えた結果、リマインダーとしての機能を果たすことができるようになりました。やったぜ。
恐らくもっとまともな実装方法があると思うので、教えていただけると嬉しいです。

他にも

イベント名と開始日を入力しただけで自動でリマインダーを設定してくれる機能(クラス等をイメージする練習ができた)や、アイドル名を指定するとプロフィールを教えてくれる機能(webスクレイピングの練習になった)などを実装しました。ここらへんの説明は余裕があれば別の機会にしたいと思います。

終わりに

プログラミング知識がかなり少ないところからなんとかbotを書きました。おかげでイベントを忘れることがかなり減ったので自分では満足です。
他にもやってみたい事や改善できる事などが無数にあるので、もっと色々なことに挑戦してみたいです。
そして、知識不足故にかなり下手くそなコードになってしまいました。こうした方がいいよ!こういう方法もあるよ!などありましたら是非教えて下さい。


明日は drun さんの記事です。お楽しみに!

この記事を書いた人
ko-cn

デレマスのオタクです

この記事をシェア

このエントリーをはてなブックマークに追加

関連する記事

2018年11月2日
pythonでwebスクレイピングしよう【アドベントカレンダー2018 9日目】
idaten
2018年11月16日
最近作ってるゲームの話 【アドベントカレンダー2018 23日目】
ryuon
2018年11月15日
転倒数と15パズル【アドベントカレンダー2018 22日目】
RyoTei
2018年11月15日
君だけの「にゃーんボタン」を作ろう!
nagatech
2018年11月14日
Android Studioの紹介
SoLA
2018年11月14日
MagicaVoxelを触ってみたお話
topaz

活動の紹介

カテゴリ

タグ