はじめに
この記事では、DiscordでのBotの制作をRustを用いて行うための方法を紹介し、その導入部分を解説します。
Rust自体の文法や、Discord Botを作るうえでの必要なトークンの準備など解説していません。
トークンの取得方法など基本的なことを知りたい場合にはこちらの記事の初期設定の項目などを参考にして、準備を行ってください。
動作環境
この記事で使用しているRustのバージョンはNightly版を使用していますが、Stable版のRustであっても問題なく起動可能です。
また、ライブラリとしてはRustのDiscord APIラッパーライブラリの最大手であるSerenityをベースに作成しているPoiseを使用します。
Poiseは開発中のライブラリであるようで、今後の実装状況次第では破壊的な変更があるかもしれません。可能な限り、変更に対して追従しますが常に動作可能なコードではないことをご理解ください。
では、なぜそのようなライブラリを使うのかということになりますが、PoiseではRustのプロシージャルマクロ機能などをうまく活用し、現状DiscordのBotを開発するうえで向き合わなくてはならないSlash Commandsへの対応が容易に行えるという利点があるためです。
Discord公式の声明によると2022年4月にメッセージの内容を取得するためのIntentが特権Intentに追加されます。(記事はこちら)
100サーバー以上に導入されたBotが特権Intentを使用するにはDiscordのサポートに、特権Intentを使う理由などを報告しなければなりません。
すなわち、100サーバーを超えるような中規模のBotを開発を予定している場合には、メッセージの内容を使用するために妥当な機能を開発する必要があります。単に>ban @someone
のような単純な引数を受け取るようなコマンドのみで構成されたBotの場合だと、Slash Commandsで十分代用可能であるため許諾が下りない可能性があります。
そのためこの記事では、Slash Commandsと従来のテキストベースのコマンド双方の対応が容易なライブラリを使用して開発を行います。
Rustのバージョンは1.60.0 Nightly(498eeb72f 2022-01-31)です。
依存関係は以下の通りです。
[package]
name = "qiita_poise"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dotenv = "0.15.0"
env_logger = "0.9.0"
log = "0.4.14"
poise = { git = "https://github.com/kangalioo/poise", rev = "9ebc7b5" }
thiserror = "1.0.30"
tokio = { version = "1.16.1", features = ["rt-multi-thread"] }
簡単なコマンドの実装
このコードの解説は後程行います。とりあえず、Botとして動かすための必要最小限のコードを以下に記します。
#[macro_use]
extern crate log;
use std::env;
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error("{0}")]
Serenity(#[from] poise::serenity::Error),
}
type Context<'a> = poise::Context<'a, (), AppError>;
#[poise::command(prefix_command, hide_in_help)]
async fn register(ctx: Context<'_>, #[flag] global: bool) -> Result<(), AppError> {
poise::builtins::register_application_commands(ctx, global).await?;
Ok(())
}
/// Add two number.
#[poise::command(prefix_command, slash_command)]
async fn add(
ctx: Context<'_>,
#[description = "The first number."] a: i32,
#[description = "The second number."] b: i32,
) -> Result<(), AppError> {
poise::say_reply(ctx, format!("{}", a + b)).await?;
Ok(())
}
async fn on_error(error: poise::FrameworkError<'_, (), AppError>) {
error!("{:?}", error);
}
#[tokio::main]
async fn main() {
dotenv::dotenv().ok();
env_logger::init();
let token = env::var("DISCORD_TOKEN").expect("DISCORD_TOKEN not set");
let options = poise::FrameworkOptions {
commands: vec![register(), add()],
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some("::".to_string()),
..Default::default()
},
on_error: |err| Box::pin(on_error(err)),
..Default::default()
};
poise::Framework::build()
.token(token)
.options(options)
.user_data_setup(|_, _, _| Box::pin(async { Ok(()) }))
.run()
.await
.unwrap();
}
このコードを記述したのち、プロジェクトのルートに.env
ファイルを作成し、最初に作成したDiscordトークンを環境変数に登録します。
RUST_LOG=info
DISCORD_TOKEN=FaketokenFaketokenFaketo.kenFak.etokenFaketokenFaketokenFak
これにより、cargo run
でコードを走らせることができます。
まず、作成したスラッシュコマンドを登録させるために::register
と入力します。
::register global
と入力することですべてのサーバーに作成したコマンドの反映を行いますが、globalを指定しない場合にはこのregisterコマンドを呼び出したサーバーのみに反映を行います。開発時には基本的にglobalを指定しないで、完成した際にglobalを使用して全体に登録を行うようにさせましょう。
また、Slash Commandに移行して、メッセージ内容の取得が行えなくなった場合であってもBotへのメンションやBot間のDMメッセージの内容は読み取ることが可能です。
Done!
の表示が確認出来たら、メッセージ欄に/
と打ち込んでみましょう。先ほど登録した/add
が見えていれば成功です。
動作確認を行いましょう。上記のように加算の結果が表示されていれば成功です。
コードの解説
上記のコードの解説を行います。
1.エラーの列挙体
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error("{0}")]
Serenity(#[from] poise::serenity::Error),
}
このコードではエラーハンドリングを容易にするためにthiserrorを使用しています。列挙体の各要素に#[error("{0}")]
などのようにアトリビュートを記述することで指定したライブラリのエラーに対する、From
やDebug
traitの実装を簡潔にすることができます。各ライブラリのエラーをAppError
列挙体に集約させているため、関数の返り値をResult<(), AppError>
と記述することができ、エラーのハンドリングをAppError
で行い、エラーに関係ないコマンドの実装部分をコードの煩雑化を防ぐことができます。
2.コマンドの実装
#[poise::command(prefix_command, hide_in_help)]
async fn register(ctx: Context<'_>, #[flag] global: bool) -> Result<(), AppError> {
poise::builtins::register_application_commands(ctx, global).await?;
Ok(())
}
/// Add two number.
#[poise::command(prefix_command, slash_command)]
async fn add(
ctx: Context<'_>,
#[description = "The first number."] a: i32,
#[description = "The second number."] b: i32,
) -> Result<(), AppError> {
poise::say_reply(ctx, format!("{}", a + b)).await?;
Ok(())
}
ここでは、registerとaddの2つのコマンドを実装しています。面倒なスラッシュコマンドの登録はpoiseが肩代わりしてくれます。poise::builtins::register_application_commands
がその正体です。後程Botに登録したコマンドをSlash Commandとして登録するために各コマンドの引数の型などを解析しDiscordAPIに登録用の処理を投げます。
各コマンドの第一引数にはContext
を指定する必要があります。このContext
は名前の通り、コマンドが打たれた「文脈」でありコマンドの投稿者やチャンネルの取得の際などに利用します。
#[poise::command]
はさまざまなオプションを受け取ることができます。
中でもよく使うものがprefix_command
、slash_command
です。前者が従来のBotのコマンドであり後者が名前の通りSlash Commandsです。
すなわち、prefix_command
とslash_command
の処理を同一化して書くことができます。(Serenity単体の使用ではこれら二つのコマンドのハンドリングを別々に記述する必要があります)
また、Slashコマンドの使用の際にはコマンド及び引数の説明を追加しなければなりません。それぞれDocumentation comment(///
の行;PythonでいうところのDocstring)と#[description]
に書かれた内容がそのまま反映されます。
3.メイン部分
#[tokio::main]
async fn main() {
// 3.1.
dotenv::dotenv().ok();
env_logger::init();
let token = env::var("DISCORD_TOKEN").expect("DISCORD_TOKEN not set");
// 3.2.
let options = poise::FrameworkOptions {
commands: vec![register(), add()],
// 3.2.1.
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some("::".to_string()),
..Default::default()
},
// 3.2.2.
on_error: |err| Box::pin(on_error(err)),
..Default::default()
};
// 3.3.
poise::Framework::build()
.token(token)
.options(options)
.user_data_setup(|_, _, _| Box::pin(async { Ok(()) }))
.run()
.await
.unwrap();
}
3.1. 環境変数の読み込み
.envに登録されたか環境変数をロードしています。RUST_LOG
環境変数を指定するとinfo!
やerror!
といったログを出力することができます。
3.2. オプションの設定
3.2.1. Prefix Option
Prefix Optionは主にコマンドの接頭辞(!
や?
など)の登録を行います。ここでは実装していませんが、メッセージの編集を検知し編集内容に応じて再度処理を行う設定なども指定可能です。
3.2.2. on_error
起動中や実行中にエラーが発生した際のコールバックを設定できます。ここでは簡易的にログに流すだけの簡素なものに設定しています。
3.3. 実行
先ほど指定したオプションを使用してBotを起動します。.user_data_setup(|_, _, _| Box::pin(async { Ok(()) }))
では、Botを通して使用するデータ(データベースへの接続や全サーバーで同期する必要のあるデータ)を登録できます。この部分の詳細は次回以降の記事にて解説を行います。
おわりに
ほんの基本的な部分の紹介でしたが、比較的直観的にRustを用いてSlashコマンドを実装できるということは伝わりましたでしょうか。
Slash Commandへの移行は辟易する部分もありますが、この記事がSlash Commandへの移行の足場となっていただければ幸いです。
需要次第ですが、より複雑なBotの実装方法を次回以降解説いたします。