Node.js
AzureWebApps
discord
AzureFunctions

DiscordBOTをケチケチ仕様で動かす

この記事の内容は初期構想で微妙な感じなので、作り直したこちらを参照ください。

<発端>

 インフラ勉強会のDiscordでLTの話題が出てた
 LTスタート時に発言、終わり間際のドラ+終わりのゴングが鳴ったら面白いかなと
 単純にWebAPI・サーバレスサービスを触るきっかけが欲しかっただけ

<ルール>

  • 他にやりたい事があるので時間は余りかけない
  • お金がないのでコストもかけない
  • 取り組んだ内容は纏めて公開する

<実現イメージ>

欲しい機能

  1. 特定のキーワードを発言するとタイマー始動
  2. 一定時間経過後に発言したチャンネルに通知が出る

最初SlackBOTと同じ要領で作るつもりで居たので、1の部分は「Webhook outgoingでよかろ〜」なんて甘い見通しで以下の様なイメージをしていた。

 Discord           |   Azure Function             
 ①”LT開始”みたいな発言
 ↓
 ②Botが受けてRESTAPIへPOST ➤ ③リクエストを受けてドラとゴングの時間を算出
                    ↓
                   ④一定時間Wait
                    ↓
 ⑥チャンネルへ通知       ← ⑤DiscordのWebhookへPOST 

②の所が自分で用意せにゃならん!と気づいて悩んだ結果、最終的に以下の様になった。
(字が汚くて申し訳ない)
構成.jpg

実現先 コスト 手間 実現難易度・前例
ローカルPC 電気代と余剰端末次第 端末管理面倒 普通に出来そう、前例多数
AWS EC2/Azure VM パフォーマンス次第 VM操作面倒 ローカルPCと同じ
GCP ConputingEng 無償枠ある Googleアカウントの問題があった(※1) 上に同じ
Azure AppService パフォーマンス次第 実現出来れば管理は楽 やってる奴いなさそう、苦難の道

※1:諸々の事情でGoogleアカウントにクレカ登録したくなかった

ローカルPCやVMでやれば出来るのはわかっていたものの、「触るのめんどくさい」という理由だけでAppServiceで出来る道を探ってみた。
Herokuでやってる人がいたので、おそらく大丈夫じゃ無いか?と思っていた点もある。

(ローカルPC・VMで実現する選択の方が無難なので、話のタネ位に読まれる事をオススメする)

<実装>

1. DiscordBOT/Incoming Webhook User
DiscordApp

  • DiscordAppでNewApp追加
  • AppName:なんでもOK
  • REDIRECT URI(S):3の実装先URIが入る。
  • App Bot Userの所のTokenはあとで使うので控えておく。
  • 作成したらOAUTH2 URL GENERATORボタンからURLを生成し、アクセスする。
  • BOTを対象サーバに招待しておく。

Webhook

  • Discordの参加済サーバのサーバ設定からWebhooksを開いてWebhookを追加する。
  • Webhook URLはあとで使うので控えておく。

2. タイマー&お知らせ機能(Azure Functions)

  • Azure PortalでFunction Appを作成する。今回の実装ではWindowsベースでの動作確認を実施。
  • クイックスタートにて、Webhook+API/JavaScriptにて関数を作成する。
  • index.jsに別WebAPIへPOSTする機能を追加する。
  • 関数URLの取得をクリックして表示されたURLを控えておく。
index.js
module.exports = function (context, req) {
    context.log('JavaScript HTTP trigger function processed a request.');
    //req             :POSTで貰うデータ
    //req.body.comment:BOTに喋らせる内容
    //req.body.return :DiscordのWebhookURL
    //req.body.timer  :Functionsが待つ時間
    if (req.body && req.body.comment && req.body.return && req.body.timer) {

        var request = require('request');
        var options = {
            uri: req.body.return,
            headers: {
                "Content-type": "application/json",
            },
            json: {
                "content": req.body.comment
            }
        };
        setTimeout(function(){
            request.post(options, function(error, response, body){})
        },req.body.timer)
    }
    else {
        context.res = {
            status: 400,
            body: "Please pass a name on the query string or in the request body"
        };
    }
    context.done();
};

  

3. Outgoing Webhookロジック(Azure WebApps)

  • WebAppsを作成する。
  • DocumentRoot(site¥wwwroot)にNode.js用サンプルPJファイルを展開する。
  • KuduのDebug Console(CMD)で必要モジュールをセットアップする
npm install discord.js
npm install request
npm install eris
  • index.jsを作成する。こちらのサイトで公開されていた内容をほぼ流用させて頂いた。
index.js
var http = require('http');

var server = http.createServer(function(request, response) {

    response.writeHead(200, {"Content-Type": "text/plain"});
    response.end("Discord Bot Activated.");

});

var port = process.env.PORT || 1337;
server.listen(port);

const Eris = require("eris");
var bot = new Eris("DiscordAppで作成したBotのToken");
bot.on("ready", () => {
    console.log("Ready!");
});

bot.on("messageCreate", (msg) => {
    if(msg.content === "LT始めます") {
        bot.createMessage(msg.channel.id, "LTタイマースタートします。5分後にお知らせするよ!");
        var request = require('request');
        var options = {
            uri: "Azure Functionsの関数URL",
            headers: {
                "Content-type": "application/json",
            },
            json: {
                "return" : "Discordサーバ設定で作成したWebhookのURL",
                "comment": "後5分だよ〜",
                "timer"  : "300000"
            }
        };
        request.post(options, function(error, response, body){});    
    } else if(msg.content === "死んだ?"){
        bot.createMessage(msg.channel.id, "生きてるよ!(@益@#)");
    } else if(msg.content === "後5分だよ〜"){
        bot.createMessage(msg.channel.id, "後5分ですと!?");
        var request = require('request');
        var options = {
            uri: "Azure Functionsの関数URL",
            headers: {
                "Content-type": "application/json",
            },
            json: {
                "return" : "Discordサーバ設定で作成したWebhookのURL",
                "comment": "時間切れ!終わり!お疲れ様!(@益@#)",
                "timer"  : "300000"
            }
        };
        request.post(options, function(error, response, body){});    
    } 
});

// Discord に接続します。
bot.connect();
  • web.configをDocRootに配置
web.config
<configuration>
  <system.webServer>
    <handlers>
      <add name="iisnode" path="index.js" verb="*" modules="iisnode" />
    </handlers>
    <rewrite>
      <rules>
        <rule name="StaticContent">
          <action type="Rewrite" url="public{REQUEST_URI}" />
        </rule>
        <rule name="DynamicContent">
          <conditions>
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True" />
          </conditions>
          <action type="Rewrite" url="index.js" />
        </rule>
      </rules>
    </rewrite>
    <security>
      <requestFiltering>
        <hiddenSegments>
          <add segment="node_modules" />
        </hiddenSegments>
      </requestFiltering>
    </security>    
  </system.webServer>
</configuration>
  • ここまで出来たら、AppServiceを再起動。
  • ブラウザでもコマンドベースでも良いのでAppServiceのURLへアクセスするとDiscord上でBOTがオンラインになるはず。

4.動作チェック

  • Discord上で「死んだ?」で動作確認する。Discord<->WebAppsが繋がっていれば応答がある。
  • OKであれば「LT始めます」でFunctionsを経由してDiscordに戻ってくるか確認する。
  • 戻ってくるまでの時間が長ければ適宜timerの時間を調整する。

5.継続して稼働させるために

4までの対応だとWebAppsを再起動させるとBOTが死ぬので、再度HTTPリクエストしてbot.jsを動かす必要がある。ブラウザで開くのもかっこ悪いので、WebAppsのWebジョブを実装する。
WebAppsのURLにアクセスすればBotがアクティベートされて利用できる様になるが、AppServiceがFreeで動かしているため、アプリが一定時間で落ちる。(約20分)
また、クォータが低めの設定なので、これを超える量は使えない。
対策としてBasic以上のプランを使えば常時接続オプションがONになるため、理屈的には回避できるがコスト面を考えるとちょっと辛い。

また、Webジョブも同様の理由で起動されないリスクがあるため、上記を回避する施策としてAutomationでWebAppsをつついて生存させることにする。
Automationは月500分までは無償なので、コストは発生しない見込み。

  • Automationを作成し、空のRunbookを追加する。
  • Powershellで以下コードを追加。
urlknock.ps1
$ProgressPreference="SilentlyContinue"
$request = Invoke-WebRequest -Uri "https://WebAppsのURL/" -UseBasicParsing
  • 20分で落とされる事を考慮し、開始時間が15分ずれたスケジュールを4つ作成してリンクさせる。

6.懸念

AutomationからWebアクセスが走った際にDiscord側でBOTトリガーが動くとうまく動かないかも。
その辺まで考慮する必要があるのであれば、諦めてVM立てるかなという感じ。
また、クローラー他何らかの訪問者がWebAppsに来ても同様の事が考えられるので、必要であればWebAppsの認証機能を使ってしまうのもいいかも。

ぶっちゃけGlitch使ってやってしまうのでもよかったなぁ。

ま、無償枠で無理矢理やってみた事で気づけた仕様なんかもあったのでよしとする。

汚いファイルのリポジトリ>

https://github.com/obird-rive/DiscordBOT_nonVM

<本件にあたり参考にしたURL>