Delphi で Discord Bot のフレームワーク作ったった
結論を急ぎたい人は↓へ
Discord ボットへの道
ただ、Discord ボットが作りたかっただけなのに WebSocket を作る羽目になった僕たちの明日はどっちだ!
こっちだ!!
Discord API
まずは、この図を見てください。
Discord API はいくつかに分かれているのですが、基本的には以下の二つを使います。
- Gateway API
- HTTP API
Gateway API
WebSocket を使って Discord がデータを送ってくる API です。
WebSocket で接続されると、まず Discord から Hello Payload が送られてきます。
Payload
Discord との通信には Payload と呼ばれる JSON が用いられます。
例えば↓は HeartBeat に対する応答です。
{"t":null,"s":null,"op":11,"d":null}
Payload には以下の要素があります。
d
Objects
Sequence Idt
Event Type
ただし、s, t に関しては op が 0 (Dispatch) だった時のみ値が存在します。
基本的に Discord Gateway API の文書が判りづらいのは、Payload JSON が省略されて d タグ内の事象について書いてあるからです。
たとえば、Hello Payload の場合
{"t":null,"s":null,"op":10,"d":{"heartbeat_interval":41250,"_trace":["discord-gateway-prd-xxx"]}}
という値が返ってきますが、Discord の方では
{
"heartbeat_interval": 45000,
"_trace": ["discord-gateway-prd-1-99"]
}
としか書いていないため d の内容だとは判りづらいのです。
これに気づくまでにもかなり掛かりました。
Gateway API で重要な Operation
押えておくべき Op についてです。
名前(OpCode)
として記載します。
まずは受信部です。
受信部
Hello (10)
WebSocket 接続後、最初に送られてくる応答です。
これを受信したら Identify を送り、Heartbeat を開始します。Heartbeat ACK (11)
Heartbeat を受信したことを教えてくれます。
これが返ってこない場合は Heartbeat を送り直す必要がありますが、返ってこない時は、基本的に Discord サーバが落ちている or 切断されたときなので、今回は処理を省略しました。Dispatch (0)
これが一番重要なコードです、というか、ここが本作業するところです。
t タグには文字列で例えば
READY
TYPING_START
USER_UPDATE
などが入っています。
これがイベントの名前となるので、このイベント毎に必要な処理をします。
ここで重要なのは、以下の2つです。
READY
準備完了を示します。
d には session_id と user.username で自身の名前が入っています。
必要であれば保存しておきます。MESSAGE_CREATE
Discord で誰かがメッセージを送信したことを示します。
d タグには mention や user名、メッセージの内容等が入っているので、それらを取得して何らかの処理をします。
READY で取得した UserName が Mention の名前と一緒であれば自分(bot)に対しての発言だと判ります。
送信部
Heartbeat(1)
Hello で受け取ったheartbeat_interval
に指定されたミリ秒ごとに Heartbeat を送信します。Identify(2)
自分の情報を送ります。
例えば、Bot Token 等です。
今回作ったライブラリでは、こんな感じで設定しています。
D.token := FToken;
D.properties.os := GetOSName;
D.properties.browser := 'pk_net_discord_bot';
D.properties.device := 'delphi';
D.compress := False;
D.large_threshold := 250;
SetLength(D.shard, 2);
D.shard[0] := 0;
D.shard[1] := 1;
D.presence.status := 'online';
D.presence.afk := False;
特に Token は、この後 HTTP API の方でも使うので重要です。
Token はボットを登録したときに Discord から発行される文字列です。
Discord でボットを登録する処理は色々な記事があるので、例えば DiscordのBot登録・設定・トークンの発行方法 などの記事をご覧ください。
HTTP API
HTTP API を通じて、Discord にチャットメッセージを送ります。
今回作ったライブラリはあえてシンプルにした物なので、HTTP API では、発言するものしか使っていません。
HTTP API は機能によって送信先が変わります。
例えば、メッセージを送信する URL は以下のようになります。
https://discordapp.com/api/v6/channels/%s/messages
%s
には、チャンネルの ID が入ります。
ヘッダ
HTTP API で、重要なのは HTTP ヘッダに指定する
- UserAgent
- Authorization
です。
UserAgent
UserAgent は何でも良いのですが必ず有効な UserAgent が指定されていなければなりません。
ライブラリでは下記の文字列を指定しています。
%d にはバージョンが入ります。
UserAgent=DiscordBot (PK.Net.Discord.Bot, %d)
Authorization
Authorization には Token を指定します。
Authorization=Bot TOKEN
とします(TOKEN
の部分は Bot の Token に置き換えます)。
これはこのライブラリ特有の書き方では無く、必ずこのように指定しなければいけません。
Payload
次に Payload JSON を作成します。
d タグの中身は送信するメッセージによって変わります。
例えば、テキストを送信する場合の d タグは
{"content":"メッセージ内容", "tts":false, "embed":null}
となります。
このライブラリでは更に画像といった APPLICATION_OCTET_STREAM も送れるようになっています。
Payload JSON やストリームデータは、最終的に Multipart FormData として Post します。
これらを処理しているコードが↓です。
procedure TDicordTalkBot.SendRaw(
const iCMDURL, iPayloadJsonStr: String;
const iStream: TStream;
const iStreamName: String);
begin
FHttpClient.UserAgent := Format(HEADER_USER_AGENT_VALUE, [SELF_VERSION]);
var Header: TNetHeaders;
SetLength(Header, 1);
Header[0].Name := HEADER_AUTHORIZATION;
Header[0].Value := Format(HEADER_AUTHORIZATION_VALUE, [FToken]);
var FormData := TMultipartFormData.Create;
try
FormData.AddField('payload_json', iPayloadJsonStr);
if iStream <> nil then
begin
FormData.AddStream(
'file',
iStream,
iStreamName,
ContentTypeToString(TRESTContentType.ctAPPLICATION_OCTET_STREAM));
end;
var Path := iCMDURL.Trim;
if Path.StartsWith('/') then
Path := Path.Substring(1);
var Res := FHttpClient.Post(DISCORD_HTTP_URL + Path, FormData, nil, Header);
if Res.StatusCode > 399 then
CallOnError(ERR_CODE_SEND_FAILED, Res.ContentAsString(TEncoding.UTF8));
finally
FormData.DisposeOf;
end;
end;
上手くいくと思った?ざんねーん!さやかちゃんでした!
本来なら、
こんな感じで処理が完了するのですが…
一番最初の図にわざわざ Claud Flareと書いた通り、Claud Flare が勝手に接続を切ってきます。
そのため、適宜繋ぎ直してやるのですが、その際に Resume(6) という Op コードを送って再接続処理を申請します。本来は。
ただ、これが必ず上手く行くわけでは無い事が経験的に判ったので、このライブラリでは、再接続処理をせずに、最初から接続をやり直しています。
ライブラリの使い方
基本的にコマンドラインアプリで使われることを想定しています。
そのため TApplication に代わる TBotApplication が定義されています。
TBotAppilcation を用いると Discord Bot が簡単に作れます。
具体的には以下のような感じです。
program HelloBot;
{$APPTYPE CONSOLE}
uses
PK.Net.Discord.Bot;
var
FBot: TBotApplication;
begin
FBot := TBotApplication.Create(TOKEN);
try
// エラー発生で呼ばれます
// 本当はここで何か処理をすべきです。
FBot.ErrorHandler :=
procedure (const iErrCode: Integer; const iMessage: String)
begin
Writeln(Format('ERROR (%d): %s', [iErrCode, iMessage]));
end;
// Discord と接続したときに呼ばれます
FBot.ReadyHandler :=
procedure
begin
Writeln('Connected.');
end;
// 自分宛にメッセージが来たとき呼ばれます。
FBot.MentionHandler :=
procedure (
const iChannelID, iFromUserID, iFromUserName, iMessage: String)
begin
// iChannelID: メッセージを受け取ったチャンネルです
// iFromUserID: メッセージを送ってきたユーザーの ID です
// iFromUserName: メッセージを送ってきたユーザーの名前です
// iMessage: メッセージ内容です
Writeln(
Format(
'#%s: %s(%s) say "%s"',
[iChannelID, iFromUserName, iFromUserID, iMessage])
);
if iMessage = 'bye' then
FBot.Stop; // これで Run ループを抜けます
// メッセージの送信はこれだけで、非常に簡単です。
FBot.Send(iChannelID, 'Hello, Discord!');
end;
// Run ループです。
FBot.Run;
finally
FBot.DisposeOf;
end;
end.
このほかにも
TBotApplication.DispatchHandler
を使えば Dispatch で送られた全てのイベントを処理できます。
TBotApplication.MessageHandler
を使えば自分宛以外のメッセージも処理できます。
まとめ
車輪を2つ作るのは流石に大変だったよ。