JavaScript
Node.js
gas
glitch
discord

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


概要

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に突っ込んだ状態


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


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);
}



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;
}



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を設定*/;



詳細説明

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