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
Object -
s
Sequence Id -
t
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つ作るのは流石に大変だったよ。