概要
Discordで参加or非参加連絡を募って自動的にスプレッドシートに記入したいと思ったので作ってみた。
事前知識
GAS
Google Apps Script。javascriptライクで実装可能。
Googleスプレッドシートなどへの書き込みができる。
Glitch
Node.jsで簡単にアプリ開発できるWebサービス。
登録から3分でサービスがリリースできる。簡単すぎて当初笑った。
Discord.js
discordへの応答が簡単にできるライブラリ。
詳細はここ見れば大抵解決する。
設計について
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に突っ込んだ状態
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);
      }
    }
  });
}
SECRET=
MADE_WITH=
DISCORD_BOT_TOKEN=/* botのトークンを設定 */
GAS_URI=/* GASヘpostするURLを背一定 */
次にGAS
/*この関数をスケジューラーで回して接続を維持する。*/
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);
}
// 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;
}
// スプレッドシート
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を設定*/;
詳細説明
特に思いつかなかったので要望があったら追加検討します。
