Rust製Discord botフレームワークの紹介と開発時つまずきポイント

# はじめに

[過去にserenityとsongbirdを使って作ったDiscord bot](https://qiita.com/ppputtyo/items/bf95c9ccdba3b6042031)をpoiseを使うようにアップデートした際に、いくつかつまずいた点があったので共有します。

アップデート後のサンプルbotのコードはこちらです。

https://github.com/ppputtyo/my-music-bot/tree/dev_poise

現時点(2024/9/23)では公式でserenity、songbird、poiseを使用したExampleコードがないので、こちらのコードが参考になれば幸いです。

サンプルbotは主に、ボイスチャンネルに接続している状態でテキストチャンネルにおいて `/play {youtube_url}`と送信すると、`youtube_url`の動画の音声をボイスチャンネルで再生する機能などを実装しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608343/25b3fcf4-345e-57f4-f061-96b8aea9b6fb.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608343/bb6638af-8483-8d97-18ae-3cbb544e552a.png)


# 注意点

紹介しといてアレなんですが、新しく大規模なDiscord botを作ろうとしている方にはpoise、serenity、songbirdを使用するのは個人的にお勧めしません。理由は以下です。

- poise、serenity、songbirdはまだ開発途上のため破壊的変更が多発する
- Changelogやドキュメントがあっさりしており、破壊的変更に関する情報が少ない
- 公式examplesも非推奨機能をガンガン使っており、良い例を見つけるのが難しい
    -  例: serenityの非推奨の構造体[StandardFramework](https://docs.rs/serenity/latest/serenity/framework/standard/struct.StandardFramework.html)を[songbirdの公式example](https://github.com/serenity-rs/songbird/blob/current/examples/serenity/voice/src/main.rs)が使用している


ある程度安定するまで待つほうが得策かもしれません。

https://zenn.dev/dms_sub/articles/7f2bb4bbddd388

# serenity、songbird、poiseをざっくり紹介

## serenity
Rust製のDiscord bot フレームワークです。
比較的簡単にDiscord botを作ることができます。

https://github.com/serenity-rs/serenity

## songbird

serenityでボイスチャンネルを扱うためのライブラリです。
Youtubeなどにある動画の音声を流したり、音声トラックをキューに追加して操作することができます。

https://github.com/serenity-rs/songbird

## poise

serenityでスラッシュコマンドを扱うためのライブラリです。

https://github.com/serenity-rs/poise

スラッシュコマンドをチャンネルに登録することで、コマンドの入力補完、引数のバリデーション、よく使うコマンドの表示などを自動で行ってくれるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608343/64f39219-8bcc-2eab-6b52-eeba9809a506.png)

:::note
serenityでは通常のテキストコマンドを扱う際もpoiseを使用することが推奨されています。
:::

# poise、serenity、songbird開発時につまずきやすいポイント

## Discordのスラッシュコマンドは仕様として3秒間応答がないとコマンドが失敗したとみなされる

Discordのスラッシュコマンドは3秒間応答がないとコマンドが失敗したとみなされます。

> Interaction tokens are valid for 15 minutes and can be used to send followup messages but you must send an initial response within 3 seconds of receiving the event. If the 3 second deadline is exceeded, the token will be invalidated.

https://discord.com/developers/docs/interactions/receiving-and-responding#:~:text=url%2C%20json%3Djson)-,Interaction,the%203%20second%20deadline%20is%20exceeded%2C%20the%20token%20will%20be%20invalidated.,-Inline%20HTTP%20Reponse

コマンド失敗とみなされた場合は以下のように「アプリケーションが応答しませんでした」と表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608343/71b4ad61-3036-7771-e4e5-ad2d30f4855a.png)

今回の実装では`play`コマンドのYoutubeの音声ダウンロードの処理が重く、3秒以上かかる可能性があったため、[`defer()`](https://serenity-rs.github.io/poise/current/poise/structs/struct.ApplicationContext.html#method.defer)を使用しました。

```Rust:play.rs
#[poise::command(slash_command, prefix_command, guild_only)]
pub(crate) async fn play(
    ctx: Context<'_>,
    #[description = "youtube url"] url: String,
) -> Result<(), Error> {
    // ~~~中略~~~

    // スラッシュコマンドは3秒間応答がないとコマンドが失敗したと判断される
    // そのため、時間のかかる動画ダウンロード処理の前に応答し、コマンド失敗を防ぐ
    // https://discord.com/developers/docs/interactions/receiving-and-responding
    ctx.defer().await?;

    if let Some(handler_lock) = manager.get(guild_id) {
        let mut handler = handler_lock.lock().await;

        let http_client = ctx.data().http.clone();
        let src = YoutubeDl::new(http_client, url);

        handler.enqueue_input(src.into()).await;

        ctx.say(format!("キューに追加: position {}", handler.queue().len()))
            .await?;
    } else {
        ctx.say("ボイスチャンネルに入ってないよ").await?;
    }

    Ok(())
}
```

`defer()`中は以下のように表示され、コマンドの実行が続行されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608343/881b48e2-6866-a201-2f9e-08bab626f1fb.png)

## `Cargo.toml`でSymphoniaで対応する形式を明示する必要がある

songbirdはSymphonia(Rust製マルチメディアライブラリ)を使用しています。


https://github.com/pdeljanov/Symphonia

:::note info
以前はffmpegを使用していましたが、[`v0.4.0`](https://github.com/serenity-rs/songbird/releases/tag/v0.4.0)からSymphoniaを使用するようになったみたいです。
:::

デフォルトではsongbirdはSymphoniaに対していかなる形式のコーデックも要求していないため、songbirdを利用するプロジェクトで対応する形式を指定する必要があります。

今回は以下のように基本的な音声形式を指定しました。
```toml:Cargo.toml
symphonia = { features = ["aac", "mp3", "isomp4", "alac"], version = "0.5.2" }
```

## 依存するreqwestのバージョンをsongbirdと同じバージョンにする必要がある

reqwestを現時点で最新版の`0.12.7`に指定したところ、以下のようなエラーが発生しました。

```console
error[E0308]: mismatched types
  --> src\commands\play.rs:43:34
   |
43 |         let src = YoutubeDl::new(http_client, url);
   |                   -------------- ^^^^^^^^^^^ expected `Client`, found `reqwest::Client` 
   |                   |
   |                   arguments to this function are incorrect
   |
   = note: `reqwest::Client` and `Client` have similar names, but are actually distinct types
```

songbirdが依存しているreqwestとバージョンを合わせた(`0.11`)ところ、エラーが解消します。

```toml: Cargo.toml
reqwest = "0.11"
```

# おわりに

serenity、songbird、poiseを使用したDiscord botに関する記事が非常に少なかったので、少しでも開発の助力になれば幸いです。
また、私自身serenity等のフレームワークを使いこなせている自信がないので、[サンプルBot](https://github.com/ppputtyo/my-music-bot/tree/dev_poise)のより良い実装等あればコメントかPR/issueで教えていただけると非常に助かります。

# 参考文献
- [RustではじめるDiscord Bot (1) #初心者 - Qiita](https://qiita.com/Shirataki2/items/be795f4c504df4a91554)
- [Rustで作るdiscord bot入門編 (serenity使用)](https://zenn.dev/t4t5u0/articles/cd731e0293cf224cb4dc)