7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

限界開発鯖Advent Calendar 2021

Day 25

Dartで実用Discord Bot(nyxx)

Last updated at Posted at 2021-12-24

はじめに

この記事はDartnyxxを利用した 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エンドポイントをサポートしている

困ったときは

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

さて、これから実際にコードを書くことになりますが、
アクセストークンを絶対に公開しないようにするために、
環境変数を設定します。

PowerShellの場合
rem 自分のBotのアクセストークンに置き換えてください
setx DISCORD_BOT_TOKEN "THi5IsDuMMyaCCesSTOK3n00.Cl2FMQ.ThIsi5DUMMyAcc3s5ToKen0000"
Bashの場合
# 自分のBotのアクセストークンに置き換えてください
export DISCORD_BOT_TOKEN="THi5IsDuMMyaCCesSTOK3n00.Cl2FMQ.ThIsi5DUMMyAcc3s5ToKen0000"

アクセストークンを絶対に公開しないようにするためには、
ソースコードにトークンをベタ書きしないことなんですが、
そのために環境変数を設定します。

とはいえ、
「デバッグ中だから、今だけ……」
「今は開発中だから、ちょっとだけ……」
と思ってソースコードにトークンをベタ書きするのが人間の性です。
そして、うっかりコミットしてしまい、気づかぬままプッシュして、トークンお漏らしするまでが人間の性です。

まあトークンお漏らししても、
GitHubは優秀なのですぐに教えてくれるし、
Discordもそれを知ってかトークンを自動で無効化してくれますけどね。
しかし、あなたがGitHubを使っているとは限りませんし、
そもそもコミットしたりプッシュするなというわけで……

そこで、git-secretsを使います。
git-secretsは機密情報が含まれたファイルのコミットを機械的にリジェクトします。

git-secretsをインストールします。
インストールの仕方の詳細はこちらをご覧ください(OSによって異なります)。

Windowsの場合
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をして動かしてみましょう。
うまく動けばこのようになります。
image.png
うまく動きましたか?

環境変数を設定したのに、うまくトークンが読み込めない場合は……
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をして動かしてみましょう。
うまく動けばこのような表示が出てきます。
image.png
コマンドを使用すると、Pong!と返事します。
image.png
うまく動きましたか?

ページネーションを使う(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をして動かしてみましょう。
うまく動けばこのような表示が出てきます。
image.png
コマンドを使用すると、ページネーションが表示されます。
image.png
>を押すと次のページに移動します。
image.png
うまく動きましたか?

実用編

さて、事始めを終えたら、実用編です。

よくある機能

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関数にこれを追加すれば、ボットが起動したときにメッセージを送信します。
image.png
こんな感じでお友達も喜びます。

インタラクションで省略可能な引数を扱う

/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関数にこれを追加すれば、ボットが起動したときにメッセージを送信します。
image.png
上が/nekoを実行したとき、
下が/neko text:ななめななじゅうななどのならびでなくなくいななくななはんななだいなんなくながめてながながめを実行したときの反応です。

インタラクションで省略できない引数を扱うとき
逆にインタラクションで省略できない引数を扱いたいときはCommandOptionBuilderrequired: 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ボットライフを!

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?