概要
Discord Botを改修した。
以前学習時間記録Discord Botを作成した。
特定のボイスチャンネルを監視し、ユーザ入退室時間を記録、滞在時間を計算して通知するbotである。
上記ではプロセス上の変数に情報を保存していたが入室時刻はDBに保存してデータが消失しないようする。
方法
Bot作成
以下の記事を参考にDiscord Botを作成する。
DB作成
HerokuにDBを作成する。
Botのコード追加・修正
(コード全文は一番下に掲載)
PythonからHerkouDB接続
PythonでHeroku上のDBに接続する例は以下の記事を参考にした。
以下の記事ではコード内にDB接続情報を埋め込んでいる。
DB接続情報の確認はHerokuの[Setting]→[Database Credentials]から確認できる。
(上記DB作成の記事にあったHerokuCLIの接続情報と同じ場所)
追加したDB接続用コードは以下。
- DB接続情報は以前のDiscord Bot Tokenと同じでheroku上の環境変数に登録している
-
execute
で実行するSQL分のパラメータは個数に関係なくリスト(orタプル)にしないといけないらしい:参考 - INSERT, DELETE後はcommitを忘れない
import psycopg2
def sqlInit():
# 設定値
users = getenv('HEROKU_POSTGRESQL_USERS') # DBにアクセスするユーザー名(適宜変更)
dbnames = getenv('HEROKU_POSTGRESQL_DBNAMES') # 接続するデータベース名(適宜変更)
passwords = getenv('HEROKU_POSTGRESQL_PASSWORDS') # DBにアクセスするユーザーのパスワード(適宜変更)
host = getenv('HEROKU_POSTGRESQL_HOST') # DBが稼働しているホスト名(適宜変更)
port = getenv('HEROKU_POSTGRESQL_PORT') # DBが稼働しているポート番号(適宜変更)
# PostgreSQLへ接続
conn = psycopg2.connect("user=" + users +" dbname=" + dbnames +" password=" + passwords, host=host, port=port)
return conn
def sqlExit(conn):
# PostgreSQLの接続を終了
conn.close()
return
def INSERT(conn, id, time):
cur = conn.cursor()
cur.execute("INSERT INTO pretime (id, time) VALUES (%s, %s)", [id, time])
conn.commit()
cur.close()
return
def SELECT(conn, id):
cur = conn.cursor()
cur.execute("SELECT * FROM pretime WHERE id = %s", [id])
result = cur.fetchone()
cur.close()
return result
def DELETE(conn, id):
cur = conn.cursor()
cur.execute("DELETE FROM pretime WHERE id = %s", [id])
conn.commit()
cur.close()
return
イベント処理
改修したコードは以下。
-
SEND_CHANNEL_ID
,WATCH_CHANNEL_ID
も環境変数化した - 以前はプログラム内変数に保存していた情報をDB保存にした
import discord
import datetime
from os import getenv
client = discord.Client()
SEND_CHANNEL_ID = int(getenv('SEND_CHANNEL_ID')) # 通知を送信するチャンネルID
WATCH_CHANNEL_ID = int(getenv('WATCH_CHANNEL_ID')) # 入退室を監視するチャンネルID
DIFF_JST_FROM_UTC = 9
@client.event
async def on_ready():
print('Logged in as')
print(client.user.name)
print(client.user.id)
print('------')
@client.event
async def on_voice_state_update(member, before, after):
# 入室時
if before.channel is None:
if after.channel.id != WATCH_CHANNEL_ID:
return
# 入室時刻を取得
dt_now = datetime.datetime.utcnow() + datetime.timedelta(hours=DIFF_JST_FROM_UTC)
dt_now_text = dt_now.strftime('%Y-%m-%d %H:%M:%S')
# 入室者ID, 入室時刻をDBに保存
try:
conn = sqlInit()
INSERT(conn, str(member.id), str(dt_now_text))
sqlExit(conn)
send_text = f'**{member.name} ({member.nick})** \n 入室: {dt_now_text}'
except Exception as e:
send_text = f'{e}'
# 入室時は特に通知はしない(HerokuLogには出力する)
print(send_text)
# 退室時
elif after.channel is None:
if before.channel.id != WATCH_CHANNEL_ID:
return
# 退室時刻を取得
dt_now = datetime.datetime.utcnow() + datetime.timedelta(hours=DIFF_JST_FROM_UTC)
dt_now_text = dt_now.strftime('%Y-%m-%d %H:%M:%S')
# 退室者IDを用いてDBから入室時刻を取得
# 滞在時間を計算
try:
conn = sqlInit()
result = SELECT(conn, str(member.id))
if result is None:
print('error: result is None')
sqlExit(conn)
return
DELETE(conn, str(member.id))
sqlExit(conn)
dt_now_before = result[1]
dt_now_before_text = dt_now_before.strftime('%Y-%m-%d %H:%M:%S')
time_text = getDurationTimeText(dt_now_before, dt_now)
send_text = f'**{member.name} ({member.nick})** \n 入室: {dt_now_before_text}\n 退室: {dt_now_text}\n 勉強時間: {time_text}'
except Exception as e:
send_text = f'{e}'
print(send_text)
# 指定のテキストチャンネルに通知
send_channel = client.get_channel(SEND_CHANNEL_ID)
await send_channel.send(send_text)
# token にDiscordのデベロッパサイトで取得したトークンを入れてください
token = getenv('DISCORD_BOT_TOKEN')
client.run(token)
コード全文
import discord
import datetime
from os import getenv
import psycopg2
def sqlInit():
# 設定値
users = getenv('HEROKU_POSTGRESQL_USERS') # DBにアクセスするユーザー名(適宜変更)
dbnames = getenv('HEROKU_POSTGRESQL_DBNAMES') # 接続するデータベース名(適宜変更)
passwords = getenv('HEROKU_POSTGRESQL_PASSWORDS') # DBにアクセスするユーザーのパスワード(適宜変更)
host = getenv('HEROKU_POSTGRESQL_HOST') # DBが稼働しているホスト名(適宜変更)
port = getenv('HEROKU_POSTGRESQL_PORT') # DBが稼働しているポート番号(適宜変更)
# PostgreSQLへ接続
conn = psycopg2.connect("user=" + users +" dbname=" + dbnames +" password=" + passwords, host=host, port=port)
return conn
def sqlExit(conn):
# PostgreSQLの接続を終了
conn.close()
return
def INSERT(conn, id, time):
cur = conn.cursor()
cur.execute("INSERT INTO pretime (id, time) VALUES (%s, %s)", [id, time])
conn.commit()
cur.close()
return
def SELECT(conn, id):
cur = conn.cursor()
cur.execute("SELECT * FROM pretime WHERE id = %s", [id])
result = cur.fetchone()
cur.close()
return result
def DELETE(conn, id):
cur = conn.cursor()
cur.execute("DELETE FROM pretime WHERE id = %s", [id])
conn.commit()
cur.close()
return
def getDurationTimeText(dt_now_before, dt_now):
duration_time = dt_now_before - dt_now
duration_time_adjust = int(duration_time.total_seconds()) * -1
hours = duration_time_adjust // 3600
remain = duration_time_adjust - (hours * 3600)
minutes = remain // 60
seconds = remain - (minutes * 60)
time_text = '{:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds))
return time_text
client = discord.Client()
SEND_CHANNEL_ID = int(getenv('SEND_CHANNEL_ID')) # 通知を送信するチャンネルID
WATCH_CHANNEL_ID = int(getenv('WATCH_CHANNEL_ID')) # 入退室を監視するチャンネルID
DIFF_JST_FROM_UTC = 9
@client.event
async def on_ready():
print('Logged in as')
print(client.user.name)
print(client.user.id)
print('------')
@client.event
async def on_voice_state_update(member, before, after):
# 入室時
if before.channel is None:
if after.channel.id != WATCH_CHANNEL_ID:
return
# 入室時刻を取得
dt_now = datetime.datetime.utcnow() + datetime.timedelta(hours=DIFF_JST_FROM_UTC)
dt_now_text = dt_now.strftime('%Y-%m-%d %H:%M:%S')
# 入室者ID, 入室時刻をDBに保存
try:
conn = sqlInit()
INSERT(conn, str(member.id), str(dt_now_text))
sqlExit(conn)
send_text = f'**{member.name} ({member.nick})** \n 入室: {dt_now_text}'
except Exception as e:
send_text = f'{e}'
# 入室時は特に通知はしない(HerokuLogには出力する)
print(send_text)
# 退室時
elif after.channel is None:
if before.channel.id != WATCH_CHANNEL_ID:
return
# 退室時刻を取得
dt_now = datetime.datetime.utcnow() + datetime.timedelta(hours=DIFF_JST_FROM_UTC)
dt_now_text = dt_now.strftime('%Y-%m-%d %H:%M:%S')
# 退室者IDを用いてDBから入室時刻を取得
# 滞在時間を計算
try:
conn = sqlInit()
result = SELECT(conn, str(member.id))
if result is None:
print('error: result is None')
sqlExit(conn)
return
DELETE(conn, str(member.id))
sqlExit(conn)
dt_now_before = result[1]
dt_now_before_text = dt_now_before.strftime('%Y-%m-%d %H:%M:%S')
time_text = getDurationTimeText(dt_now_before, dt_now)
send_text = f'**{member.name} ({member.nick})** \n 入室: {dt_now_before_text}\n 退室: {dt_now_text}\n 勉強時間: {time_text}'
except Exception as e:
send_text = f'{e}'
print(send_text)
# 指定のテキストチャンネルに通知
send_channel = client.get_channel(SEND_CHANNEL_ID)
await send_channel.send(send_text)
# token にDiscordのデベロッパサイトで取得したトークンを入れてください
token = getenv('DISCORD_BOT_TOKEN')
client.run(token)