13
8

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 5 years have passed since last update.

Delphi で Discord Bot を作る

Last updated at Posted at 2019-05-08

Delphi で Discord Bot のフレームワーク作ったった

結論を急ぎたい人は↓へ

Discord ボットへの道

ただ、Discord ボットが作りたかっただけなのに WebSocket を作る羽目になった僕たちの明日はどっちだ!
こっちだ!!

Discord API

まずは、この図を見てください。

disco.png

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 には以下の要素があります。

ただし、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;

上手くいくと思った?ざんねーん!さやかちゃんでした!

本来なら、

image.png

こんな感じで処理が完了するのですが…
一番最初の図にわざわざ 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つ作るのは流石に大変だったよ。

13
8
3

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
13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?