📝

学校のタスク管理Discord Botを運用する

に公開

この記事は 朝活部 Advent Calendar 2024 3日目の記事です

2J Advent Calendar 2024 3日目の記事でもあります(サボりですみません)

クラスのDiscord鯖で動いているBotです。
https://github.com/1STEP621/task-bot-rs

機能

タスク一覧を見る


毎日12時にタスク通知する

タスク追加/削除/編集


よく使う時間リスト/教科リストの編集

バックアップ

大変だったことなど

Discordの制限との戦い

Discordの仕様で、セレクトボックスは25種類の選択肢までしか作れないので、それを回避しつつ楽に追加できるように頑張りました。スマホから追加することが多いので文字や数字の手入力を極限まで減らしています。

最終的にできたタスク追加フローの仕様は以下のようになっています。

  1. タスクの種類(イベント、テスト、宿題、持ち物、その他)、科目、日付、時刻を選択式で入力する
    日付は今日から24日間、時刻はあらかじめ登録したものから選択できるようにする
    日付/時刻には「その他」の選択肢を用意しておく
  2. 日付がその他だったら月/日を入力させる
    普通にやると25個ルールによって1日〜31日までの選択肢を用意できないので、1月(〜15日)と1月(16日〜)に分けて月の入力によって日の選択肢を変えるようにする
  3. 時刻がその他だったら入力させる
    例によって分が選択肢で入力できないので、妥協して5分区切り(と59分)のみ選択できる仕様とする
  4. タスクの詳細を入力させる
    Discordのモーダル機能を使い文字入力させる

実装は大変でしたがかなり便利になったと思います。

また、タスク削除などに使うタスク選択セレクトボックスもタスクが多くなってくると制限をオーバーするのでうまいことページネーション(次のページ/前のページで選択肢を切り替える)しています。

データふっ飛ばしすぎ問題

ポンコツすぎてデータをふっとばしてしまうことがあったので一日一回管理者向けログチャンネルにバックアップする仕組みを作りました

技術的な話

メッセージの更新

このBotでは、ページネーションなどユーザーの選択に応じてメッセージを少しだけ更新するような処理がたくさんあります。
Serenity(RustのDiscord Botライブラリ)ではメッセージの更新はこんな感じで行います↓

let response = CreateInteractionResponse::UpdateMessage(
    CreateInteractionResponseMessage::default().components(/* 新しいコンポーネント */),
);
interaction.create_response(&ctx, response).await?;

しかしメッセージを更新するたびに毎回同じようなコンポーネントを書くのは煩雑です。かといって、コンポーネントをmut変数で持って更新していくのもあまりおしゃれではありません。
そこで、コンポーネントを|プロパティ(ページネーションなら"ページ数")| -> コンポーネントという形の関数で表し、更新のたびに新しいプロパティで呼び直すことでいい感じ™にやっています。ちょっとReactっぽくて面白いです。(Reactはミリ知らですが)

例はこのあたりで見られます。
https://github.com/1STEP621/task-bot-rs/blob/a8f1f2f84261e9130b7f6cbe08f150212283d79d/src%2Fcommands%2Fping_config.rs#L38-L54

インタラクションの切り出し

別のコマンドで同じインタラクションを使いまわしたい場合があります。例えば、タスク削除にもタスク編集にも「タスク選択」のインタラクションが必要です。

そこで該当部分をこんな関数に切り出しました。

pub async fn select_task(
    ctx: PoiseContext<'_>, // コンテキスト
    interaction: Option<ComponentInteraction>, // 前のインタラクションを引き継いで使う場合はその情報
    embed: Option<CreateEmbed>, // メッセージの本体部分を変更する場合はその情報
) -> Result<(ComponentInteraction, Task), Error> { // 最後のインタラクション、得られたタスク

これでかなりコードをきれいにすることができました。(yadokani389がやってくれました)

TaskPartialTask

タスクを入力している最中は日付と時間を分けて入力するのに対し、Taskの構造体側ではdatetime: Datetime<Local>となっていて、ここに無理が生じていたので、新しくPartialTaskの構造体↓

pub struct PartialTask {
    pub category: Option<Category>,
    pub subject: Option<Subject>,
    pub details: Option<String>,
    pub date: Option<NaiveDate>,
    pub time: Option<NaiveTime>,
}

を作りいい感じ™にしました。全部入力し終わってSomeになっていることが想定できるようになったら.unpartial()を呼んでTaskに変換します。

    pub fn unpartial(self) -> Result<Task, Error> {
        let category = self.category.context("Category not selected")?;
        let subject = self.subject.context("Subject not selected")?;
        let details = self.details.context("Details not selected")?;
        let date = self.date.context("Date not selected")?;
        let time = self.time.context("Time not selected")?;
        let datetime = Local
            .from_local_datetime(&date.and_time(time))
            .single()
            .context("Invalid date and time")?;
        Ok(Task {
            category,
            subject,
            details,
            datetime,
        })
    }

今後やりたいこと

  • 毎日12時の通知が走ってしまった後にタスクを追加しても、通知側が更新されるようにする
    • ↑済
  • 現状ほぼ鯖缶しかタスクを追加していないので、もっと気軽に追加できるような何らかの改善(漠然)

終わり

コードがきれいで満足です

Discussion