はじめに
大学のサークルの毎週の出席管理のためにDiscordのBotを制作しました。
Discord.pyを使ったスケジューラーの動作があまり見つからなかったので、記事にしてみました。
初めてのPythonなので、もしも間違っている所等ありましたら、コメントよろしくお願いします。
動作環境
- Python 3.6.6
- discord.py 0.16.12
下準備
他に詳しく書かれているDiscordのBot作成記事があるのでそちらを確認してください。
https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f
概要
- StartTime及びEndTimeに設定されている時間で参加投票ができます。
また、WeekDayは、0〜6で月〜日に対応してます。 - DebugIDに設定されているチャンネルにコマンドを打ち込むと返信が返ってきます。
- /start { WeekDay } { HourTime } { MinutesTime }
- 出席確認の投票開始タイミングを変更します。
- /end { WeekDay } { HourTime } { MinutesTime }
- 出席確認の投票終了タイミングを変更します。
- /check {start | end | people}
- 後に書いたstartかendでその設定時間を確認できます。peopleは、現在時点での参加人数を返します。
- /start { WeekDay } { HourTime } { MinutesTime }
プログラムの作成
Pythonには、Scheduleというライブラリがあるみたいですが、最後にDiscordに投稿するのに非同期で送らなければいけなくその方法がわからなかったため、datetimeを使って独自に実装してます。
import discord
import asyncio
from datetime import datetime
class ScheduleTime:
def __init__(self, weekday, hourTime, minutesTime):
self.Weekday = weekday
self.HourTime = hourTime
self.MinutesTime = minutesTime
def edit_Weekday(self, weekday):
self.Weekday = weekday
pass
def edit_HourTime(self, hourTime):
self.HourTime = hourTime
pass
def edit_MinutesTime(self, minutesTime):
self.MinutesTime = minutesTime
pass
def edit_AllValue(self, weekday, hourTime, minutesTime):
self.edit_Weekday(int(weekday))
self.edit_HourTime(int(hourTime))
self.edit_MinutesTime(int(minutesTime))
pass
def is_match(firstValue, secondValue):
if firstValue.Weekday != secondValue.Weekday :
return False
if firstValue.HourTime != secondValue.HourTime :
return False
if firstValue.MinutesTime != secondValue.MinutesTime :
return False
return True
token = '' # DiscordBotのトークン
client = discord.Client() # ディスコードの接続に使用するオブジェクト
DebugId = '' # コマンドなどを入力するチャンネル
DefaultId = '' # 呟くチャンネル
StartTime = ScheduleTime(0,10,0) # 集計開始
EndTime = ScheduleTime(2,23,59) # 集計おわり
week = ['月','火','水','木','金','土','日']
commandList = {
'/help':0,
'/start':3,
'/end':3,
'/check':1
}
part = 0
isStartSended = False
isEndSended = False
@asyncio.coroutine
def SendMsg(Channel, msg):
print('reply = '+ msg)
if msg != '':
yield from client.send_message(Channel, msg)
pass
@asyncio.coroutine
async def check_for_reminder():
while True:
await asyncio.sleep(3)
now = datetime.now()
currentTime = ScheduleTime(now.weekday(), now.hour, now.minute)
if ScheduleTime.is_match(StartTime, currentTime):
global isStartSended
global part
if not isStartSended:
part = 0
StartText = '今週の活動は、{}月{}日です。\n参加する方は、リアクションをお願いします。\n投票は{}曜日に無慈悲に締め切ります。'.format(datetime.now().month, datetime.now().day + 4, week[int(EndTime.Weekday)])
await SendMsg(DefaultChannel, StartText)
isStartSended = True
else:
isStartSended = False
if ScheduleTime.is_match(EndTime, currentTime):
global isEndSended
if not isEndSended:
global part
EndText = '今週の参加登録を締め切りました。参加人数は{}人です!!'.format(part)
await SendMsg(DefaultChannel, EndText)
isEndSended = True
else:
isEndSended = False
@asyncio.coroutine
def main_task():
print(datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
yield from client.start(token)
pass
def botCommand(command, contents):
SendMsg =''
if command not in commandList:
return 'そんなコマンドはないよ? /help で確認してね'
if len(contents) != commandList[command]:
return '要素の数がおかしいよ?もう一度確認してね!'
if command == '/start':
StartTime.edit_AllValue(contents[0],contents[1],contents[2])
SendMsg = '投票開始の時間を{}曜日の{}時{}分にセットしました。'.format(week[int(contents[0])],contents[1],contents[2])
elif command == '/end':
EndTime.edit_AllValue(contents[0],contents[1],contents[2])
SendMsg = '投票終わりの時間を{}曜日の{}時{}分にセットしました。'.format(week[int(contents[0])],contents[1],contents[2])
elif command == '/check':
if contents[0] == 'start':
SendMsg = '投票開始時間は、{}曜日{}時{}分に設定されています。'.format(week[int(StartTime.Weekday)],StartTime.HourTime,StartTime.MinutesTime)
elif contents[0] == 'end':
SendMsg = '投票終了時間は、{}曜日{}時{}分に設定されています。'.format(week[int(EndTime.Weekday)],EndTime.HourTime,EndTime.MinutesTime)
elif contents[0] == 'people':
global part
SendMsg = '現在の参加人数は、{}人です。'.format(part)
else:
SendMsg = 'コマンドを確認してね!'
pass
else :
SendMsg = 'コマンドを確認してね!'
return SendMsg
# Discord Event
@client.event
async def on_ready():
global DebugChannel
global DefaultChannel
DebugChannel = client.get_channel(DebugId)
DefaultChannel = client.get_channel(DefaultId)
await SendMsg(DebugChannel, 'ログインしました')
pass
@client.event
async def on_message(message):
# メッセージの送り主がBotならなにもしない
if client.user == message.author:
return
contents = message.content.split(' ')
bot_command = str(contents[0]).lower()
contents = contents[1:]
contents = [str(content) for content in contents]
reply = botCommand(bot_command, contents)
if message.channel.name == "bot":
await SendMsg(message.channel, reply)
pass
@client.event
async def on_reaction_add(reaction, user):
global part
if reaction.message.author == client.user:
part += 1
print('part = '+ str(part))
await SendMsg(DefaultChannel, f'{user.mention} 参加登録しました。')
@client.event
async def on_reaction_remove(reaction, user):
global part
if reaction.message.author == client.user:
part -= 1
await SendMsg(DefaultChannel, f'{user.mention} 参加をキャンセルしました。')
pass
loop = asyncio.get_event_loop()
try:
asyncio.async(main_task())
asyncio.async(check_for_reminder())
loop.run_forever()
except:
loop.run_until_complete(client.logout())
finally:
loop.close()
プログラムの詳細
1. Discord.pyのイベント
Discord.pyの基本的なイベントは、
@client.event
async def hoge()
になります。hogeにイベント名を書きます。イベントについては、
リファレンス
を見ると色々載っています。
日本語翻訳はこちら(一部未翻訳)
2. コマンドの読み取り
contents = message.content.split(' ')
bot_command = str(contents[0]).lower()
contents = contents[1:]
メッセージを受け取ったら、半角スペースで分割し、最初とそれ以外に分割しています。
一応大文字は小文字に直し、最初から空白までの区間の文字をコマンドとして文字列判定を行なっています。
3. client.run()と自作ループの同時処理
このプログラムの中で一番間違っているか心配な部分です。
loop = asyncio.get_event_loop()
try:
asyncio.async(main_task())
asyncio.async(check_for_reminder())
loop.run_forever()
except:
loop.run_until_complete(client.logout())
finally:
loop.close()
asyncでDiscord.pyのstart()と自作のReminderループを同時(?)に回しています。
色々な解説に出てくるDiscord.pyのclient.run()は、そのままループに入ってしまい他の処理ができません。
調べたら、loop.run_untill_complete()で分割する方法が見つかりましたが、それでもうまくいかなかったので、
asyncに書き換えてあります。pythonの非同期処理については全くわからず、とりあえず動く様に組んだだけです…。
最後に
Discord用のBotを作りがてらPythonを勉強しましたが、かなり面白いと思いました。
今後としては、半期(6ヶ月)ごとの出席数のカウントをCSVに書き出して部費の金額割り出したり、
会計などの集計システムも組み込めたらと思っています。
そのあたりもできたら記事にしたいです。ここまで読んでくださってありがとうございます!