Discord.pyを利用したDiscordで出席確認をするBot

## はじめに
  大学のサークルの毎週の出席管理のために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は、０〜６で月〜日に対応してます。  
  - DebugIDに設定されているチャンネルにコマンドを打ち込むと返信が返ってきます。
    - /start &#123; WeekDay &#125; &#123; HourTime &#125; &#123; MinutesTime &#125;
      - 出席確認の投票開始タイミングを変更します。
    - /end &#123; WeekDay &#125; &#123; HourTime &#125; &#123; MinutesTime &#125;
      - 出席確認の投票終了タイミングを変更します。
    - /check &#123;start | end | people&#125;
      - 後に書いたstartかendでその設定時間を確認できます。peopleは、現在時点での参加人数を返します。

## プログラムの作成
Pythonには、Scheduleというライブラリがあるみたいですが、最後にDiscordに投稿するのに非同期で送らなければいけなくその方法がわからなかったため、datetimeを使って独自に実装してます。

```python
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の基本的なイベントは、

``` python
@client.event
async def hoge()
```
になります。hogeにイベント名を書きます。イベントについては、
[リファレンス](https://discordpy.readthedocs.io/en/latest/api.html#event-reference )
を見ると色々載っています。  
日本語翻訳は[こちら](https://discordpy.readthedocs.io/ja/latest/)(一部未翻訳)  

### 2. コマンドの読み取り

```python
contents = message.content.split(' ')
bot_command = str(contents[0]).lower()
contents = contents[1:]
```

メッセージを受け取ったら、半角スペースで分割し、最初とそれ以外に分割しています。  
一応大文字は小文字に直し、最初から空白までの区間の文字をコマンドとして文字列判定を行なっています。

### 3. client.run()と自作ループの同時処理
このプログラムの中で一番間違っているか心配な部分です。

``` python

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に書き出して部費の金額割り出したり、  
会計などの集計システムも組み込めたらと思っています。  
そのあたりもできたら記事にしたいです。ここまで読んでくださってありがとうございます！

## 参考文献
- [Pythonで実用Discord bot(discord.py解説)](https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f)  
- [AtomでPythonの開発環境を整える](https://qiita.com/suecharo/items/dbc525dd5f39bdb8403c)  
- [API Reference &mdash; discord.py 0.16.12 documentation](https://discordpy.readthedocs.io/en/latest/api.html#event-reference )