自動応答のDiscord Botを完全無料で構築する[gas][glitch]

#概要
Discordで参加or非参加連絡を募って自動的にスプレッドシートに記入したいと思ったので作ってみた。

#事前知識
**GAS**
Google Apps Script。javascriptライクで実装可能。
Googleスプレッドシートなどへの書き込みができる。

**[Glitch](https://glitch.com/)**
Node.jsで簡単にアプリ開発できるWebサービス。
登録から3分でサービスがリリースできる。簡単すぎて当初笑った。

**[Discord.js](https://discord.js.org/#/)**
discordへの応答が簡単にできるライブラリ。
詳細は[ここ](https://discord.js.org/#/docs/main/stable/general/welcome)見れば大抵解決する。

#設計について
ggって見た感じ、bot作成に関してはPCを常時稼働前提or鯖に突っ込む前提の記事しかなかった。
僕の要件としては、
・PC常時稼働は論外
・借りてるVPSに突っ込むのは環境設定周りで面倒
・追加でサーバー借りるつもりもない
じゃぁ金かけず実装して記事書こう！ってことで実装開始。

①discordでチャットに自動応答するbotを作ろうとするとサーバを建てる必要があった。
　　-> Glitchを利用して解決

②Glitchが5分程度で落ちる。
　　-> GASのトリガー機能を実行して毎分postして接続を維持する。(公式で提示されてる手法なので問題なし)

という流れでこの構成に落ち着いた。もともとGASは使う予定だったので最小構成にできたと思う。

###[応答機序]
**Discord**・・・ユーザの書き込み
　-> **Glitch**・・・チャンネルへの書き込みを取得、特定の形式だった場合にそのままGASにpostする。
　　-> **GAS**・・・glitchからのpostを取得して解析、実行結果に応じてGlitchにレスポンス投げる。
　　　-> **スプレッドシート書き込み**
　　　-> **Glitch**・・・GASからのpostを取得、discordにsendする。
　　　　-> **discord**・・・botからの書き込み

###[接続維持]
**GASトリガー**・・・毎分GAS関数を実行。1分の次は5分でしか設定できないので妥協。
　-> **GAS**・・・Glitchへ空のpostを投げる。
　　-> **Glitch**・・・接続があったので自動スリープ時間延長

#サンプル
まぁまずは見てください

Glitchの処理。
全部main.jsに突っ込んだ状態

```js:main.js

const DC = this;

// msgの一時退避
var msg;

var discord = require('discord.js');
var client = null;

// Response for Uptime Robot
const http = require('http');
http.createServer(function(request, response)
{
  connectDiscord();
	response.end('Discord bot is active now.');
}).listen(3000);

function connectDiscord(){
  console.log('00');
  
  // 状態によって処理変更する。
  if (client == null){
    client = new discord.Client();
  } else {
    if (client.readyAt != null){
      switch(client.status){
        case 0: // READY
        case 1: // CONNECTING
        case 2: // RECONNECTING
        case 3: // IDLE
        case 4: // NEARLY
          return;
        case 5: // DISCONNECTED
          client.destroy();
          break;
        default:
          client = new discord.Client();
          break;
      }
    }
  }
  
  client.login( process.env.DISCORD_BOT_TOKEN );
  client.on('ready', message =>
  {
    console.log('bot is ready!');
  });

  client.on('message', message =>
  {
    // 自分のメッセージには反応しない
    if (message.author.id == /* botのCLIENT_IDを入力 */) {
      return;
    }
    msg = message;

    // botへのリプライ応対
    if(msg.isMemberMentioned(client.user))
    {
      if (msg.content.indexOf('content') > 0){
        msg.reply(msg.content);
        console.log(msg.content);
        return;
      }
      if (msg.content.indexOf('logout') > 0){
        msg.reply('ログアウトします。');
        client.destroy();
        return;
      }
      if (msg.content.indexOf('status') > 0){
        msg.reply(client.status);
        return;
      }
      if (msg.content.indexOf('help') > 0){
        msg.reply('ヘルプメッセージ');
        return;
      }
      sendGAS(msg);
      return;
    }

    // botへのリプライじゃなかった場合
    else {
      // []でくくったコマンドに応対する。
      if(msg.content.match(/^\[(?=.*\])/i)){
        sendGAS(msg);
        return;
      }
    }
  });
  
  if(process.env.DISCORD_BOT_TOKEN == undefined)
  {
    console.log('please set ENV: DISCORD_BOT_TOKEN');
    process.exit(0);
  }
}

// send
var sendGAS = function(msg){
  var params = msg.content.split(' ');
  var userId = params[0];
  var value = null;
  for (var n=1; n < params.length; n++){
    if (n == 1)
      value = params[n];
    else
      value = value + ' ' + params[n];
  }

  var jsonData = {
    'userId':msg.member.id,
    'value':value,
    'message':msg.content,
    'channelId':msg.channel.id,
  }

  post(process.env.GAS_URI, jsonData)
}

// post
var post = function(uri, jsonData){
  var request = require('request');
  var options = {
    uri: uri,
    headers: {"Content-type": "application/json"},
    json: jsonData,
    followAllRedirects: true,
  }

  request.post(options, function(error, response, body){
    if (error != null){
      msg.reply('更新に失敗しました');
      return;
    }

    var userid = response.body.userid;
    var channelid = response.body.channelid;
    var message = response.body.message;
    if(userid != undefined && channelid != undefined && message != undefined){
      var channel = client.channels.get(channelid);
      if (channel != null){
        channel.send(message);
      }
    }
  });
}
```

```config.env
SECRET=
MADE_WITH=
DISCORD_BOT_TOKEN=/* botのトークンを設定 */
GAS_URI=/* GASヘpostするURLを背一定 */
```


次にGAS

```js:RetainGlitch.gs
/*この関数をスケジューラーで回して接続を維持する。*/
function doRetain() {
  var json = {
    'userid':'',
    'channelid':'',
    'message':'',
  };
  sendGlitch(GLITCH_URL, json);
}

function sendGlitch(uri, json) {
  var params = {
    'contentType' : 'application/json; charset=utf-8',
    'method' : 'post',
    'payload' : json,
    'headers' : json,
    'muteHttpExceptions': true
  };
  
  response = UrlFetchApp.fetch(uri, params);
}
```


```js:receiver.gs
// discordからのvalueを受け取ってリプライ用メッセージを返す
function receiveDiscordValue(message, userId) {
  var resMsg = null;
  
  var values = message.split(' ');

  // []でくくったコマンドを判定
  if(values[0].match(/^\[(?=.*\])/i)){
    var fastValue = values[0].split(']')[1];
    var order = values[0].split(']')[0].substring(1);

    /* ここらへんにGASに書き込んだりコマンドを実行したりする処理を追加*/

    resMsg = 'glitchに返すメッセージを設定';
  }
  // 命令判定(glitchで返すもの以外)
  else if(values[0].match('<@'+ BOT_CLIENT +'>')){
    var fastValue = value[0].split('>')[1];

    if (fastValue == 'help'){
      resMsg = 'ヘルプメッセージを設定';
    }
  }
  
  if (resMsg == null)
    resMsg = '不正な処理です。'
  
  return resMsg;
}
```

```js:env.gs
// スプレッドシート
var SS = SpreadsheetApp.openById(/*スプレッドシートのIDを設定*/);

// BOT_TOKEN
var BOT_TOKEN = /* botのトークンを設定 */;
// BOT_CLIENT_ID
var BOT_CLIENT = /* botのCLIENT_IDを設定 */;

// GlitchのURL
var GLITCH_URL = /*postするglitchのURLを設定*/;
```

#詳細説明
特に思いつかなかったので要望があったら追加検討します。