🔖

Rustで作るdiscord bot入門編 (serenity使用)

9 min read

はじめに

この記事は、Rust 製 discord bot 用ライブラリserenityについてとてもざっくり解説するものです。公式 GitHub の example を元に構成しています。紹介しきれていない機能がたくさんあるので、気になった方は参考の公式ドキュメントをご覧ください。

本記事での目標

  • serenity を用いて簡単な Discord Bot を作成する
    今回は、コマンドを、メッセージの判定ベースではなく、関数として実装していきます。いくつか理由はありますが、一番の理由は見通しがよくなるからです。メッセージの判定ベースでのやりかたは、serenity/main.rs at current · serenity-rs/serenityをご覧ください。

本記事の対象読者

Rust の文法をある程度知っていることが望ましいですが、Rust がわからないという人でも大丈夫です。The Rust Programming Language 日本語版 を隣に開きながら進めるといいでしょう。資料中でも簡単な解説は行います。

Discord とは

グループでの通話やチャットに特化した SNS です。サーバーと呼ばれるグループの中にチャンネルというスペースがあります。チャンネルは複数作ることができて、目的ごとに使い分けることができます。DL はこちらから。Discord | 想像してみよう

Discord bot とは

Discord はサーバーに Bot を追加できます。Bot には様々な機能をもたせることができます。既存のものでは、音楽をかける Bot や、サイコロを振る Bot などがあります。今回は、独自の機能を持った Bot を作っていきます。

Rust とは

一言でいうと、高速かつ安全な素晴らしい言語です。イケメンです。

serenity を選んだ理由

  • 現在(2021 年 5 月)開発が盛んなこと
  • discord.py によく似ていること
    の 2 点です。

先述しましたが、Rapptz/discord.py: An API wrapper for Discord written in Python.というPython製の discord bot フレームワークが開発終了となりました。以降公式にメンテナンスはされないので、機能追加やセキュリティ面での不安があります。

本編

ディレクトリ構成

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をクリックします。
8ae2fa4d70a0bad909a929f196bbdd6a.png (947×224)

Bot の名前を入力し、Bot を作成します。
1ba19d681bbad34af0b6d93419c5df86.png (473×403)

左上のハンバーガーメニューをクリックし、Bot のページに遷移します。
1996537dfdfbd24a36cd6a3e80d08ad5.png (954×501)

Add Bot をクリックします。
c0d594349d6c414fe65a10d86d4ea408.png (947×484)

これで Bot が作成されました。Copyを押すと Bot のトークンがクリップボードにコピーされます。これは後で使います。
e9e29ae38dc4f03ec6545ba4732ce3f3.png (953×587)

Bot の招待リンクを作成ます。OAuth のページを開きます。
f314701c5145c95e3e1ab240fb869040.png (952×478)

下にスクロールし、bot にチェックを入れます。更に下に招待リンクが作成されました。
3bbd95b2bd7bc158aba31e276b781fcb.png (952×926)
これをブラウザに貼り付けて 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 ファイルを操作するためのクレートです。

クレートのインポート

説明のためには、それぞれ必要な場所に書いたほうがわかりやすいですが、面倒なのでまとめています。(気が向いたら分けます)

main.rs
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コマンドを実装します。

commands/neko.rs
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 を書いて公開します。

commands/mod.rs
pub mod neko;

コマンドを読み込みます。

main.rs
use commands::{neko::*};

#[group]
#[description("汎用コマンド")]
#[summary("一般")]
#[commands(neko)]
struct General;

help コマンド

ユーザ定義の help コマンドです。~help(~はコマンドプレフィックス) と入力したときに送信されるメッセージです。さらに細かくカスタマイズできます。

main.rs
#[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というクレートを使用しています。

main.rs
#[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 のメインループになります。ここでは、実際に動かすクライアントを作成します。

main.rs
#[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 にあります。

動作例

74607e4ad2c9c18aa48a4c26c09221b3.png (456×433)

おわりに

今回のプログラムはt4t5u0/DiscordBotWithRustにあります。個人的には、discord.py と書き方が似ているのでとても書きやすかったです。まだまだ追えていないコンフィグや機能がたくさんありますが、知見がたまったら何かの形で共有したいと思います。-> serenity チートシート(予定) にちまちまと書き溜めています。参考にしてみてください。

公式のサンプルはserenity/examples at current · serenity-rs/serenityになります。参考になるかもしれません。

参考

Discussion

ログインするとコメントできます