1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[python]Discord Botを使って勉強時間を計測するオンライン自習室環境を作ってみた

Last updated at Posted at 2023-03-01

はじめに

こんにちは、hiroと言います。

この記事では、Discord.pyとherokuとGoogle Spread Sheet APIを用いたオンライン自習室環境を統制してくれるbotの作成方法について解説したいと思います。

完成品

20220725214336.png
20220523115029.png
20220523105710.png
2023-03-01 (2).png
20220725214835.png

前提知識

前提知識として、以下の記事を読んでからこの記事を読んでください。

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初投稿でした、不慣れな点もあったかと思いますが、最後までお読みいただきありがとうございました。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?