記事一覧ページへ移動

【Discord Bot】serenity-rsで指定した時間にメッセージを送信する

2024-07-21
2024-04-24

最近こんな物を作りました。

そういえば:昨日はこんなものを書いていました tomlで日付を書いたら指定した時間に残り日数をお知らせしてくれるDiscord Bot(Rust製)
2時間で作ったからハッカソンみたいなもんだね \

watasuke102/remind4f: It reminds me of upcoming events via Discord
https://github.com/watasuke102/remind4f

-- わたすけ (@Watasuke102, 凍結済み)

Remind4F は Rust で開発された Discord Bot1です。Serenity を使っています。ツイートにもある通り、event.toml でイベント情報を管理し、env.toml で指定した時間になったら、指定したチャンネルへイベントの日付と残り日数を教えてくれます。

GitHub で公開しています。よければ star していってください。

GitHub - watasuke102/remind4f: It reminds me of upcoming events via DiscordGitHub - watasuke102/remind4f: It reminds me of upcoming events via Discordhttps://github.com/watasuke102/remind4fIt reminds me of upcoming events via Discord. Contribute to watasuke102/remind4f development by creating an account on GitHub.

さて、「決まった時間にメッセージを送信する」という機能を持つ Bot は以前にも開発したことがあります。それは TimeTree-NoticeBot-rust です。

GitHub - watasuke102/TimeTree-NoticeBot-rust: TimeTreeの予定を確認し、Discordに通知するGitHub - watasuke102/TimeTree-NoticeBot-rust: TimeTreeの予定を確認し、Discordに通知するhttps://github.com/watasuke102/TimeTree-NoticeBot-rustTimeTreeの予定を確認し、Discordに通知する. Contribute to watasuke102/TimeTree-NoticeBot-rust development by creating an account on GitHub.

TimeTree のイベントを取得し、朝 8 時にお知らせしてくれます。名前の通りこちらも Rust 製ですが、Serenity を使わず、HTTP リクエストを直接組み立ててメッセージを送信しています。

なぜ Serenity を使わなかったかというと、「機能が少ないからライブラリを使うまでもない規模だった」というのもありますが、「朝 8 時にメッセージを送信する」という機能をどうやって実装すればよいか分からなかったからです。

今回作成した Remind4F では、Serenity を用いて決まった時間にメッセージを送信する機能を実装しました。ベストプラクティスかどうかはわかりませんが、とりあえず紹介します。

検証環境

クレートについては後で出します。

  • rustc / cargo 1.79.0-nightly (6f06fe908 2024-04-16)
  • Arch Linux linux-zen 6.8.7

やり方

これも reminds4f/src/main.rs を見れば大体わかります。

どうやって実装するかというと、つまり別スレッドで現在時刻を確認すれば良いわけです。Tokio を使ってスレッドを生やし、無限ループ内で chrono を使って現在時刻を確認します。

まずは使用するクレートを Cargo.toml に列挙しておきましょう。いい感じのバージョンを指定してください。

chrono = "0.4"
tokio = { version = "1.10", features = ["full"] }
serenity = "0.12"

次に、EventHandler の ready 関数内で tokio::spawn を使います。tokio の詳しい使い方は省略します2。こんな感じに書いてください。

struct Handler;
#[async_trait]
impl EventHandler for Handler {
  // Botの準備が出来たら実行
  async fn ready(&self, ctx: Context, _ready: Ready) {
    // スレッドを生やす
    tokio::spawn(async move {
      // 処理を書いていく
    });
  }
}

そうしたら、無限ループの中で時間を確認します。AM 10:30 (JST) にメッセージを送りたいときは以下のようになります(tokio::spawn 以外を省略しています)。

tokio::spawn(async move {
  let hour = 10;
  let minute = 30;
  // JST (+9h)
  let jst = FixedOffset::east_opt(9 * 3600).unwrap();
  loop {
    // 現在の日時をJST基準で取得する
    let now = Utc::now().with_timezone(&jst);
    // 指定した時間になったら
    if now.time().hour() == hour && now.time().minute() == minute {
      // メッセージを送信する処理を書く
    }
    // 1分待つ
    tokio::time::sleep(tokio::time::Duration::from_millis(1000 * 60)).await;
  }
});

この例だと秒レベルの精度はいらないので、1 分ごとにループを回しています。もっと精度が必要なら最後の from_millis に渡す値を適切に変更して、if 文を変更したりすれば良いと思います。

メッセージを送信する

メッセージ送信は Serenity が Example として用意してくれてそうなのでそっちを見たほうが良いかもしれません。とりあえずここでは、指定したチャンネルにメッセージを送る方法を紹介します。

Discord の設定から「詳細設定」を開き、開発者モードをオンにしてから、任意のチャンネルをクリックすると、「チャンネル ID をコピー」という項目が出てきます。これをクリックすると、数字だけで構成されたチャンネル ID がコピーされます。

ということで、このチャンネル ID を使って以下のようなコードを書けば、メッセージを送信できます。

let channel_id: u64 = /* チャンネルIDをここに入れる */;
ChannelId::new(channel_id).send_message(
  &ctx.http,
  CreateMessage::new().content("ここにメッセージを入力")
).await.unwrap();

実際には、チャンネル ID は環境変数として設定したり、外部ファイルから読み込んだりしたほうが良いと思います。

embed を使いたい場合は examples/e09_create_message_builder が参考になると思います。

概形

ということで、ここまでの作業をまとめると、こんな感じになります(channel_id を宣言する位置をちょっと変えています)。

struct Handler;
#[async_trait]
impl EventHandler for Handler {
  // Botの準備が出来たら実行
  async fn ready(&self, ctx: Context, _ready: Ready) {
    let channel_id: u64 = /* チャンネルIDをここに入れる */;
    // スレッドを生やす
    tokio::spawn(async move {
      let hour = 10;
      let minute = 30;
      // JST (+9h)
      let jst = FixedOffset::east_opt(9 * 3600).unwrap();
      loop {
        // 現在の日時をJST基準で取得する
        let now = Utc::now().with_timezone(&jst);
        // 指定した時間になったら
        if now.time().hour() == hour && now.time().minute() == minute {
          ChannelId::new(channel_id).send_message(
            &ctx.http,
            CreateMessage::new().content("ここにメッセージを入力")
          ).await.unwrap();
        }
        // 1分待つ
        tokio::time::sleep(tokio::time::Duration::from_millis(1000 * 60)).await;
      }
    });
  }
}

あとは main 関数で、この構造体を Client::builder に渡して client を start すれば終了です!

#[tokio::main]
async fn main() {
  let bot_token: String = /* ここにトークンを入れる */;
  Client::builder(&bot_token, GatewayIntents::empty())
    .event_handler(Handler)
    .await.unwrap();
  client.start().await.unwrap();
}

おわりに

実は、初期の Remind4F ではこの機能を Serenity を使わずに実装していました。既に TimeTree-NoticeBot-rust で実装した経験があったので楽そうだなと思ったからです。実際楽でした。ただし今回は、これに加えてスラッシュコマンドを使ってみたいと思っていて、そしてライブラリなしでスラッシュコマンドを実装するのは異常に面倒だということが分かったので、諦めました。

そもそも Serenity には examples/e13_parallel_loops というサンプルがあるので、Serenity でも定刻メッセージ送信は実装できそうだなとは思っていました。

まあサンプルどおり cache_ready() の中で tokio::spawn しようとしても動かなかったんですけどね。そもそも cache_ready() が呼ばれませんでした。英語弱者すぎて examples に書いてあるコメント3の意味がわかんないよ~。ドキュメントによると、cache_ready は Bot が起動した時に(かなり素早く)呼ばれるらしいんですけどね。

とにかく、Serenity で時間に応じてメッセージを送信する方法はどこでも紹介されていなかったはずなので、この記事が役に立てば良いなあと思っています。

ちなみに今回書いたコード実行してないです。動かなかったら教えて下さい。

Footnotes

  1. App と呼ぶほうが正しいらしい(サーバーのユーザーリストにも App って書いてるし、Developer Portal でも Application と呼ばれているため)

  2. 自分でもあまり理解できていないからです

  3. L33-34, We use the cache_ready event just in case some cache operation is required in whatever use case you have for this.

Comments

Powered by Giscus