はじめに
この記事はDartとnyxxを利用した Discord Bot 開発のチュートリアルです。
私はdiscord.pyの開発終了と共にDiscord BotをDartに切り替えました。
しかし、Dartで書かれたDiscord Botに関する記事はまだ少なく、ライブラリのサンプルコードも簡潔であるため、初めて触る方は苦労するでしょう。
この記事ではnyxxの内容を簡単に噛み砕き、
Shun Tannai (@1ntegrale9)さんのPythonで実用Discord Bot(discordpy解説)に倣って、
Botを作成する手順とよく使う機能の実装方法を紹介します。
nyxxについて
nyxxはDartでDiscordボットを作成するためのシンプルで堅牢なフレームワークです。公式によると特徴は以下の通り。
-
スラッシュコマンドのサポート
スラッシュコマンドを作成および処理するための簡単なAPIをサポートおよび提供する -
コマンドフレームワークが含まれている
コマンドをサポートするボットをすばやく作成する方法がある。フレームワークの実装はシンプルで、なおかつすべてが自動的に行われる -
クロスプラットフォーム
コマンドライン、ブラウザ、およびモバイルデバイスで動作する -
素晴らしいコントロール
すべての送信HTTPリクエストまたはWebSocketメッセージを制御できる -
完成したライブラリ
ほぼすべてのDiscordAPIエンドポイントをサポートしている
困ったときは
- Dartの言語仕様とコミュニティ
-
Dart documentation(英語)
公式サイトのドキュメント。Javaなどに比べてシンプルに書かれています。日本語版はありません。 -
Dart 2 Language Guide(日本語)
言語概説書(A Tour of the Dart Language)の和訳をベースとし、一部を補足して解説したサイト。
-
Dart documentation(英語)
- Discord公式APIの仕様とコミュニティ
- Discord Developer Portal(英語)
- Discord サーバー(英語)
- nyxxの仕様とコミュニティ
- nyxx - Dart API docs(英語)
- Discord サーバー(英語)
Discord API Keyの取得
まずはDiscord Developer PortalでBotのアカウントを作成し、
Discordサーバーに登録しましょう。
アクセストークンも必要なので取得してください。
アクセストークンってなーんだ
アクセストークンとは、Discordボットとやり取りするときに必要な合言葉です。アクセストークンを知っていればボットを操ることができます。アクセストークンは絶対に公開してはいけません
アクセストークンさえ分かればボットを乗っ取ることができます。
詳細な手順は以下のShun Tannai (@1ntegrale9)さんの記事を参照してください。
Discord Botアカウント初期設定ガイド for Developer
環境構築
Windowsではscoopで簡単にdart
コマンドが使えるようになります。
PS> scoop install dart
PS> dart --version
Dart SDK version: 2.14.4 (stable) (Wed Oct 13 11:11:32 2021 +0200) on "windows_x64"
以下のコマンドを実行してDiscordボットを作るためのリポジトリを作ります。
your-repository-name
はリポジトリの名前です。
PS> dart create your-repository-name
PS> dart run # 動作確認
Hello, world!
nyxx
をインストールします。
PS> dart pub add nyxx
さて、これから実際にコードを書くことになりますが、
アクセストークンを絶対に公開しないようにするために、
環境変数を設定します。
rem 自分のBotのアクセストークンに置き換えてください
setx DISCORD_BOT_TOKEN "THi5IsDuMMyaCCesSTOK3n00.Cl2FMQ.ThIsi5DUMMyAcc3s5ToKen0000"
# 自分のBotのアクセストークンに置き換えてください
export DISCORD_BOT_TOKEN="THi5IsDuMMyaCCesSTOK3n00.Cl2FMQ.ThIsi5DUMMyAcc3s5ToKen0000"
アクセストークンを絶対に公開しないようにするためには、
ソースコードにトークンをベタ書きしないことなんですが、
そのために環境変数を設定します。
とはいえ、
「デバッグ中だから、今だけ……」
「今は開発中だから、ちょっとだけ……」
と思ってソースコードにトークンをベタ書きするのが人間の性です。
そして、うっかりコミットしてしまい、気づかぬままプッシュして、トークンお漏らしするまでが人間の性です。
まあトークンお漏らししても、
GitHubは優秀なのですぐに教えてくれるし、
Discordもそれを知ってかトークンを自動で無効化してくれますけどね。
しかし、あなたがGitHubを使っているとは限りませんし、
そもそもコミットしたりプッシュするなというわけで……
そこで、git-secrets
を使います。
git-secrets
は機密情報が含まれたファイルのコミットを機械的にリジェクトします。
git-secrets
をインストールします。
インストールの仕方の詳細はこちらをご覧ください(OSによって異なります)。
PS> cd .. # 任意のリポジトリに移動
PS> git clone https://github.com/awslabs/git-secrets
PS> cd ./git-secrets/
PS> ./install.ps1
Checking to see if installation directory already exists...
Creating installation directory.
Copying files.
Checking if directory already exists in Path...
Adding to path.
Adding to user session.
Done.
さて、Discordボットのリポジトリに再び戻って、設定を行います。
PS> cd ../your-repository-name
PS> git secrets --add '[A-z0-9_]{24}\.[A-z0-9_]{6}\.[A-z0-9_]{27}\.'
PS> git secrets --install
事始め
何事もやってみることが大切です。
nyxxではいくつかの方法をサポートしています。
-
nyxx
ボットを作るときの必須ライブラリです
これだけでも基本的なボットを作ることができます -
nyxx_interactions
スラッシュコマンドを使いたい場合はこれを使いましょう -
nyxx_commander
簡単にコマンドで反応するボットが作れます
しかし、サンプルコードがちょっと動かなかったので、これについては後日書きます -
nyxx_extensions
拡張機能を作ることができますが、上級者向けなので本記事では割愛します -
nyxx_lavalink
Lavalink APIにサポートを追加することで音楽ボットを作成できますが、本記事では割愛します -
nyxx_pagination
ページネーションを使いたい場合はこれを使いましょう
基本的な使用法(nyxx)
まずはサンプルコードを書いて、きちんと動くか確認してみましょう。
Ping!
というメッセージを送信すると、
それに反応して、Pong!
というメッセージを送信するボットを作ります。
import 'dart:io';
import "package:nyxx/nyxx.dart";
// メイン関数
void main() {
// アクセストークンの取得
final Map<String, String> envVars = Platform.environment;
final String? token = envVars["DISCORD_BOT_TOKEN"];
if (token == null) {
throw "Token is not difined. Please set DISCORD_BOT_TOKEN.";
}
// 新しいボットのインスタンスを作成します
final bot =
NyxxFactory.createNyxxWebsocket(token, GatewayIntents.allUnprivileged)
..registerPlugin(Logging()) // デフォルトのロギングプラグイン
..registerPlugin(CliIntegration()) // SIGTERMおよびSIGKILLを介してアプリケーションを停止できるプラグイン
..registerPlugin(IgnoreExceptions()) // 発生する可能性のあるキャッチされない例外を処理するプラグイン
..connect();
// ボットが起動すると発生するイベントです
// キャッシュは空である場合もあれば、完全でない場合もあることに注意してください
bot.eventsWs.onReady.listen((e) {
print("Ready!");
});
// メッセージを受信するとイベントが発火します
bot.eventsWs.onMessageReceived.listen((e) {
// メッセージの内容が「Ping!」と等しいかどうかを確認します
if (e.message.content == "Ping!") {
// メッセージが受信されたチャンネルへ「Pong!」を送ります
e.message.channel.sendMessage(MessageBuilder.content("Pong!"));
}
});
}
説明はコメントアウトに書いてる通りです。
dart run
をして動かしてみましょう。
うまく動けばこのようになります。
うまく動きましたか?
環境変数を設定したのに、うまくトークンが読み込めない場合は……
Windowsの場合、再起動するとうまくいく場合があります。
スラッシュコマンドを使う(nyxx_interactions)
スラッシュコマンドを使いたいときは、nyxx_interactions
をインストールします。
PS> dart pub add nyxx_interactions
サンプルコードを書いて、きちんと動くか確認してみましょう。
ping
コマンドを送信すると、
それに反応して、Pong!
というメッセージを送信するボットを作ります。
import 'dart:io';
import "package:nyxx/nyxx.dart";
import "package:nyxx_interactions/nyxx_interactions.dart";
// 名前(name)、説明(description)、およびサブオプション(sub options)を使用してスラッシュコマンドビルダーのインスタンスを作成します
// コマンドをDiscordと同期させ、それらに応答できるようにするために使用されます
// SlashCommandBuilderを使用すると、コマンドイベントに応答できるスラッシュコマンドのハンドラを登録できます
final singleCommand = SlashCommandBuilder("ping", "\"Pong!\"を返します", [],
// スラッシュコマンドが登録されているギルドです
// グローバルコマンドの場合は指定しません
guild: 302360552993456135.toSnowflake())
// ハンドラは、相互作用に応答するために必要なすべてのものを含むSlashCommandInteractionのパラメータを持つ関数を受け入れます
// そこから2つのルートがあります。ACKしてから後で応答するか、ACKなしですぐに応答します
// ACKを送信すると、ボットが考えていることを示すインジケータが表示され、そこから15分以内にそのインタラクションに応答します
..registerHandler((event) async {
await event.respond(MessageBuilder.content("Pong!"));
});
void main() {
final Map<String, String> envVars = Platform.environment;
final String? token = envVars["DISCORD_BOT_TOKEN"];
if (token == null) {
throw "Token is not difined. Please set DISCORD_BOT_TOKEN.";
}
final bot =
NyxxFactory.createNyxxWebsocket(token, GatewayIntents.allUnprivileged)
..registerPlugin(Logging())
..registerPlugin(CliIntegration())
..registerPlugin(IgnoreExceptions())
..connect();
IInteractions.create(WebsocketInteractionBackend(bot))
..registerSlashCommand(singleCommand)
..syncOnReady();
}
説明はコメントアウトに書いてる通りです。
ギルド(Guild)について
Discordサーバーのことを、Discord APIでは「ギルド(Guild)」と呼びます。なんでUIとAPIで名前が違うかというと、これはサーバーが本来のサーバーと紛らわしいからだと推測します。じゃあなんで最初からギルドっていう名前にしなかったんだ……
dart run
をして動かしてみましょう。
うまく動けばこのような表示が出てきます。
コマンドを使用すると、Pong!と返事します。
うまく動きましたか?
ページネーションを使う(nyxx_pagination)
import "dart:async";
import "package:nyxx/nyxx.dart";
import 'package:nyxx_interactions/nyxx_interactions.dart';
import "package:nyxx_pagination/nyxx_pagination.dart";
FutureOr<void> paginationExampleInteraction(ISlashCommandInteractionEvent event) {
final paginator = EmbedComponentPagination(event.interactions, [
EmbedBuilder()..description = "最初のページ",
EmbedBuilder()..description = "2番目のページ",
]);
event.respond(paginator.initMessageBuilder());
}
void main() {
final Map<String, String> envVars = Platform.environment;
final String? token = envVars["DISCORD_BOT_TOKEN"];
if (token == null) {
throw "Token is not difined. Please set DISCORD_BOT_TOKEN.";
}
final bot =
NyxxFactory.createNyxxWebsocket(token, GatewayIntents.allUnprivileged)
..registerPlugin(Logging())
..registerPlugin(CliIntegration())
..registerPlugin(IgnoreExceptions())
..connect();
IInteractions.create(WebsocketInteractionBackend(bot))
..registerSlashCommand(SlashCommandBuilder("paginated", "これはページネーションの例です", [], guild: 302360552993456135.toSnowflake())
..registerHandler(paginationExampleInteraction))
..syncOnReady();
}
dart run
をして動かしてみましょう。
うまく動けばこのような表示が出てきます。
コマンドを使用すると、ページネーションが表示されます。
>
を押すと次のページに移動します。
うまく動きましたか?
実用編
さて、事始めを終えたら、実用編です。
よくある機能
Discordボットで使いそうな具体的な機能の例をいくつか紹介します。
ボットが起動したときにメッセージを送信する
// ボットが起動したときにイベントが発火します
bot.eventsWs.onReady.listen((event) async {
// ボットから非同期でチャンネルを取得します(302360552993456135は送信先のチャンネルID)
final channel = await bot.fetchChannel(302360552993456135.toSnowflake());
// チャンネルの種類がテキストチャンネルであることを確認します
if (channel.channelType == ChannelType.text) {
// テキストチャンネルにキャストします
final textChannel = channel as ITextChannel;
// テキストチャンネルに「起動しました!」というメッセージを送信します
await textChannel.sendMessage(MessageBuilder.content("起動しました!"));
} else {
// テキストチャンネルでなければ、チャンネルIDを間違えています
// プログラムを修正する必要があるので、Errorを投げます
throw Error();
}
});
main
関数にこれを追加すれば、ボットが起動したときにメッセージを送信します。
こんな感じでお友達も喜びます。
インタラクションで省略可能な引数を扱う
/neko
で「にゃーん」と返信し、/neko text:テキスト
でテキストに含まれるすべての「な」を「にゃ」に置換し、語尾に「にゃ」を付けたメッセージを返信するコマンドを実装します。
IInteractions.create(WebsocketInteractionBackend(bot))
..registerSlashCommand(SlashCommandBuilder("neko", "「にゃーん」と反応します",
[CommandOptionBuilder(CommandOptionType.string, "text", "メッセージ")],
guild: 302360552993456135.toSnowflake())
..registerHandler((event) {
// コマンドに対して応答する
event.respond(MessageBuilder.content(
// 引数に"text"という名前の要素があるか確認する
event.args.any((element) => element.name == "text")
// "text"がある場合
? event // イベントから
.getArg("text") // 引数"text"を取得して
.value // その引数の値を取得して
.toString() // 文字列に変換して
.replaceAll("な", "にゃ") // 「な」を「にゃ」に置換して
.replaceAll(RegExp(r"$"), "にゃ") // 末尾に「にゃ」を追加
// "text"がない場合
: "にゃーん")); // 「にゃーん」と返信する
}))
..syncOnReady();
main
関数にこれを追加すれば、ボットが起動したときにメッセージを送信します。
上が/neko
を実行したとき、
下が/neko text:ななめななじゅうななどのならびでなくなくいななくななはんななだいなんなくながめてながながめ
を実行したときの反応です。
インタラクションで省略できない引数を扱うとき
逆にインタラクションで省略できない引数を扱いたいときはCommandOptionBuilder
にrequired: true
を追加します。
イベントの一覧
イベントの一覧を紹介します。
bot.eventsWs.'ここにイベント名を書きます'.listen((event) {});
この表を見れば、何ができるか色々と想像することができるでしょう。
イベント名 | 説明 |
---|---|
onDisconnect |
切断されたときに発行されるイベント |
onReady |
起動できたときに発行されるイベント |
onMessageReceived |
メッセージを受信したときに発行されるイベント |
onDmReceived |
プライベートメッセージを受信したときに発行されるイベント |
onChannelPinsUpdate |
チャンネルのピンが更新されたときに発行されるイベント |
onGuildEmojisUpdate |
サーバーの絵文字が変更されたときに発行されるイベント |
onMessageUpdate |
メッセージが更新されたときに発行されるイベント |
onMessageDelete |
メッセージが削除されたときに発行されるイベント |
onChannelCreate |
チャンネルが作成されたときに発行されるイベント |
onChannelUpdate |
チャンネルが更新されたときに発行されるイベント |
onChannelDelete |
チャンネルが削除されたときに発行されるイベント |
onGuildBanAdd |
メンバーがバンされたときに発行されるイベント |
onGuildBanRemove |
バンを取り消したときに発行されるイベント |
onGuildCreate |
サーバーにボットが参加したときに発行されるイベント |
onGuildUpdate |
サーバーが更新されたときに発行されるイベント |
onGuildDelete |
サーバーからボットが去るときに発行されるイベント |
onGuildMemberAdd |
メンバーがサーバーに参加したときに発行されるイベント |
onGuildMemberUpdate |
メンバーが更新したときに発行されるイベント |
onGuildMemberRemove |
ユーザーがサーバーから去るときに発行されるイベント |
onPresenceUpdate |
|
onTyping |
ユーザーが入力を始めたときに発行されるイベント |
onRoleCreate |
ロールが作成されたときに発行されるイベント |
onRoleUpdate |
ロールが更新されたときに発行されるイベント |
onRoleDelete |
ロールが削除されたときに発行されるイベント |
onMessageDeleteBulk |
|
onMessageReactionAdded |
ユーザーがメッセージにリアクションを追加したときに発行されるイベント |
onMessageReactionRemove |
ユーザーがメーセージからリアクションを削除したときに発行されるイベント |
onMessageReactionsRemoved |
ユーザーがメーセージからすべてのリアクションを削除したときに発行されるイベント |
onVoiceStateUpdate |
ボイスチャットの入退室・移動時に発行されるイベント |
onVoiceServerUpdate |
ユーザーが更新されたときに発行されるイベント |
onUserUpdate |
ユーザーが更新されたときに発行されるイベント |
onSelfMention |
ボットがメンションされたときに発行されるイベント |
onInviteCreated |
招待リンクが作成されたときに発行されるイベント |
onInviteDeleted |
招待リンクが削除されたときに発行されるイベント |
onMessageReactionRemoveEmoji |
|
onThreadCreated |
スレッドが作成されたときに発行されるイベント |
onThreadMembersUpdate |
スレッドにメンバーが追加・削除されたときに発行されるイベント |
onThreadDelete |
スレッドが削除されたときに発行されるイベント |
onStageInstanceCreate |
ステージチャンネルが作成されたときに発行されるイベント |
onStageInstanceUpdate |
ステージチャンネルが更新されたときに発行されるイベント |
onStageInstanceDelete |
ステージチャンネルが削除されたときに発行されるイベント |
onGuildStickersUpdate |
|
onGuildEventCreate |
|
onGuildEventUpdate |
|
onGuildEventDelete |
空白部分は複雑な説明のため省いているものと、ライブラリのドキュメントが明らかに誤っているため書いていないものがあります。
終わりに
Dartは知名度が致命的ですが、良い言語だと思います。
みんな好きな言語でDiscordボットを書くといいですよ。
それではみなさん、いいDiscordボットライフを!