Top > Blog > Article > Serenity (Rust) でDiscord Botを開発するときに躓いたところ

Serenity (Rust) でDiscord Botを開発するときに躓いたところ

2022-11-27
2021-12-05

自分は同じクラスの人間が集まるDiscordサーバーに入っているのですが、わりとVCが活発なんですよね。ただ、「人がいたら入ろうかな」という人も一定数いて(自分もそうです)、そういう人々はいちいちDiscordをチェックしなければいけません。面倒ですよね。

ということで、DiscordのVCチャンネルに誰かが入退出したら通知してくれるBotを作成しました。たぶん探したらたくさんあるとは思いますが、今回はRustとBot作成用クレートのSerenityを学ぼうと思い、自分で書きました。

今回は、制作過程で難しかったところ、日本語でまとめておけばわかりやすそうだなと思ったことをまとめておこうと思います。


この記事は共同開発鯖Advent Calendar 2021の5日目の記事です。昨日は鯖民向けBlogの使い方でした。鯖民向けのブログがあるのにこっちで書いてます。あと、今回のBotはこの鯖にいるBotを丸パクリしたものです。ゆるしてください。

どんなやつ作ったの

これです。

GitHub - watasuke102/discord-voicechat-notice: Notice a member who joined/leaved

誰かがどこかのボイスチャンネルに入ったり出たりすると、あらかじめ指定したチャンネルに、こんな感じのメッセージを送ります。

discord-voicechat-noticebot.png

わからないポイント0:そもそもドキュメントが読めない

Rustのクレートはかなりしっかりドキュメントが整備されていることが多いですよね。ビルドツールそのものにドキュメント生成機能が備わっているだけあります。

もちろんSerenityのドキュメントも用意されています。

[OGP image not found]

ただ、開発初期はこのドキュメントをぜんぜん読めませんでした。正直どこに何が書いてあるのかわかりません。

これはどうしようもないと思います(?)。exampleを見ながら、わからない構造体をぜんぶ検索欄に入れましょう。検索欄は結構役に立ちました。

serenity/examples at current · serenity-rs/serenity · GitHub

わからないポイント1:VCに入ったかどうかはどうやって検出する?

そもそもVCに入ったタイミングがわからなければおわりです。

ということで、とりあえずexamplesの一番上、basic_ping_botでも見ましょうか。

// コメント等、いろいろ省略
struct Handler;

#[async_trait]
impl EventHandler for Handler {
    async fn message(&self, ctx: Context, msg: Message) {

// 中略

#[tokio::main]
async fn main() {
    let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
    let mut client =
        Client::builder(&token).event_handler(Handler).await.expect("Err creating client");
    if let Err(why) = client.start().await {
        println!("Client error: {:?}", why);
    }
}

構造体Handlerに、EventHandlerをimplしています(表現が正しいのかわからん)。そして、Handlerはクライアント生成の時に使われるっぽいです。

ということで、ドキュメントでEventHandlerを検索しましょう。serenity::prelude::EventHandlerがそれっぽいので開き、ページ内検索でchannelとかvoiceとか調べてみます。

ヒットしたのはvoice_server_updatevoice_state_update。それぞれ説明文を見てみます。

voice_server_updateはこんな感じ。

Dispatched when a guild’s voice server was updated (or changed to another one).
Provides the voice server’s data.

voice_state_updateはこんな感じ。

Dispatched when a user joins, leaves or moves to a voice channel.
Provides the guild’s id (if available) and the old and the new state of the guild’s voice channels.

ということで、見るからにvoice_state_updateっぽいですね。これを使います。

(最初の方、間違えてvoice_server_updateと書いててずっと悩んでいました)

わからないポイント2:入退出はどう検出する?

誰かがVCに入退出した時に関数が呼ばれる(だけでなく、ミュートにしたときなども呼ばれるのですが、このときは気づかなかった)というのはわかりましたが、入室と退出はどうやって判定するのでしょうか?

これはもう引数をぜんぶprintln!で出力して比較しました。oldはOption<VoiceState>で、newがVoiceStateなのですが、oldがSomeかつnew.channel_idがNoneであればLeave、oldがNoneであればJoinということがわかりました。

これもVoiceStateをドキュメントで検索して、それを見つつやりました。

わからないポイント3:メッセージ送信

入退出が検知できたら9割完成です。あとは適切なタイミングでメッセージを送れば・・・メッセージを・・・送る・・・?

メッセージの送り方がわかりません。exampleを見ましょう。またもやbasic_ping_botです。

serenity/examples/e01_basic_ping_bot at current · serenity-rs/serenity · GitHub

    async fn message(&self, ctx: Context, msg: Message) {
        if msg.content == "!ping" {
            if let Err(why) = msg.channel_id.say(&ctx.http, "Pong!").await {
                println!("Error sending message: {:?}", why);
            }
        }
    }

Message構造体がいるらしい?どうしよう・・・

散々悩んで、Twitterの人に助けてもらいました。というかよく読めばわかるのですが、ChannelIdにsay関数があるのでこれを使えば良いです。

わからないポイント4:設定からチャンネルIDを読み込む

といっても設定自体の読み込みはserde_jsonにポイすれば終了で、問題は「構造体(Handler)からどうやって設定を読み込めば良いのか?」です。

examplesフォルダを流し読みするとglobal_dataという、いかにもなサンプルがあるので見ます。

serenity/examples/e12_global_data at current · serenity-rs/serenity · GitHub

struct CommandCounter;

impl TypeMapKey for CommandCounter {
    type Value = Arc<RwLock<HashMap<String, u64>>>;
}

struct MessageCount;

impl TypeMapKey for MessageCount {
    // While you will be using RwLock or Mutex most of the time you want to modify data,
    // sometimes it's not required; like for example, with static data, or if you are using other
    // kinds of atomic operators.
    //
    // Arc should stay, to allow for the data lock to be closed early.
    type Value = Arc<AtomicUsize>;
}

見てもわかりませんでした。Arc<RwLock<HashMap<String, u64>>>って何だよ・・・。

ただ、これは書き込みもできるデータ構造で、下に書いてあるとおり、読み込み専用ならArc<hoge>でよさそうです。hoge内には登録したい型が入るらしく、StringならArc<String>です。今回はSetting構造体を使いたいのでArc<Setting>となります。

そして、クライアントを生成した後、こんな感じで登録します。

let file = File::open("env.json").unwrap();
let settings: Settings = serde_json::from_reader(BufReader::new(file)).unwrap();
{
    let mut data = client.data.write().await;
    data.insert::<Settings>(Arc::new(settings));
}

EventHandlerの関数にはたいていContextが渡されるので、以下のような感じで読み出せばOKです。最終的にOption型が返されることに注意してください。

let data = ctx.data.read().await;
let settings = data.get::<Settings>();

さっきのと合わせて、設定ファイルで記述したチャンネルにメッセージを送信するには、こんな感じにすれば良いわけです。

let ch = ChannelId(settings.channel_id);
ch.say(&ctx.http, "Hello);

ちなみに、ドキュメントのserenity::model::id::ChannelIdを見る限り、sayの第2引数にはDisplayが実装されてたら(println!("{}")で表示できたら?)なんでもよさそうです。

わからないポイント5:embed使いたい

Botといえばやっぱりembedじゃないですか?テキストだけだとやっぱり味気ないですよね。

またexamplesを漁ります。create_message_builderが良さげです。

serenity/examples/e09_create_message_builder at current · serenity-rs/serenity · GitHub

channel_id.send_messageの第2引数にクロージャを渡して、その中でembedすれば解決するらしいです。具体的にどんな関数があるのかはドキュメントでわかります。

ちなみに、クロージャ内で全然補完が効きませんでした。変数名にカーソル合わせたら型推論してくれるのになんで補完はしてくれないんですかね・・・。いちおう型注釈すれば治ります。

ch.send_message(&ctx.http, |m: &mut CreateMessage| {
    m.embed(|e: &mut CreateEmbed| {
        ...

おしまい

情報量が少なくて大変でした・・・公式ドキュメントとexamplesのおかげでなんとかなったと思います。

今回の制作過程では嫌というほどOptionを使ったので、Option完全に理解しました。今後もRustを使っていろいろ書いていきたいです。

明日は@kat0h氏による「Vimでmarkdownをプレビューするプラグインを作った(最新版)」です(たぶん)。よろしくおねがいします。

icon

わたすけ

高専生 プログラミングでツールを作ったりLinux触ったりゲームしたりしてる
プロフィール詳細はこちら

タグ
どんなやつ作ったのわからないポイント0:そもそもドキュメントが読めないわからないポイント1:VCに入ったかどうかはどうやって検出する?わからないポイント2:入退出はどう検出する?わからないポイント3:メッセージ送信わからないポイント4:設定からチャンネルIDを読み込むわからないポイント5:embed使いたいおしまい