Discord Botの作成方法:概要とチュートリアル

公開: 2022-03-11

Discordは、「ゲーマー向けのオールインワンの音声およびテキストチャット」と自称するリアルタイムメッセージングプラットフォームです。 洗練されたインターフェイス、使いやすさ、豊富な機能により、Discordは急速な成長を遂げ、ビデオゲームにほとんど関心のない人々の間でもますます人気が高まっています。 2017年5月から2018年5月の間に、そのユーザーベースは4500万ユーザーから1億3000万人以上に急増し、1日あたりのユーザー数はSlackの2倍以上になりました。

チャットボット開発者の観点から見たDiscordの最も魅力的な機能の1つは、Discordを外の世界と統合し、ユーザーにより魅力的なエクスペリエンスを提供するのに役立つプログラム可能なボットの強力なサポートです。 ボットはDiscordに遍在しており、モデレート支援、ゲーム、音楽、インターネット検索、支払い処理など、幅広いサービスを提供します。

このDiscordボットのチュートリアルでは、まず、Discordユーザーインターフェイスとボット用のRESTおよびWebSocket APIについて説明してから、JavaScriptで簡単なDiscordボットを作成するチュートリアルに進みます。 最後に、特定のメトリックによって、Discordの最も人気のあるボットの開発者と、彼の重要なインフラストラクチャとコードベースの開発と保守の経験について聞きます。

Discordユーザーインターフェース

技術的な詳細について説明する前に、ユーザーがDiscordとどのように対話し、Discordがユーザーにどのように表示されるかを理解することが重要です。 ボットに表示される方法は、概念的には似ています(ただし、もちろん非視覚的です)。 実際、公式のDiscordアプリケーションは、ボットが使用するのと同じAPIに基づいて構築されています。 技術的には、ほとんど変更を加えずに通常のユーザーアカウント内でボットを実行することは可能ですが、これはDiscordの利用規約で禁止されています。 ボットはボットアカウントで実行する必要があります。

Chrome内で実行されているDiscordアプリケーションのブラウザバージョン1を見てみましょう。

Discord Web UI

1デスクトップアプリケーションのDiscordUIは、Electronにパッケージ化されたWebアプリケーションと実質的に同じです。 iOSアプリケーションはReactNativeで構築されています。 AndroidアプリケーションはネイティブのAndroidJavaコードです。

分解してみましょう。

1.サーバーリスト

左側には、私がメンバーになっているサーバーのリストがあります。 Slackに精通している場合、サーバーはSlackワークスペースに類似しており、サーバー内の1つ以上のチャネル内で相互に対話できるユーザーのグループを表します。 サーバーは、その作成者および/またはサーバーが選択して責任を委任することを選択したスタッフによって管理されます。 作成者やスタッフは、ルール、サーバー内のチャネルの構造を定義し、ユーザーを管理します。

私の場合、 DiscordAPIサーバーは私のサーバーリストの一番上にあります。 ヘルプを取得したり、他の開発者と話したりするのに最適な場所です。 その下には、私が作成したTestというサーバーがあります。 後で作成するボットをテストします。 その下には、新しいサーバーを作成するためのボタンがあります。 誰でも数回クリックするだけでサーバーを作成できます。

Discordのユーザーインターフェースで使用される用語はサーバーですが、開発者向けドキュメントとAPIで使用される用語はギルドであることに注意してください。 技術的なトピックについて話すことに移ったら、ギルドについて話すことに切り替えます。 2つの用語は交換可能です。

2.チャンネルリスト

サーバーリストのすぐ右側には、現在表示しているサーバー(この場合はDiscord APIサーバー)のチャネルのリストがあります。 チャネルは、任意の数のカテゴリに分割できます。 示されているように、Discord APIサーバーでは、カテゴリーには、INFORMATION、GENERAL、およびLIBSが含まれます。 各チャネルはチャットルームとして機能し、ユーザーはチャネルが専門としているトピックについて話し合うことができます。 現在表示しているチャンネル(情報)の背景は明るいです。 最後に表示してから新しいメッセージがあるチャネルのテキストの色は白です。

3.チャンネルビュー

これは、現在表示しているチャンネルでユーザーが話している内容を確認できるチャンネルビューです。 ここに1つのメッセージが表示されますが、部分的にしか表示されません。 これは、個々のDiscordボットライブラリのサーバーをサポートするためのリンクのリストです。 サーバー管理者は、私のような通常のユーザーがこのチャネルでメッセージを送信できないようにこのチャネルを構成しました。 管理者は、このチャネルを掲示板として使用して、チャットで簡単に確認でき、溺れることのない重要な情報を投稿します。

4.ユーザーリスト

右側には、このサーバーで現在オンラインになっているユーザーのリストがあります。 ユーザーはさまざまなカテゴリに分類され、名前の色も異なります。 これは彼らが持っている役割の結果です。 ロールは、ユーザーが表示されるカテゴリ(存在する場合)、名前の色、およびサーバーでのアクセス許可を記述します。 ユーザーは複数の役割を持つことができ(そして非常に頻繁にそうします)、その場合に何が起こるかを決定するいくつかの優先順位の計算があります。 少なくとも、すべてのユーザーには@everyoneロールがあります。 その他の役割は、サーバースタッフによって作成および割り当てられます。

5.テキスト入力

これは、許可されている場合にメッセージを入力して送信できるテキスト入力です。 このチャンネルでメッセージを送信する権限がないため、ここに入力できません。

6.ユーザー

これは現在のユーザーです。 混乱しないように、また名前の選択がひどいので、ユーザー名を「私」に設定しました。 私のユーザー名の下には、私の識別器である番号(#9484)があります。 「Me」という名前のユーザーは他にもたくさんいるかもしれませんが、「Me#9484」は私だけです。 サーバーごとに自分のニックネームを設定することもできるので、サーバーごとに異なる名前で知られるようになります。

これらはDiscordユーザーインターフェイスの基本的な部分ですが、他にもたくさんあります。 アカウントを作成しなくてもDiscordの使用を開始するのは簡単ですので、お気軽にご確認ください。 Discordのホームページにアクセスし、「ブラウザでDiscordを開く」をクリックし、ユーザー名を選択して、「バスの写真をクリックする」というさわやかなラウンドを1、2回再生することで、Discordに入ることができます。

Discord API

Discord APIは、WebSocketAPIとRESTAPIの2つの別個の部分で構成されています。 大まかに言えば、WebSocket APIはDiscordからイベントをリアルタイムで受信するために使用され、RESTAPIはDiscord内でアクションを実行するために使用されます。

不和ボット通信ループを作成する方法

WebSocket API

WebSocket APIは、メッセージの作成、メッセージの削除、ユーザーのキック/禁止イベント、ユーザー権限の更新など、Discordからイベントを受信するために使用されます。 一方、ボットからWebSocketAPIへの通信はより制限されています。 ボットはWebSocketAPIを使用して、接続を要求し、自身を識別し、ハートビートし、音声接続を管理し、さらにいくつかの基本的なことを行います。 詳細については、Discordのゲートウェイドキュメントを参照してください(WebSocket APIへの単一の接続はゲートウェイと呼ばれます)。 他のアクションを実行するには、RESTAPIが使用されます。

WebSocket APIからのイベントには、イベントのタイプに依存する情報を含むペイロードが含まれています。 たとえば、すべてのメッセージ作成イベントには、メッセージの作成者を表すユーザーオブジェクトが付随します。 ただし、ユーザーオブジェクトだけでは、ユーザーについて知っておくべきすべての情報が含まれているわけではありません。 たとえば、ユーザーの権限に関する情報は含まれていません。 さらに情報が必要な場合は、REST APIにクエリを実行できますが、次のセクションでさらに説明する理由により、通常は、前のイベントから受信したペイロードから構築する必要があるキャッシュにアクセスする必要があります。 Guild CreateGuild Role UpdateChannel Updateなど、ユーザーの権限に関連するペイロードを配信するイベントは多数あります。

ボットは、WebSocket接続ごとに最大2,500ギルドに存在できます。 ボットがより多くのギルドに存在できるようにするには、ボットはシャーディングを実装し、Discordへのいくつかの個別のWebSocket接続を開く必要があります。 ボットが単一ノード上の単一プロセス内で実行されている場合、これは複雑さを増すだけであり、不必要に思えるかもしれません。 ただし、ボットが非常に人気があり、バックエンドを別々のノードに分散させる必要がある場合、Discordのシャーディングサポートにより、他の場合よりもはるかに簡単になります。

RESTAPI

Discord REST APIは、メッセージの送信、ユーザーのキック/禁止、ユーザー権限の更新(WebSocket APIから受信したイベントにほぼ類似)など、ほとんどのアクションを実行するためにボットによって使用されます。 REST APIを使用して、情報を照会することもできます。 ただし、ボットは主にWebSocket APIからのイベントに依存し、WebSocketイベントから受信した情報をキャッシュします。

これにはいくつかの理由があります。 たとえば、メッセージ作成イベントを受信するたびにユーザー情報を取得するためにREST APIにクエリを実行しても、RESTAPIのレート制限のためにスケーリングされません。 また、ほとんどの場合、WebSocket APIが必要な情報を提供し、それをキャッシュに保存する必要があるため、冗長です。

ただし、いくつかの例外があり、キャッシュに存在しない情報が必要になる場合があります。 ボットが最初にWebSocketゲートウェイに接続すると、そのシャードにボットが存在するReadyイベントとギルドごとに1つのGuild Createイベントが最初にボットに送信され、キャッシュに現在の状態を取り込むことができます。 人口の多いギルドのギルド作成イベントには、オンラインユーザーに関する情報のみが含まれます。 ボットがオフラインユーザーに関する情報を取得する必要がある場合、関連情報がキャッシュに存在しない可能性があります。 この場合、RESTAPIにリクエストを送信するのは理にかなっています。 または、オフラインユーザーに関する情報を頻繁に取得する必要がある場合は、代わりに、リクエストギルドメンバーのオペコードをWebSocket APIに送信して、オフラインギルドメンバーをリクエストすることを選択できます。

もう1つの例外は、アプリケーションがWebSocketAPIにまったく接続されていない場合です。 たとえば、ボットにWebダッシュボードがあり、ユーザーがログインしてサーバーのボットの設定を変更できる場合です。 Webダッシュボードは、WebSocket APIに接続せず、Discordからのデータをキャッシュせずに、別のプロセスで実行できます。 たまにいくつかのRESTAPIリクエストを行うだけでよい場合があります。 この種のシナリオでは、必要な情報を取得するためにRESTAPIに依存することは理にかなっています。

APIラッパー

テクノロジースタックのすべてのレベルをある程度理解することは常に良い考えですが、DiscordWebSocketとRESTAPIを直接使用することは、時間がかかり、エラーが発生しやすく、一般的に不要であり、実際には危険です。

Discordは、公式に精査された図書館の厳選されたリストを提供し、次のように警告しています。

APIを悪用したり、過度のレート制限を引き起こしたりするカスタム実装や非準拠ライブラリを使用すると、永久に禁止される可能性があります。

Discordによって公式に精査されたライブラリは、一般的に成熟しており、十分に文書化されており、DiscordAPIを完全にカバーしています。 ほとんどのボット開発者は、好奇心や勇気を除いて、カスタム実装を開発する正当な理由はありません。

現時点では、公式に精査されたライブラリには、Crystal、C#、D、Go、Java、JavaScript、Lua、Nim、PHP、Python、Ruby、Rust、およびSwiftの実装が含まれています。 選択した言語用に2つ以上の異なるライブラリが存在する場合があります。 どちらを使用するかを選択するのは難しい決断です。 それぞれのドキュメントをチェックすることに加えて、非公式のDiscord APIサーバーに参加して、各ライブラリの背後にあるコミュニティの種類を把握することをお勧めします。

不和ボットの作り方

ビジネスに取り掛かりましょう。 サーバーにぶら下がってKo-fiからのWebhookをリッスンするDiscordボットを作成します。 Ko-fiは、PayPalアカウントへの寄付を簡単に受け入れることができるサービスです。 ビジネスアカウントが必要なPayPalとは対照的に、そこにWebhookを設定するのは非常に簡単なので、デモンストレーション目的や小規模な寄付処理に最適です。

ユーザーが10ドル以上を寄付すると、ボットは名前の色を変更してオンラインユーザーのリストの一番上に移動するPremium Memberの役割をユーザーに割り当てます。 このプロジェクトでは、Node.jsとErisと呼ばれるDiscord APIライブラリを使用します(ドキュメントリンク:https://abal.moe/Eris/)。 ErisだけがJavaScriptライブラリではありません。 代わりにdiscord.jsを選択できます。 どちらの方法でも、作成するコードは非常に似ています。

余談ですが、別の寄付プロセッサであるPatreonは、公式のDiscordボットを提供し、貢献者の利益としてDiscordの役割を構成することをサポートしています。 似たようなものを実装しますが、もちろんもっと基本的なものです。

チュートリアルのすべてのステップのコードは、GitHub(https://github.com/mistval/premium_bot)で入手できます。 この投稿に示されている手順の一部では、簡潔にするために変更されていないコードが省略されているため、何かが足りないと思われる場合は、GitHubへのリンクをたどってください。

ボットアカウントの作成

コードの記述を開始する前に、ボットアカウントが必要です。 ボットアカウントを作成する前に、ユーザーアカウントが必要です。 ユーザーアカウントを作成するには、こちらの手順に従ってください。

次に、ボットアカウントを作成するには、次のようにします。

1)開発者ポータルでアプリケーションを作成します。

開発者ポータルのスクリーンショット

2)アプリケーションに関する基本的な詳細を入力します(ここに表示されているクライアントIDに注意してください。後で必要になります)。

基本的な詳細を入力するスクリーンショット

3)アプリケーションに接続しているボットユーザーを追加します。

ボットユーザーを追加するスクリーンショット

4)PUBLIC BOTスイッチをオフにして、表示されているボットトークンをメモします(これも後で必要になります)。 たとえば、Toptal Blogの投稿の画像でボットトークンを公開した場合など、ボットトークンが漏洩した場合は、すぐに再生成する必要があります。 ボットトークンを所有している人は誰でもボットのアカウントを制御し、あなたとあなたのユーザーに深刻で永続的な問題を引き起こす可能性があります。

「野生のボットが登場しました」のスクリーンショット

5)ボットをテストギルドに追加します。 ボットをギルドに追加するには、そのクライアントID(前に表示)を次のURIに置き換えて、ブラウザーでそのボットに移動します。

https://discordapp.com/api/oauth2/authorize?scope=bot&client_id=XXX

ボットをテストギルドに追加します

[承認]をクリックすると、ボットはテストギルドに追加され、ユーザーリストに表示されます。 オフラインですが、すぐに修正します。

プロジェクトの作成

Node.jsがインストールされていると仮定して、プロジェクトを作成し、Eris(使用するボットライブラリ)、Express(Webhookリスナーの作成に使用するWebアプリケーションフレームワーク)、およびbody-parser(Webhook本体の解析用)をインストールします。 )。

 mkdir premium_bot cd premium_bot npm init npm install eris express body-parser

ボットをオンラインで応答性の高いものにする

赤ちゃんのステップから始めましょう。 まず、ボットをオンラインにして応答します。 これは、10〜20行のコードで実行できます。 新しいbot.jsファイル内で、Erisクライアントインスタンスを作成し、ボットトークン(上記のボットアプリケーションを作成したときに取得)を渡し、クライアントインスタンスでいくつかのイベントをサブスクライブし、Discordに接続するように指示する必要があります。 。 デモンストレーションの目的で、ボットトークンをbot.jsファイルにハードコーディングしますが、別の構成ファイルを作成してソース管理から除外することをお勧めします。

(GitHubコードリンク:https://github.com/mistval/premium_bot/blob/master/src/bot_step1.js)

 const eris = require('eris'); // Create a Client instance with our bot token. const bot = new eris.Client('my_token'); // When the bot is connected and ready, log to console. bot.on('ready', () => { console.log('Connected and ready.'); }); // Every time a message is sent anywhere the bot is present, // this event will fire and we will check if the bot was mentioned. // If it was, the bot will attempt to respond with "Present". bot.on('messageCreate', async (msg) => { const botWasMentioned = msg.mentions.find( mentionedUser => mentionedUser.id === bot.user.id, ); if (botWasMentioned) { try { await msg.channel.createMessage('Present'); } catch (err) { // There are various reasons why sending a message may fail. // The API might time out or choke and return a 5xx status, // or the bot may not have permission to send the // message (403 status). console.warn('Failed to respond to mention.'); console.warn(err); } } }); bot.on('error', err => { console.warn(err); }); bot.connect();

すべてがうまくいけば、独自のボットトークンを使用してこのコードを実行するConnected and ready. コンソールに印刷され、テストサーバーでボットがオンラインになるのがわかります。 ボットを右クリックして[メンション]を選択するか、名前の前に@を入力して、ボットに言及することができます。 ボットは「現在」と応答する必要があります。

ボットが存在します

2メンションは、他のユーザーがいない場合でも、他のユーザーの注意を引く方法です。 通常のユーザーは、言及されると、デスクトップ通知、モバイルプッシュ通知、および/またはシステムトレイのDiscordのアイコンの上に表示される小さな赤いアイコンによって通知されます。 ユーザーに通知する方法は、ユーザーの設定とオンライン状態によって異なります。 一方、ボットは、言及されたときに特別な通知を受け取りません。 他のメッセージと同じように定期的にメッセージ作成イベントを受け取り、イベントに添付されている言及をチェックして、言及されているかどうかを判断できます。

支払いコマンドの記録

ボットをオンラインで取得できることがわかったので、現在のメッセージ作成イベントハンドラーを削除して、ユーザーから支払いを受け取ったことをボットに通知できる新しいハンドラーを作成しましょう。

ボットに支払いを通知するために、次のようなコマンドを発行します。

 pb!addpayment @user_mention payment_amount

たとえば、Meによる$ 10.00の支払いを記録するには、 pb!addpayment @Me 10.00

pb! 一部はコマンドプレフィックスと呼ばれます。 ボットへのすべてのコマンドで始まるプレフィックスを選択することをお勧めします。 これにより、ボットの名前空間の尺度が作成され、他のボットとの衝突を回避できます。 ほとんどのボットにはヘルプコマンドが含まれていますが、ギルドに10個のボットがあり、それらすべてがヘルプに応答した場合の混乱を想像してみてください。 pbを使用して! 同じプレフィックスを使用する他のボットが存在する可能性があるため、プレフィックスは絶対確実なソリューションではありません。 最も人気のあるボットでは、衝突を防ぐために、ギルドごとにプレフィックスを構成できます。 もう1つのオプションは、ボット自身の言及をプレフィックスとして使用することですが、これによりコマンドの発行がより冗長になります。

(GitHubコードリンク:https://github.com/mistval/premium_bot/blob/master/src/bot_step2.js)

 const eris = require('eris'); const PREFIX = 'pb!'; const bot = new eris.Client('my_token'); const commandHandlerForCommandName = {}; commandHandlerForCommandName['addpayment'] = (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`); }; bot.on('messageCreate', async (msg) => { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts of the command and the command name const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the appropriate handler for the command, if there is one. const commandHandler = commandHandlerForCommandName[commandName]; if (!commandHandler) { return; } // Separate the command arguments from the command prefix and command name. const args = parts.slice(1); try { // Execute the command. await commandHandler(msg, args); } catch (err) { console.warn('Error handling command'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();

試してみよう。

ボットとの対話

ボットにpb!addpaymentコマンドに応答させるだけでなく、コマンドを処理するための一般化されたパターンを作成しました。 commandHandlerForCommandNameディクショナリにハンドラーを追加するだけで、コマンドを追加できます。 ここに簡単なコマンドフレームワークの作成があります。 コマンドの処理はボットを作成する上で非常に基本的な部分であるため、多くの人が独自のコマンドフレームワークを作成する代わりに使用できるオープンソースのコマンドフレームワークを作成しています。 コマンドフレームワークでは、多くの場合、クールダウン、必要なユーザー権限、コマンドエイリアス、コマンドの説明と使用例(自動生成されたヘルプコマンドの場合)などを指定できます。 Erisにはコマンドフレームワークが組み込まれています。

権限について言えば、ボットにはセキュリティ上の問題が少しあります。 誰でもaddpaymentコマンドを実行できます。 ボットの所有者だけが使用できるように制限しましょう。 commandHandlerForCommandNameディクショナリをリファクタリングし、値としてJavaScriptオブジェクトを含めるようにします。 これらのオブジェクトには、コマンドハンドラーを含むexecuteプロパティと、ブール値を含むbotOwnerOnlyプロパティが含まれます。 また、ユーザーIDをボットの定数セクションにハードコーディングして、所有者が誰であるかを認識できるようにします。 ユーザーIDを見つけるには、Discord設定で開発者モードを有効にし、ユーザー名を右クリックして[IDのコピー]を選択します。

(GitHubコードリンク:https://github.com/mistval/premium_bot/blob/master/src/bot_step3.js)

 const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const bot = new eris.Client('my_token'); const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`); }, }; bot.on('messageCreate', async (msg) => { try { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts and name of the command const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the requested command, if there is one. const command = commandForName[commandName]; if (!command) { return; } // If this command is only for the bot owner, refuse // to execute it for any other user. const authorIsBotOwner = msg.author.id === BOT_OWNER_ID; if (command.botOwnerOnly && !authorIsBotOwner) { return await msg.channel.createMessage('Hey, only my owner can issue that command!'); } // Separate the command arguments from the command prefix and name. const args = parts.slice(1); // Execute the command. await command.execute(msg, args); } catch (err) { console.warn('Error handling message create event'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();

これで、ボットの所有者以外の誰かがaddpaymentコマンドを実行しようとすると、ボットは怒ってaddpaymentコマンドの実行を拒否します。

次に、ボットに10ドル以上を寄付する人にPremium Memberの役割を割り当てさせましょう。 bot.jsファイルの上部:

(GitHubコードリンク:https://github.com/mistval/premium_bot/blob/master/src/bot_step4.js)

 const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const PREMIUM_CUTOFF = 10; const bot = new eris.Client('my_token'); const premiumRole = { name: 'Premium Member', color: 0x6aa84f, hoist: true, // Show users with this role in their own section of the member list. }; async function updateMemberRoleForDonation(guild, member, donationAmount) { // If the user donated more than $10, give them the premium role. if (guild && member && donationAmount >= PREMIUM_CUTOFF) { // Get the role, or if it doesn't exist, create it. let role = Array.from(guild.roles.values()) .find(role => role.name === premiumRole.name); if (!role) { role = await guild.createRole(premiumRole); } // Add the role to the user, along with an explanation // for the guild log (the "audit log"). return member.addRole(role.id, 'Donated $10 or more.'); } } const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, };

これで、 pb!addpayment @Me 10.00と言ってみることができ、ボットは私にPremium Memberの役割を割り当てる必要があります。

おっと、MissingPermissionsエラーがコンソールに表示されます。

 DiscordRESTError: DiscordRESTError [50013]: Missing Permissions index.js:85 code:50013

ボットにはテストギルドでの役割の管理権限がないため、役割を作成または割り当てることはできません。 ボットに管理者権限を与えることができ、この種の問題が二度と発生することはありませんが、他のシステムと同様に、ユーザー(またはこの場合はボット)に必要な最小限の権限のみを与えるのが最善です。

サーバー設定でロールを作成し、そのロールのロールの管理権限を有効にして、ボットにロールを割り当てることで、ボットにロールの管理権限を与えることができます。

役割の管理を開始する

新しい役割を作成する

これで、コマンドを再度実行しようとすると、役割が作成されて割り当てられ、名前の色とメンバーリストの特別な位置がわかります。

新しい役割が割り当てられます

コマンドハンドラーには、無効な引数をチェックする必要があることを示唆するTODOコメントがあります。 今それを世話しましょう。

(GitHubコードリンク:https://github.com/mistval/premium_bot/blob/master/src/bot_step5.js)

 const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); const userIsInGuild = !!member; if (!userIsInGuild) { return msg.channel.createMessage('User not found in this guild.'); } const amountIsValid = amount && !Number.isNaN(amount); if (!amountIsValid) { return msg.channel.createMessage('Invalid donation amount'); } return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, };

これまでの完全なコードは次のとおりです。

(GitHubコードリンク:https://github.com/mistval/premium_bot/blob/master/src/bot_step5.js)

 const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const PREMIUM_CUTOFF = 10; const bot = new eris.Client('my_token'); const premiumRole = { name: 'Premium Member', color: 0x6aa84f, hoist: true, // Show users with this role in their own section of the member list. }; async function updateMemberRoleForDonation(guild, member, donationAmount) { // If the user donated more than $10, give them the premium role. if (guild && member && donationAmount >= PREMIUM_CUTOFF) { // Get the role, or if it doesn't exist, create it. let role = Array.from(guild.roles.values()) .find(role => role.name === premiumRole.name); if (!role) { role = await guild.createRole(premiumRole); } // Add the role to the user, along with an explanation // for the guild log (the "audit log"). return member.addRole(role.id, 'Donated $10 or more.'); } } const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); const userIsInGuild = !!member; if (!userIsInGuild) { return msg.channel.createMessage('User not found in this guild.'); } const amountIsValid = amount && !Number.isNaN(amount); if (!amountIsValid) { return msg.channel.createMessage('Invalid donation amount'); } return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, }; bot.on('messageCreate', async (msg) => { try { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts and name of the command const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the requested command, if there is one. const command = commandForName[commandName]; if (!command) { return; } // If this command is only for the bot owner, refuse // to execute it for any other user. const authorIsBotOwner = msg.author.id === BOT_OWNER_ID; if (command.botOwnerOnly && !authorIsBotOwner) { return await msg.channel.createMessage('Hey, only my owner can issue that command!'); } // Separate the command arguments from the command prefix and name. const args = parts.slice(1); // Execute the command. await command.execute(msg, args); } catch (err) { console.warn('Error handling message create event'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();

これにより、Discordボットを作成する方法の基本的なアイデアが得られるはずです。 次に、ボットをKo-fiと統合する方法を見ていきます。 必要に応じて、Ko-fiのダッシュボードにWebhookを作成し、ルーターがポート80を転送するように構成されていることを確認して、実際のライブテストWebhookを自分に送信できます。 ただし、Postmanを使用してリクエストをシミュレートします。

Ko-fiのWebhookは、次のようなペイロードを配信します。

 data: { "message_id":"3a1fac0c-f960-4506-a60e-824979a74e74", "timestamp":"2017-08-21T13:04:30.7296166Z", "type":"Donation","from_name":"John Smith", "message":"Good luck with the integration!", "amount":"3.00", "url":"https://ko-fi.com" }

webhook_listener.jsという新しいソースファイルを作成し、Expressを使用してWebhookをリッスンしましょう。 Expressルートは1つだけですが、これはデモンストレーション用であるため、慣用的なディレクトリ構造を使用することについてあまり心配する必要はありません。 すべてのWebサーバーロジックを1つのファイルにまとめます。

(GitHubコードリンク:https://github.com/mistval/premium_bot/blob/master/src/webhook_listener_step6.js)

 const express = require('express'); const app = express(); const PORT = process.env.PORT || 80; class WebhookListener { listen() { app.get('/kofi', (req, res) => { res.send('Hello'); }); app.listen(PORT); } } const listener = new WebhookListener(); listener.listen(); module.exports = listener;

次に、bot.jsを実行したときにリスナーが起動するように、bot.jsの先頭に新しいファイルを要求しましょう。

(GitHubコードリンク:https://github.com/mistval/premium_bot/blob/master/src/bot_step6.js)

 const eris = require('eris'); const webhookListener = require('./webhook_listener.js');

ボットを起動した後、ブラウザでhttp:// localhost / kofiに移動すると、「こんにちは」と表示されます。

次に、WebhookListenerにWebhookListenerからのデータを処理させ、イベントを発行させましょう。 ブラウザがルートにアクセスできることをテストしたので、Ko-fiからのWebhookがPOSTリクエストになるため、ルートをPOSTルートに変更しましょう。

(GitHubコードリンク:https://github.com/mistval/premium_bot/blob/master/src/bot_step7.js)

 const express = require('express'); const bodyParser = require('body-parser'); const EventEmitter = require('events'); const PORT = process.env.PORT || 80; const app = express(); app.use(bodyParser.json()); class WebhookListener extends EventEmitter { listen() { app.post('/kofi', (req, res) => { const data = req.body.data; const { message, timestamp } = data; const amount = parseFloat(data.amount); const senderName = data.from_name; const paymentId = data.message_id; const paymentSource = 'Ko-fi'; // The OK is just for us to see in Postman. Ko-fi doesn't care // about the response body, it just wants a 200. res.send({ status: 'OK' }); this.emit( 'donation', paymentSource, paymentId, timestamp, amount, senderName, message, ); }); app.listen(PORT); } } const listener = new WebhookListener(); listener.listen(); module.exports = listener;

Next we need to have the bot listen for the event, decide which user donated, and assign them a role. To decide which user donated, we'll try to find a user whose username is a substring of the message received from Ko-fi. Donors must be instructed to provide their username (with the discriminator) in the message than they write when they make their donation.

At the bottom of bot.js:

(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step7.js)

 function findUserInString(str) { const lowercaseStr = str.toLowerCase(); // Look for a matching username in the form of username#discriminator. const user = bot.users.find( user => lowercaseStr.indexOf(`${user.username.toLowerCase()}#${user.discriminator}`) !== -1, ); return user; } async function onDonation( paymentSource, paymentId, timestamp, amount, senderName, message, ) { try { const user = findUserInString(message); const guild = user ? bot.guilds.find(guild => guild.members.has(user.id)) : null; const guildMember = guild ? guild.members.get(user.id) : null; return await updateMemberRoleForDonation(guild, guildMember, amount); } catch (err) { console.warn('Error handling donation event.'); console.warn(err); } } webhookListener.on('donation', onDonation); bot.connect();

In the onDonation function, we see two representations of a user: as a User, and as a Member. These both represent the same person, but the Member object contains guild-specific information about the User, such as their roles in the guild and their nickname. Since we want to add a role, we need to use the Member representation of the user. Each User in Discord has one Member representation for each guild that they are in.

Now I can use Postman to test the code.

Postmanによるテスト

I receive a 200 status code, and I get the role granted to me in the server.

If the message from Ko-fi does not contain a valid username; however, nothing happens. The donor doesn't get a role, and we are not aware that we received an orphaned donation. Let's add a log for logging donations, including donations that can't be attributed to a guild member.

First we need to create a log channel in Discord and get its channel ID. The channel ID can be found using the developer tools, which can be enabled in Discord's settings. Then you can right-click any channel and click “Copy ID.”

The log channel ID should be added to the constants section of bot.js.

(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step8.js)

 const LOG_CHANNEL_;

And then we can write a logDonation function.

(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step8.js)

 function logDonation(member, donationAmount, paymentSource, paymentId, senderName, message, timestamp) { const isKnownMember = !!member; const memberName = isKnownMember ? `${member.username}#${member.discriminator}` : 'Unknown'; const embedColor = isKnownMember ? 0x00ff00 : 0xff0000; const logMessage = { embed: { title: 'Donation received', color: embedColor, timestamp: timestamp, fields: [ { name: 'Payment Source', value: paymentSource, inline: true }, { name: 'Payment ID', value: paymentId, inline: true }, { name: 'Sender', value: senderName, inline: true }, { name: 'Donor Discord name', value: memberName, inline: true }, { name: 'Donation amount', value: donationAmount.toString(), inline: true }, { name: 'Message', value: message, inline: true }, ], } } bot.createMessage(LOG_CHANNEL_ID, logMessage); }

Now we can update onDonation to call the log function:

 async function onDonation( paymentSource, paymentId, timestamp, amount, senderName, message, ) { try { const user = findUserInString(message); const guild = user ? bot.guilds.find(guild => guild.members.has(user.id)) : null; const guildMember = guild ? guild.members.get(user.id) : null; return await Promise.all([ updateMemberRoleForDonation(guild, guildMember, amount), logDonation(guildMember, amount, paymentSource, paymentId, senderName, message, timestamp), ]); } catch (err) { console.warn('Error updating donor role and logging donation'); console.warn(err); } }

Now I can invoke the webhook again, first with a valid username, and then without one, and I get two nice log messages in the log channel.

Two nice messages

Previously, we were just sending strings to Discord to display as messages. The more complex JavaScript object that we create and send to Discord in the new logDonation function is a special type of message referred to as a rich embed. An embed gives you some scaffolding for making attractive messages like those shown. Only bots can create embeds, users cannot.

Now we are being notified of donations, logging them, and rewarding our supporters. We can also add donations manually with the addpayment command in case a user forgets to specify their username when they donate. それでは、一日を呼び出してみましょう。

The completed code for this tutorial is available on GitHub here https://github.com/mistval/premium_bot

次のステップ

We've successfully created a bot that can help us track donations. Is this something we can actually use? Well, perhaps. It covers the basics, but not much more. Here are some shortcomings you might want to think about first:

  1. If a user leaves our guild (or if they weren't even in our guild in the first place), they will lose their Premium Member role, and if they rejoin, they won't get it back. We should store payments by user ID in a database, so if a premium member rejoins, we can give them their role back and maybe send them a nice welcome-back message if we were so inclined.
  2. Paying in installments won't work. If a user sends $5 and then later sends another $5, they won't get a premium role. Similar to the above issue, storing payments in a database and issuing the Premium Member role when the total payments from a user reaches $10 would help here.
  3. It's possible to receive the same webhook more than once, and this bot will record the payment multiple times. If Ko-fi doesn't receive or doesn't properly acknowledge a code 200 response from the webhook listener, it will try to send the webhook again later. Keeping track of payments in a database and ignoring webhooks with the same ID as previously received ones would help here.
  4. Our webhook listener isn't very secure. Anyone could forge a webhook and get a Premium Member role for free. Ko-fi doesn't seem to sign webhooks, so you'll have to rely on either no one knowing your webhook address (bad), or IP whitelisting (a bit better).
  5. The bot is designed to be used in one guild only.

Interview: When a Bot Gets Big

There are over a dozen websites for listing Discord bots and making them available to the public at large, including DiscordBots.org and Discord.Bots.gg. Although Discord bots are mostly the foray of small-time hobbyists, some bots experience tremendous popularity and maintaining them evolves into a complex and demanding job.

By guild-count, Rythm is currently the most widespread bot on Discord. Rythm is a music bot whose specialty is connecting to voice channels in Discord and playing music requested by users. Rythm is currently present in over 2,850,000 guilds containing a sum population of around 90 million users, and at its peak plays audio for around 100,000 simultaneous users in 20,000 separate guilds. Rythm's creator and main developer, ImBursting, kindly agreed to answer a few questions about what it's like to develop and maintain a large-scale bot like Rythm.

Interviewer: Can you tell us a bit about Rythm's high level architecture and how it's hosted?

ImBursting: Rythm is scaled across 9 physical servers, each have 32 cores, 96GB of RAM and a 10gbps connection. These servers are collocated at a data center with help from a small hosting company, GalaxyGate.

I imagine that when you started working on Rythm, you didn't design it to scale anywhere near as much as it has. Can you tell us about about how Rythm started, and its technical evolution over time?

Rythm's first evolution was written in Python, which isn't a very performant language, so around the time we hit 10,000 servers (after many scaling attempts) I realised this was the biggest roadblock and so I began recoding the bot to Java, the reason being Java's audio libraries were a lot more optimised and it was generally a better suited language for such a huge application. After re-coding, performance improved tenfold and kept the issues at bay for a while. And then we hit the 300,000 servers milestone when issues started surfacing again, at which point I realised that more scaling was required since one JVM just wasn't able to handle all that. So we slowly started implementing improvements and major changes like tuning the garbage collector and splitting voice connections onto separate microservices using an open source server called Lavalink. This improved performance quite a bit but the final round of infrastructure was when we split this into 9 seperate clusters to run on 9 physical servers, and made custom gateway and stats microservices to make sure everything ran smoothly like it would on one machine.

I noticed that Rythm has a canary version and you get some help from other developers and staff. I imagine you and your team must put a lot of effort into making sure things are done right. Can you tell us about what processes are involved in updating Rythm?

Rythm canary is the alpha bot we use to test freshly made features and performance improvements before usually deploying them to Rythm 2 to test on a wider scale and then production Rythm. The biggest issue we encounter is really long reboot times due to Discord rate limits, and is the reason I try my best to make sure an update is ready before deciding to push it.

I do get a lot of help from volunteer developers and people who genuinely want to help the community, I want to make sure everything is done correctly and that people will always get their questions answered and get the best support possible which means im constantly on the lookout for new opportunities.

まとめ

Discord's days of being a new kid on the block are past, and it is now one of the largest real-time communication platforms in the world. While Discord bots are largely the foray of small-time hobbyists, we may well see commercial opportunities increase as the population of the service continues to increase. Some companies, like the aforementioned Patreon, have already waded in.

In this article, we saw a high-level overview of Discord's user interface, a high-level overview of its APIs, a complete lesson in Discord bot programming, and we got to hear about what it's like to operate a bot at enterprise scale. I hope you come away interested in the technology and feeling like you understand the fundamentals of how it works.

Chatbots are generally fun, except when their responses to your intricate queries have the intellectual the depth of a cup of water. To ensure a great UX for your users see The Chat Crash - When a Chatbot Fails by the Toptal Design Blog for 5 design problems to avoid.

関連: JSのベストプラクティス:TypeScriptと依存性注入を使用してDiscordボットを構築する