🐔

サークル向けにつくったDiscord BOTの中身

2022/12/24に公開

CIST Advent Calendar 2022 21目の記事です
https://qiita.com/advent-calendar/2022/cist

CIST LTのサーバーで動いていた(Herokuがお亡くなりになっているので移住先を見つけるまでは停止中です)Discord botの中身についてまとめようと思います

つくったBOTのソースコード

https://github.com/618knot/Discordbot-Times/blob/main/bot.py
https://github.com/618knot/Discordbot-Times/blob/main/ggcal.py

備えている機能

  • Times
    timesチャンネルでの発言をすべてtimelineに流す
  • VC参加通知
    誰かが特定のボイスチャットに参加したことをどこかのチャンネルに通知する
  • イベントリマインド・通知
    google calendarに登録した予定を事前にリマインド・通知する

中身のはなし

起動時の処理

@client.event
async def on_ready():
    print("on_ready")
    print(client.user.name) #bot name
    print(discord.__version__) #discord.pyのversion
    print("--------")
    print(f"waiting {60 - datetime.now().second} sec for loop to start")
    time.sleep(60 - datetime.now().second)
    scheduling_notice.start()

    await client.change_presence(activity=discord.Game(name = ""))

上記on_ready()が起動時に走る処理です

    print(f"waiting {60 - datetime.now().second} sec for loop to start")
    time.sleep(60 - datetime.now().second)
    scheduling_notice.start()

これはgoogleカレンダーの通知に必要で、scheduling_noticeをhhmm00の状態から動かすための処理です

テキストチャンネル監視

@client.event
async def on_message(message):
    #botの送信ははじく
    if message.author.bot:
        return

    #times投稿
    #timesカテゴリのみを監視、timelineチャンネルは無視
    if message.channel.category_id == categoryId and message.channel.id != timesId:
        await client.get_channel(timesId).send(message.channel.mention + " " + message.author.name + "\n" + message.content)
        
        if message.attachments:
            for i in message.attachments:
                await client.get_channel(timesId).send(i)

サーバで投げられたテキストチャットを監視して、「TIMESカテゴリ内の投稿である」かつ「チャットの投稿先がtimelineでない」ときにtimelineに受け取ったチャットの内容が送信されるようになっています。

例)TIMESカテゴリのknotチャンネルに送られたチャットはtimelineチャンネルにも同じ内容が送信されます。

VC参加通知

#もくもく会入室通知
@client.event
async def on_voice_state_update(member, before, after):
    if after.channel.id == mokumokuId and after is not before and after.self_mute is before.self_mute and after.self_stream is before.self_stream and after.self_deaf is before.self_deaf:
        await client.get_channel(timesId).send("<#" + str(mokumokuId) + ">" + " " + member.name + "\n" + "もくもく会に参加しました")

クソ長条件文
on_voice_state_updateはボイスチャット内で何かが起こった時に呼ばれます。おそらく。

  • 何かが起こったとき
    • VCの入退出があった
    • 誰かがマイクミュートにした
    • 誰かがスピーカーミュートにした
      など
if after.channel.id == mokumokuId and after is not before and after.self_mute is before.self_mute and after.self_stream is before.self_stream and after.self_deaf is before.self_deaf:

つまり、この条件文はVCの入室があったときにだけ通知を飛ばすためのものです。
細かい中身は覚えてないです。ごめんなさい

予定リマインド・通知

先にgooglecalendarの予定を取ってくる方からいきます。

カレンダーにある予定を3つ取ってくる

def calendar_info3():
    """Shows basic usage of the Google Calendar API.
    Prints the start and name of the next 10 events on the user's calendar.
    """
    # Load credential file for service account
    creds = load_credentials_from_file(
        'cistlt-calendar.json', SCOPES
    )[0]

    service = build('calendar', 'v3', credentials=creds)

    # Call the Calendar API
    now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time

    # NOTE: Set your calendar id
    events_result = service.events().list(calendarId='cist.lt.club@gmail.com', timeMin=now,
                                        maxResults=3, singleEvents=True,
                                        orderBy='startTime').execute()
    events = events_result.get('items', [])

    schedule = []
    if not events:
        return -1
    for event in events:
        start = event['start'].get('dateTime', event['start'].get('date'))
        schedule.append((start, event['summary']))

    return schedule

これはテンプレを少しいじっただけだったはずなので中身よくわかってないですね。
予定を3つとってきて、それをひとつのリストにして返すといった感じです。
[('2022-12-24T21:30:00+09:00', '定例会'), ('2023-01-07T21:30:00+09:00', '定例会'), ('2023-01-21T21:30:00+09:00', '定例会')]
こんな感じ

datetime型になおしてやる

def to_datetime(schedule):
    day_time = str(schedule[0]).split("T")
    sche_day = str(day_time[0]).split("-")
    sche_time = str(day_time[1]).split(":")

    return datetime.datetime(year=int(sche_day[0]), month=int(sche_day[1]), day=int(sche_day[2]), hour=int(sche_time[0]), minute=int(sche_time[1]))

bot本体ではdatetime型で使いたいので、そのための関数です。
なんかもっといいやり方がありそう。

BOT本体

#予定通知
day = (datetime.now()).day
schedule = calendar_info3()
@tasks.loop(seconds=60)
async def scheduling_notice():
    global day
    global schedule
    now = datetime.now()
    
    for sche_index in schedule:
        sche_datetime = to_datetime(sche_index)
        if timedelta(hours=23, minutes=59, seconds=55) <= sche_datetime - now <= timedelta(days=1, seconds=5):
            await client.get_channel(noticeId).send(f"`{sche_datetime}`より`{sche_index[1]}`があります")

        if timedelta(seconds=-5) <= sche_datetime - now <= timedelta(seconds=5):
            await client.get_channel(noticeId).send(f"@everyone 今から`{sche_index[1]}`が始まります")
    
    if now.day - day == 1:
        day = now.day
        schedule = calendar_info3()

@tasks.loop(seconds=60)で1分に1度scheduling_noticeが実行されます。
予定の1日前と予定開始時間に通知を飛ばします。
dayはbot起動した時の日付を覚えておいて、最後の条件文で1日経過したときに日付を更新し予定の取得をしています。つまり、1日に1回予定を取得するための処理です。

おわり

Herokuの代替を探すのサボっているので、気が向いたら探しに行きたいですね。

Discussion