Rustで作るdiscord bot入門編 (serenity使用)
はじめに
この記事は、Rust 製 discord bot 用ライブラリserenity
についてとてもざっくり解説するものです。公式 GitHub の example を元に構成しています。紹介しきれていない機能がたくさんあるので、気になった方は参考の公式ドキュメントをご覧ください。
本記事での目標
- serenity を用いて簡単な Discord Bot を作成する
今回は、コマンドを、メッセージの判定ベースではなく、関数として実装していきます。いくつか理由はありますが、一番の理由は見通しがよくなるからです。メッセージの判定ベースでのやりかたは、serenity/main.rs at current · serenity-rs/serenityをご覧ください。
本記事の対象読者
- Rust を用いて Discord Bot を開発したい人
- Discord Bot を Python などから Rust に移行したい人
- Rapptz/discord.py: An API wrapper for Discord written in Python.は開発終了になりました(2021/9/28)
- Rust を使って実際にモノを作ってみたい人
Discord とは
グループでの通話やチャットに特化した SNS です。サーバーと呼ばれるグループの中にチャンネルというスペースがあります。チャンネルは複数作ることができて、目的ごとに使い分けることができます。DL はこちらから。Discord | 想像してみよう
Discord bot とは
Discord はサーバーに Bot を追加できます。Bot には様々な機能をもたせることができます。既存のものでは、音楽をかける Bot や、サイコロを振る Bot などがあります。今回は、独自の機能を持った Bot を作っていきます。
Rust とは
一言でいうと、高速かつ安全な素晴らしい言語です。イケメンです。
serenity を選んだ理由
- 現在(2021 年 5 月)開発が盛んなこと
- discord.py によく似ていること
の 2 点です。
本編
ディレクトリ構成
Rust の環境構築は終わっているものとします。終わっていない方は、Install Rust - Rust Programming Languageからお願いします。
まずは以下のコマンドを実行します。
cd 任意のディレクトリ
cargo init discord-bot-rust
cd discord-bot-rust
touch ./config.json
ディレクトリは以下のようになっているはずです。
discord-bot-rust
├── Cargo.toml
├── config.json
├── LICENSE
├── README.md
└── src
├── commands
│ ├── mod.rs
│ └── neko.rs
└── main.rs
Discord 側の準備
Bot を動かすために必要なトークンを作成します。まず、Discord Developer Portal — My Applicationsにアクセスします。
右上のNew Application
をクリックします。
Bot の名前を入力し、Bot を作成します。
左上のハンバーガーメニューをクリックし、Bot のページに遷移します。
Add Bot をクリックします。
これで Bot が作成されました。Copy
を押すと Bot のトークンがクリップボードにコピーされます。これは後で使います。
Bot の招待リンクを作成ます。OAuth のページを開きます。
下にスクロールし、bot にチェックを入れます。更に下に招待リンクが作成されました。
これをブラウザに貼り付けて Bot をサーバーに招待しましょう。
Cargo.toml
Cargo.toml を以下のようにします。
[package]
authors = ["yourname"]
edition = "2018"
name = "discord-bot-rust"
version = "0.1.0"
[dependencies]
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
serenity = {version = "0.10.7", features = ["framework", "standard_framework"]}
tokio = {version = "1.6.0", features = ["macros", "rt-multi-thread"]}
dipendencies 項目を付け加えます。これは外部クレート(パッケージ)を使用することを、明示するためのものです。tokio
は非同期プログラミング向けのクレートです。serde
は JSON ファイルを操作するためのクレートです。
クレートのインポート
説明のためには、それぞれ必要な場所に書いたほうがわかりやすいですが、面倒なのでまとめています。(気が向いたら分けます)
mod commands;
use std::{collections::HashSet, fs::File, io::BufReader, usize};
use serenity::async_trait;
use serenity::framework::standard::{
help_commands,
macros::{group, help},
Args, CommandGroup, CommandResult, HelpOptions,
};
use serenity::framework::StandardFramework;
use serenity::model::{channel::Message, gateway::Ready, id::UserId};
use serenity::prelude::{Client, Context, EventHandler};
use serde::{Deserialize, Serialize};
use serde_json::Result;
use commands::{channels::*, neko::*};
イベントハンドラ
// Handler構造体。取得したいイベントを実装する
struct Handler;
#[async_trait]
impl EventHandler for Handler {
// Botが起動したときに走る処理
async fn ready(&self, _: Context, ready: Ready) {
// デフォルトでC言語のprintfのようなことができる
println!("{} is connected!", ready.user.name);
}
}
neko コマンド
commands
以下にneko
コマンドを実装します。
use serenity::framework::standard::{macros::command, CommandResult};
use serenity::model::prelude::*;
use serenity::prelude::*;
#[command]
#[description = "猫のように鳴く"]
async fn neko(ctx: &Context, msg: &Message) -> CommandResult {
// msg.channel_id.say で、channel_id の channel にメッセージを投稿
msg.channel_id
.say(&ctx.http, format!("{} にゃーん", msg.author.mention()))
.await?;
// CommandResultはResultを継承している
// `Result?` は正常な値の場合、Resultの中身を返し、エラーの場合は即座にreturnする演算子
Ok(())
}
mod.rs
を書いて公開します。
pub mod neko;
コマンドを読み込みます。
use commands::{neko::*};
#[group]
#[description("汎用コマンド")]
#[summary("一般")]
#[commands(neko)]
struct General;
help コマンド
ユーザ定義の help コマンドです。~help
(~
はコマンドプレフィックス) と入力したときに送信されるメッセージです。さらに細かくカスタマイズできます。
#[help] // Helpコマンド
#[individual_command_tip = "これはヘルプコマンド"] // Helpコマンドの説明
#[strikethrough_commands_tip_in_guild = ""] // 使用できないコマンドについての説明を削除
async fn my_help(
ctx: &Context,
msg: &Message,
args: Args,
help_options: &'static HelpOptions,
groups: &[&'static CommandGroup],
owners: HashSet<UserId>,
) -> CommandResult {
// _ は使用しない返り値を捨てることを明示している
let _ = help_commands::with_embeds(ctx, msg, args, help_options, groups, owners).await;
// 空のタプルをreturn(仕様)
// Rustでは`;`なしの行は値としてreturnすることを表す
// return Ok(()); と同義
Ok(())
}
トークン読み込み
config.json
に先程作った Bot のトークンを貼り付けましょう。"This_is_Token" という部分を置き換えてください。
JSON の読み込みについて詳しくは他の記事を参照ください。serde
というクレートを使用しています。
#[derive(Serialize, Deserialize)]
struct Token {
token: String,
}
//{"token": "This_is_Token"} の形のトークンを取り出す関数
fn get_token(file_name: &str) -> Result<String> {
let file = File::open(file_name).unwrap();
let reader = BufReader::new(file);
let t: Token = serde_json::from_reader(reader).unwrap();
Ok(t.token)
}
main 関数
Bot のメインループになります。ここでは、実際に動かすクライアントを作成します。
#[tokio::main]
async fn main() {
// Discord Bot Token を設定
let token = get_token("config.json").expect("Err トークンが見つかりません");
// コマンド系の設定
let framework = StandardFramework::new()
// |c| c はラムダ式
.configure(|c| c.prefix("~")) // コマンドプレフィックス
.help(&MY_HELP) // ヘルプコマンドを追加
.group(&GENERAL_GROUP); // general を追加するには,GENERAL_GROUP とグループ名をすべて大文字にする
// Botのクライアントを作成
let mut client = Client::builder(&token)
.event_handler(Handler) // 取得するイベント
.framework(framework) // コマンドを登録
.await
.expect("Err creating client"); // エラーハンドリング
// メインループ。Botを起動
if let Err(why) = client.start().await {
println!("Client error: {:?}", why);
}
}
実行
cargo run
を実行すると Bot が起動します。run
はプログラムのビルドと実行を同時に行います。
Linux を使っていて、バックグラウンドで実行したい場合は
nohup cargo run &
を実行します。
最終的にリリースビルドを行い、Bot を起動するときは、
cargo run --release
を実行します。--release
フラグにより、約 20 倍高速化されます。
全コード
t4t5u0/DiscordBotWithRust にあります。
動作例
おわりに
今回のプログラムはt4t5u0/DiscordBotWithRustにあります。個人的には、discord.py と書き方が似ているのでとても書きやすかったです。まだまだ追えていないコンフィグや機能がたくさんありますが、知見がたまったら何かの形で共有したいと思います。-> serenity チートシート(予定) にちまちまと書き溜めています。参考にしてみてください。
公式のサンプルはserenity/examples at current · serenity-rs/serenityになります。参考になるかもしれません。
Discussion