はじめに
こんにちは、hiroと言います。
この記事では、Discord.pyとherokuとGoogle Spread Sheet APIを用いたオンライン自習室環境を統制してくれるbotの作成方法について解説したいと思います。
完成品
前提知識
前提知識として、以下の記事を読んでからこの記事を読んでください。
Discord botの作成とheorkuの使い方
Pythonを使ったGoogleスプレッドシートの操作方法
作り方
環境
- Discordに時間計測したいボイスチャンネル、コマンドを受け付けるテキストチャンネルを作る
- Discordのロールに称号を追加する(弊サーバでは8つ)
- python実行ディレクトリに「img」フォルダを作成し、その中に各種ロールの画像を追加する
- Discordと紐づけたスプレッドシートの1シート目に月ごとの合計時間(1年分見れる)、2シート目~13シート目に日ごとの合計時間(1か月分見れる)を記録する為のシートを作る
ソースコード
time_record.py
TOKEN = "ここにDiscord botのTOKENを貼る"
import discord
import gspread
import datetime
from oauth2client.service_account import ServiceAccountCredentials
import os
intents = discord.Intents.all()
client = discord.Client(intents=intents)
pretime_dict = {}
acvc01_id = "時間計測するボイスチャンネルのIDを貼る" # 自習室
ch_id = "コマンドを受け付けるテキストチャンネルのIDを貼る" # 進捗報告
ID_ROLE1 = "称号のロールIDを貼る"
ID_ROLE2 = "称号のロールIDを貼る"
ID_ROLE3 = "称号のロールIDを貼る"
ID_ROLE4 = "称号のロールIDを貼る"
ID_ROLE5 = "称号のロールIDを貼る"
ID_ROLE6 = "称号のロールIDを貼る"
ID_ROLE7 = "称号のロールIDを貼る"
ID_ROLE8 = "称号のロールIDを貼る"
def connect_gspread(jsonf, key):
scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
credentials = ServiceAccountCredentials.from_json_keyfile_name(jsonf, scope)
gc = gspread.authorize(credentials)
SPREADSHEET_KEY = key
worksheet_sum = gc.open_by_key(SPREADSHEET_KEY).get_worksheet(0)
return worksheet_sum
def connect_gspread_individual(jsonf, key):
today = datetime.date.today()
print(today.day)
month = today.month
scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
credentials = ServiceAccountCredentials.from_json_keyfile_name(jsonf, scope)
gc = gspread.authorize(credentials)
SPREADSHEET_KEY = key
ss = gc.open_by_key(SPREADSHEET_KEY)
if month == 1:
worksheet = ss.get_worksheet(1)
elif month == 2:
worksheet = ss.get_worksheet(2)
elif month == 3:
worksheet = ss.get_worksheet(3)
elif month == 4:
worksheet = ss.get_worksheet(4)
elif month == 5:
worksheet = ss.get_worksheet(5)
elif month == 6:
worksheet = ss.get_worksheet(6)
elif month == 7:
worksheet = ss.get_worksheet(7)
elif month == 8:
worksheet = ss.get_worksheet(8)
elif month == 9:
worksheet = ss.get_worksheet(9)
elif month == 10:
worksheet = ss.get_worksheet(10)
elif month == 11:
worksheet = ss.get_worksheet(11)
elif month == 12:
worksheet = ss.get_worksheet(12)
return worksheet
jsonf = "スプレッドシートを操作する為のjsonファイルを貼る"
spread_sheet_key = "スプレッドシートのキー(URLから取得)を貼る"
ws = connect_gspread(jsonf, spread_sheet_key)
jsonf = "スプレッドシートを操作する為のjsonファイルを貼る"
spread_sheet_key = "スプレッドシートのキー(URLから取得)を貼る"
ws_idv = connect_gspread_individual(jsonf, spread_sheet_key)
def next_available_row(ws):
str_list = list(filter(None, ws.col_values(1)))
return str(len(str_list) + 1)
def next_available_col(ws_idv):
str_list = list(filter(None, ws_idv.row_values(1)))
return str(len(str_list) + 1)
@client.event
async def on_voice_state_update(member, before, after):
today = datetime.date.today()
# 監視するボイスチャンネル(チャンネルID)
announceChannelIds = acvc01_id
if ((before.self_mute is not after.self_mute) or (
before.self_deaf is not after.self_deaf)) and after.channel.id == announceChannelIds:
return
if (before.channel is None) and (after.channel.id == announceChannelIds):
pretime_dict[member.name] = datetime.datetime.now()
elif (after.channel is None) and (before.channel.id == announceChannelIds):
duration_time = pretime_dict[member.name] - datetime.datetime.now()
duration_time_adjust = int(duration_time.total_seconds()) * -1 / 60
time = round(duration_time_adjust)
month = int(today.month + 1) # スプレッドシート上での月(列)を取得
day = int(today.day + 1) #スプレッドシート上での日(行)を取得
if ws.find(member.name) is None:
next_row = next_available_row(ws)
ws.update_cell(next_row, 1, member.name)
ws.update_cell(next_row, month, time)
else:
cell = ws.find(member.name)
nowtime = ws.cell(cell.row, month).value
if nowtime == None:
nowtime = 0
cum_time = int(nowtime) + time
ws.update_cell(cell.row, month, cum_time)
if ws_idv.find(member.name) is None:
next_col = next_available_col(ws_idv)
ws_idv.update_cell(1, next_col, member.name)
ws_idv.update_cell(day, next_col, time)
else:
cell = ws_idv.find(member.name)
nowtime = ws_idv.cell(day, cell.col).value
if nowtime == None:
nowtime = 0
cum_time = int(nowtime) + time
ws_idv.update_cell(day, cell.col, cum_time)
@client.event
async def on_message(message):
today = datetime.date.today()
print(today.day)
month = int(today.month + 1) # スプレッドシート上での月(列)を取得
day = int(today.day + 1) #スプレッドシート上での日(行)を取得
role1 = message.guild.get_role(ID_ROLE1)
role2 = message.guild.get_role(ID_ROLE2)
role3 = message.guild.get_role(ID_ROLE3)
role4 = message.guild.get_role(ID_ROLE4)
role5 = message.guild.get_role(ID_ROLE5)
role6 = message.guild.get_role(ID_ROLE6)
role7 = message.guild.get_role(ID_ROLE7)
role8 = message.guild.get_role(ID_ROLE8)
if message.author.bot: # 拾ったメッセージがBotからのメッセージだったら(=Bot自身の発言だったら弾く)
return
elif message.content.startswith("!time"):
today = datetime.date.today()
cell = ws.find(message.author.name)
val = int(ws.cell(cell.row, month).value)
cell_idv = ws_idv.find(message.author.name)
val_idv = int(ws_idv.cell(day, cell_idv.col).value)
user_id = int(message.author.id)
current_role = message.author.roles
if val < 180:
user = await message.guild.fetch_member(user_id)
await user.remove_roles(role1)
await user.remove_roles(role2)
await user.remove_roles(role3)
await user.remove_roles(role4)
await user.remove_roles(role5)
await user.remove_roles(role6)
await user.remove_roles(role7)
await user.remove_roles(role8)
elif val >= 180 and val < 300:
user = await message.guild.fetch_member(user_id)
await user.remove_roles(role2)
await user.remove_roles(role3)
await user.remove_roles(role4)
await user.remove_roles(role5)
await user.remove_roles(role6)
await user.remove_roles(role7)
await user.remove_roles(role8)
await user.add_roles(role1)
current_role = role1
color_embed = 0x0067c0
fname="ロールの画像ファイル"
elif val >= 300 and val < 600:
user = await message.guild.fetch_member(user_id)
await user.remove_roles(role1)
await user.remove_roles(role3)
await user.remove_roles(role4)
await user.remove_roles(role5)
await user.remove_roles(role6)
await user.remove_roles(role7)
await user.remove_roles(role8)
await user.add_roles(role2)
current_role = role2
color_embed = 0x008000
fname="ロールの画像ファイル"
elif val >= 600 and val < 900:
user = await message.guild.fetch_member(user_id)
await user.remove_roles(role1)
await user.remove_roles(role2)
await user.remove_roles(role4)
await user.remove_roles(role5)
await user.remove_roles(role6)
await user.remove_roles(role7)
await user.remove_roles(role8)
await user.add_roles(role3)
current_role = role3
color_embed = 0xffde34
fname="ロールの画像ファイル"
elif val >= 900 and val < 1800:
user = await message.guild.fetch_member(user_id)
await user.remove_roles(role1)
await user.remove_roles(role2)
await user.remove_roles(role3)
await user.remove_roles(role5)
await user.remove_roles(role6)
await user.remove_roles(role7)
await user.remove_roles(role8)
await user.add_roles(role4)
current_role = role4
color_embed = 0xee710a
fname="ロールの画像ファイル"
elif val >= 1800 and val < 3000:
user = await message.guild.fetch_member(user_id)
await user.remove_roles(role1)
await user.remove_roles(role2)
await user.remove_roles(role3)
await user.remove_roles(role4)
await user.remove_roles(role6)
await user.remove_roles(role7)
await user.remove_roles(role8)
await user.add_roles(role5)
current_role = role5
color_embed = 0xff0000
fname="ロールの画像ファイル"
elif val >= 3000 and val < 6000:
user = await message.guild.fetch_member(user_id)
await user.remove_roles(role1)
await user.remove_roles(role2)
await user.remove_roles(role3)
await user.remove_roles(role4)
await user.remove_roles(role5)
await user.remove_roles(role7)
await user.remove_roles(role8)
await user.add_roles(role6)
current_role = role6
color_embed = 0xa260bf
fname="ロールの画像ファイル"
elif val >= 6000 and val < 10800:
user = await message.guild.fetch_member(user_id)
await user.remove_roles(role1)
await user.remove_roles(role2)
await user.remove_roles(role3)
await user.remove_roles(role4)
await user.remove_roles(role5)
await user.remove_roles(role6)
await user.remove_roles(role8)
await user.add_roles(role7)
current_role = role7
color_embed = 0xf2f2f2
fname="ロールの画像ファイル"
elif val >= 10800:
user = await message.guild.fetch_member(user_id)
await user.remove_roles(role1)
await user.remove_roles(role2)
await user.remove_roles(role3)
await user.remove_roles(role4)
await user.remove_roles(role5)
await user.remove_roles(role6)
await user.remove_roles(role7)
await user.add_roles(role8)
current_role = role8
color_embed = 0xe9a28d
fname="ロールの画像ファイル"
chkrls = message.author.roles
role_name_list = []
for role in chkrls: # roleにはRoleオブジェクトが入っている
role_name_list.append(role.name)
if "@everyone" in role_name_list:
role_name_list.remove("@everyone")
if val < 180:
reply_text = f'{message.author.mention}さん' + "の" \
+ str(today.month) + "月の勉強時間は" + str(val) + "分だよ!\n" \
+ "今日の勉強時間は" + str(val_idv) + "分だよ!\n" \
+ "合計180分以上勉強すると称号がもらえるよ!" \
+ "頑晴れ!"
channel = client.get_channel(ch_id)
await channel.send(reply_text)
else:
val = round(val / 60, 1)
val_idv = round(val_idv /60, 1)
embed = discord.Embed(title=f'{message.author.mention}さん' + "の実績",\
url="",color=color_embed)
embed.add_field(name= ":cloud:" + str(today.month) + "月" + str(today.day) + "日の勉強時間" + ":cloud:",value=str(val_idv) + "時間",inline=False)
embed.add_field(name= ":cloud:" + str(today.month) + "月の勉強時間" + ":cloud:",value=str(val) + "時間",inline=False)
embed.add_field(name= ":medal:" + "ユーザーランク",value=str(current_role.name),inline=False)
PATH = os.path.join(os.path.dirname(__file__), 'img/' + fname)
file = discord.File(PATH)
embed.set_thumbnail(url=f"attachment://{fname}") # embedに画像を埋め込むときのURLはattachment://ファイル名
channel = client.get_channel(ch_id)
await channel.send(file=file, embed=embed)
elif message.content.startswith("!plus"):
# split関数を用いて文字列を分割,command[1] に追加したい時間が整数で入っているはず、これをチェック
command = message.content.split()
if command[1].isdecimal():
addtime_min = int(command[1])
#月合計時間スプレッドシートに時間を加算
if ws_time.find(message.author.name) is None:
next_row = next_available_row(ws_time)
ws_time.update_cell(next_row, 1, message.author.name)
ws_time.update_cell(next_row, month, addtime_min)
else:
cell = ws_time.find(message.author.name)
nowtime = ws_time.cell(cell.row, month).value
if nowtime == None:
nowtime = 0
cum_time = int(nowtime) + addtime_min
ws_time.update_cell(cell.row, month, cum_time)
if ws_idv_time.find(message.author.name) is None:
next_col = next_available_col_time(ws_idv_time)
ws_idv_time.update_cell(1, next_col, message.author.name)
ws_idv_time.update_cell(day, next_col, addtime_min)
else:
#日ごとのスプレッドシートに時間を加算
cell0 = ws_idv_time.find(message.author.name)
nowtime0 = ws_idv_time.cell(day, cell0.col).value
if nowtime0 == None:
nowtime0 = 0
cum_time0 = int(nowtime0) + addtime_min
ws_idv_time.update_cell(day, cell0.col, cum_time0)
reply_text = f'{message.author.mention}さん' + "の" \
+ str(today.month) + "月の勉強時間に" + str(addtime_min) + "分を加算したよ!"
channel = client.get_channel(ch_id_shinchoku)
await channel.send(reply_text)
else:
err_text = "plusの後にはスペースを置いて整数を入力してね!"
channel = client.get_channel(ch_id_shinchoku)
await channel.send(err_text)
elif message.content.startswith("!minus"):
# split関数を用いて文字列を分割,command[1] に追加したい時間が整数で入っているはず、これをチェック
command = message.content.split()
if command[1].isdecimal():
subtracttime_min = int(command[1])
#月合計時間スプレッドシートに時間を加算
cell = ws.find(message.author.name)
nowtime = ws.cell(cell.row, month).value
if nowtime == None:
nowtime = 0
cum_time = int(nowtime) - subtracttime_min
ws.update_cell(cell.row, month, cum_time)
#日ごとのスプレッドシートに時間を加算
cell0 = ws_idv.find(message.author.name)
nowtime0 = ws_idv.cell(day, cell0.col).value
if nowtime0 == None:
nowtime0 = 0
cum_time0 = int(nowtime0) - subtracttime_min
ws_idv.update_cell(day, cell0.col, cum_time0)
reply_text = f'{message.author.mention}さん' + "の" \
+ str(today.month) + "月の勉強時間から" + str(subtracttime_min) + "分を減算したよ!"
channel = client.get_channel(ch_id)
await channel.send(reply_text)
else:
err_text = "plusの後にはスペースを置いて整数を入力してね!"
channel = client.get_channel(ch_id)
await channel.send(err_text)
client.run(TOKEN)
おわりに
この記事では、時間計測、時間加算減算、称号付与機能に絞って紹介しました。
他にも、私が管理の一員を担っているDiscordサーバーでは、計測したスプレッドシートの値を使って様々なbotが動いています。
例えば、皆で合計した時間を元に、月の合計目標時間を目指して毎日定時に称号を付与するbotに、個人の週の合計勉強時間をランキング形式にして1位から5位まで発表するbot等です。
Qiita初投稿でした、不慣れな点もあったかと思いますが、最後までお読みいただきありがとうございました。