GASとGlitchでDiscord botを作る(無料)

初Qiita記事
# 概要

### 実現したいこと

botに送られたメッセージをGASでいろいろ処理して返信できるbotを作りたい。無料で。
とりあえず今回はGAS経由でオウム返しをするbotを作る。

### 利用するサービス


+ [Google Apps Script (GAS) ](https://script.google.com/home)
+ [Glitch](https://glitch.com/)
+ [Discord]()

すべて**無料**で利用可能。すばらしい！

### 処理の流れ

無料版Glitch はアクセスがないと5分で落ちます。24時間稼働のために、GASの時限トリガーで5分ごとにアクセスし続けます。Discord.jsはバージョン13を使います。

```mermaid
sequenceDiagram
  actor U as User
  participant GL as Glitch
  participant GAS as Google Apps Script
  U->>+GL: message
  GL->>+GAS: message
  GAS->>+GL: reply
```
多分こんな感じ。

# 実践

### botの作成

自分のDiscordサーバーにbot用のチャンネルを用意します。[Discord Portal](https://discord.com/developers/applications) からbotを作成し、TokenとclientIdを控えます。botを自分のDiscordサーバーに追加します。scopeはbotとapplication.commandsを選べばOK.

## GASのコード
GAS自体の説明は省略。新しくプロジェクトを作成し、`code.gs`を書きます。

```code.js
//GASのプロパティを使いやすいように
//プロパティを使うことで機密情報のべた書きを避けられます
const P = {
  prop: PropertiesService.getScriptProperties(),
  get(key) {
    return this.prop.getProperty(key);
  }
  set(key, value) {
    this.prop.setProperty(key, value);
  }
}


//5分おきにこの関数を実行するように時限トリガーを設定
const retainGlitch = () => {
  //あらかじめ設定しておいたプロパティを呼び出す
  const glitchURL = P.get('glitchURL')

  const data = {}
  const headers = { 'Content-Type': 'application/json; charset=UTF-8' }
  const params = {
    method: 'post',
    payload: JSON.stringify(data),
    headers: headers,
    muteHttpExceptions: true
  }
  //特に中身のないデータをGlitchへPOST
  response = UrlFetchApp.fetch(glitchURL, params);
  console.log(response);
}


//Glitchからのメッセージが入ったPOSTを受け取る
const doPost = e => {
  const message = JSON.parse(e.postData.contents);

  //返信データを作成
  const messageObj = {
    'messageType': 'send',
    'content': 'via GAS: ' + message.content
  }
  const payload = JSON.stringify(messageObj);

  //json形式データを返す
  const output = ContentService.createTextOutput().setMimeType(ContentService.MimeType.JSON);
  return output.setContent(payload);
};

```

これをエディタから、だれでもアクセスできるWEBアプリとして公開し、URLを控えます。

## Glitchのコード

Glitchのアカウントを作成します。New Projectから`glitch-hello-node`のapp starterを選びます。`package.json`内の`"engines: { "node": "12.x" }`を`"engines: { "node": "16.x" }`に書き換えます。画面下のTERMINALを開き、`$ npm i discord.js`と`$ npm i axios`を実行します。エラーが出てうまくいかなければ、`$ rm package-lock.json`、`$ npm init`などを実行してみてください。

```.env
# このファイル内に機密情報を書く
# = の前後には空白を開けない
# ''や""で値を囲まない

DISCORD_BOT_TOKEN={botのToken}
GAS_URL={GASのURL}
```
```main.js
//モジュールのインポート
const http = require("http"); 
const axios = require('axios');
const discord = require("discord.js");
let client;


const connectDiscord = () => {
  client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MESSAGE_REACTIONS] });
  client.login(process.env.DISCORD_BOT_TOKEN);
  client.on("ready", () => console.log("bot is ready!"));

  //送信者がbot自身でないとき、GASへPOSTする関数を実行
  client.on("messageCreate", (message) => !message.author.bot ? sendGAS(message) : "");

  if (process.env.DISCORD_BOT_TOKEN == undefined) {
    console.log("please set ENV: DISCORD_BOT_TOKEN");
    process.exit(0);
  }
};


// GASにデータをPOSTする関数
const sendGAS = (message) => {
  const jsonData = {
    author: message.author,
    content: message.content,
    channel: message.channel,
  };
  const post = async () => {
    try {
      await axios({
        method: "post",
        url: process.env.GAS_URL,
        data: jsonData,
        responseType: "json",
      }).then((response) => {
        const msg = response.data;
        
        //送信方法を振り分け
        switch (msg.messagetype) {
          case "nothing": //何もしない
            break;

          case "reply": //返信
            message.reply(msg.content);
            break;

          case "send": //ただ送る
          default:
            message.channel.send(msg.content);
            break;
        }
      });
    } catch (error) {
      message.reply("an error occurred: " + error);
    }
  };
  post();
};

//実行
connectDiscord();

//GASからのPOSTリクエストを受け取る用
http
  .createServer( (request, response) => {
    console.log('post from gas')
    response.end("Discord bot is active now.");
  })
  .listen(3000);

```
`package.json`のAdd Packageからaxiosを検索して追加します。WEBエディタ上のShareボタンを押すと表示されるLive siteのリンクを、GASの`glitchURL`に設定します。

一旦以下の関数を書いて実行すれば設定完了です。
```code.js
const setURL = () => P.set('glitchURL', 'your_project_url');
```

# 結果と振り返り

想定通りに動きました。
![スクリーンショット (47).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637844/448ac3ea-a15a-f69e-45e2-032606c3050c.png)


+ ローカル環境を使わないのは非常にお手軽。

+ GASだけで作れるLINE botのほうが楽だった。

+ requestやrequest-promiseモジュールが非推奨とのことで、[axios](https://github.com/axios/axios)というのを使ってみた。

+ GASをAPIのようにつかうのは初めてだった。`ContentService`か`HtmlService`でないと値を返せないみたい。


# 参考にしたサイト
+ [Discord Botの作り方、まとめ（Node）- Qiita](https://qiita.com/kiririri/items/845da86f95f0e3472953)
+ [Glitchを使ってNode.js製Discord Botを動かしてみる - Qiita](https://qiita.com/allegrogiken/items/f437c5e89aeabc587a96)
+ [自動応答のDiscord Botを完全無料で構築する[gas][glitch] - Qiita](https://qiita.com/u__/items/23935e4a49035d313e5e)

# 追記

* Discord.js v13用に書き替えました。
* console.log()とかでやたらめったらアロー関数を使ってるのはなんでだっけ...? (←コメントを残さないバカ)
* ~~axiosではなくで標準のhttpsモジュールを使うほうがいいかも~~
* 逆に受け取り側はhttpモジュールではなくexpressを使ったほうがいいかな
* セミコロンの有無統一しようぜ