DiscordサーバのVC滞在時間を常時計測するBotを作成する

本記事は，東京学芸大学 櫨山研究室 Advent Calendar 2021の記事ではなく，勝手に書いてる記事です．

# 現在の状況
Herokuの無料枠が無くなって，現在このアプリも動いていないです！移行先を現在考えています．

# はじめに
こんにちは，東京学芸大学 教育学部 櫨山(ソフトウェア工学)研究室所属学生のsatohgfです．
みなさん，卒論書いてますか？僕は1文字も書いていません．
昨年2020年は研究室でのアドベントカレンダーがあり，[偉大なる先輩](https://qiita.com/wf-yamaday)や同期が[記事](https://qiita.com/hanehaneland/items/dff2af13b3dee2982036)を書いていました．今年は研究室のアドベントカレンダーがないのですが，卒論からの逃避でDiscordのBotを作りました．せっかくなので記録に残しておきます．

# 開発背景
みなさんDiscordは知っていますか？ゲーマーを中心に人気の(ボイス)チャットアプリですね．以前に友達と
>「DiscordのVCに入ってた時間を計測するBotがあれば面白いよね～」

なんて話をしてたので，卒論時期にも関わらずVCに入り浸る自分への戒めのためにも，Discordを常時監視して滞在時間を計測するBotを開発する運びになりました．

# 環境など
### 開発環境
* Windows10PC(はるか昔に自作)
* Python 3.9.7
* discord.py 1.7.3
* MySQL 8.0.25
* mysql-connector-python 8.0.27

### 動作環境
* Heroku
* ClearDB MySQL

#動作イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257934/3ec10cce-1b5c-08e2-e028-e9cc7d07e9c0.png)



こんな感じの処理を繰り返して，ユーザごとのVC滞在時間を計測します．

#DBをつくる
まずはデータ保存のためのDB設計ですが，良さげに書いていきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257934/eea9a3f7-1b7d-f454-5935-001ef37d2684.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257934/c1f1073c-2c10-db45-a750-5b18e782184b.png)
ユーザ関連のデータを置いておくテーブルと，ユーザがVCに入った時間を置いておくテーブルを作りました．

#コーディング
Discord.pyをHerokuで常時作動させるときの[テンプレート](https://github.com/DiscordBotPortalJP/discordpy-startup)がとても便利でしたので，これを使ってテンプレートを作成します．
テンプレートを作り終わったら，上のフローチャットっぽくなるようにコードを書いていきます．

```python:discordbot.py

from discord.ext import commands
from os import getenv, name
import discord
import time
import mysql.connector

client = discord.Client()
DB_HOST = getenv("DB_HOST")
DB_USER = getenv("DB_USER")
DB_PASS = getenv("DB_PASS")
DB_NAME = getenv("DB_NAME")
@client.event
async def on_voice_state_update(member , before ,after):
    if before.channel != after.channel:
        if after.channel is not None:
            conn = mysql.connector.connect(
                host = DB_HOST,
                user = DB_USER,
                password = DB_PASS,
                database = DB_NAME
            )
            cur = conn.cursor()
            cur.execute("select * from users where user_id = %s",(member.id, ))
            rows = cur.fetchall()
            if not rows:
                cur.execute("INSERT INTO users VALUES (%s, %s ,%s ,%s)",(member.id, member.name,"0",member.guild.id ))
                conn.commit()
                print("new user recorded,He/She is " + member.name)
            cur.execute("INSERT INTO user_entertimes VALUES (%s, %s)",(member.id, str(time.time())))
            conn.commit()
            conn.close()

        if before.channel is not None:
            conn = mysql.connector.connect(
                host = DB_HOST,
                user = DB_USER,
                password = DB_PASS,
                database = DB_NAME
            )
            cur = conn.cursor()
            cur.execute("select * from user_entertimes where user_id = %s",(member.id, ))
            rows = cur.fetchall()
            enter_time = rows[0][1]
            print(enter_time)
            cur.execute("select * from users where user_id = %s",(member.id, ))
            staytime_rows = cur.fetchall()
            stay_time = staytime_rows[0][2]
            delta_stay_time = float(time.time()) - float(enter_time) + float(stay_time)
            cur.execute("UPDATE users SET user_staytime = %s WHERE user_id = %s AND user_guild_id = %s",(str(delta_stay_time),member.id,member.guild.id ))
            cur.execute("DELETE FROM user_entertimes WHERE user_id = %s",(member.id, ))
            conn.commit()
            conn.close()
token = getenv('DISCORD_BOT_TOKEN')
client.run(token)
```
いい感じですね．mysql.connectorをインポートしているので，requirements.txtも更新しておきます．

```text:requirements.txt
discord.py[voice]>=1.7.3
mysql-connector-python==8.0.27
```
ここまででDBにユーザの滞在時間を記録する機能までできました．

#ユーザの滞在時間を投稿する
せっかくなので，ユーザの滞在時間を任意のチャンネルに投稿する機能もつけておきます．
特定のワードを検出したら，チャンネルにユーザの滞在時間情報を流す機能をdiscordbot.pyに追記していきます．

```python:discordbot.py
@client.event
async def on_message(message):
    if message.content.startswith("/nekoWake"):
        await print_time(message.guild.id,message.channel.id)


async def print_time(guild_id,mes_ch_id):
    conn = mysql.connector.connect(
                host = DB_HOST,
                user = DB_USER,
                password = DB_PASS,
                database = DB_NAME
    )
    cur = conn.cursor()
    cur.execute("select * from users WHERE user_guild_id = %s ORDER BY CAST(user_staytime as signed) DESC",(str(guild_id),))
    rows = cur.fetchall()
    mess = "前回からのサーバ滞在時間報告です．\n"
    count = 1
    for row in rows:
        conn = mysql.connector.connect(
                host = DB_HOST,
                user = DB_USER,
                password = DB_PASS,
                database = DB_NAME
        )
        cur = conn.cursor()
        stay_sec = round(float(row[2]))
        stay_hours = stay_sec // 3600
        stay_mins = (stay_sec - stay_hours * 3600) // 60
        stay_sec = stay_sec - stay_hours * 3600 - stay_mins * 60 
        this_mess = ("第" + str(count) + "位は " + row[1] + " さん．滞在時間は" + str(stay_hours) + "時間" + str(stay_mins) + "分" + str(stay_sec) + "秒でした．\n") 
        mess += this_mess
        count += 1
        cur.execute("UPDATE users SET user_staytime = %s WHERE user_id = %s",("0",row[0] ))
        conn.commit()
        conn.close()
    print(mess)
    bot_room = client.get_channel(mes_ch_id)
    await bot_room.send(mess)
```
できあがりです．

# 運用
実際に色々こねくり回したコードとかは[Github](https://github.com/sato064/Haizin_kanshi_cat)にあります．
このBotを導入したい方は[こちらからどうぞ](https://discord.com/api/oauth2/authorize?client_id=918712541868359730&permissions=2048&scope=bot)，ただし何が起きても責任は持てません．
Discordのテキストチャンネルで

```
/nekoWake
```
と唱えるとネコが前回の起動からの滞在時間を報告します．他の方が作られた[定期投稿Bot](https://marukot-ch.hatenablog.com/entry/2021/08/17/215055)などと組み合わせると，定期的に滞在時間を投稿してくれるので，サーバの治安維持にどうぞ．

# おわり
本記事では，Discord.pyで作成したBotをHerokuにデプロイしてサーバの滞在時間を常時監視するシステムについて扱いました．
良いDiscordライフをお過ごしください．