Slack Botで自動化生活: その2 GAS

これは慶應義塾大学ロボット技術研究会 その2 Advent Calendar 2019 21日目の記事です

5日目 | 25日目→

こんにちは、ADCが空いているのをいいことに連載を始めたFastriver(@fastriver_org)です。前回分で終わらなかったのでSlack Botの作成を引き続きやっていきます。

前回分

GASとの連携

さて、前回はSlackとGoogle Cloud Functionsを連携してオウム返しをするところまで作りました。次はCloud Functionsで受け取ったデータを他に送って処理してみましょう。今回はGASを使います。

GASとは?

GAS(Google Apps Script)はExcelで言うところのVBAマクロのようなもので、G Suite(Drive, Sheet, Docs etc.)上のファイルの操作、中身の動作などを自動化できます。開発言語はJavascriptをベースとしたScript言語で、ブラウザ上でコードを書くことが可能です。Visual Basicを書きたくない人におすすめ。

Apps Scriptの作成

https://script.google.com/home にアクセスしましょう。”G Suite Developer Hub”というページらしいです(別にG Suiteじゃなくても使える)。※keio.jpのアカウントだとscriptの公開で引っかかるので外部公開を目指している人は一般のアカウントを使いましょう

左上の[+ 新しいプロジェクト]を押すとエディタに飛びます。ここの”コード.gs”というところを編集して関数を作っていくわけです。

claspの導入

先にも言ったとおりGASはJavascript(みたいなもの)で記述します。案の定私はJavascriptを書きたくないので、claspを導入してローカルで開発します。

claspは本家Googleが開発した、GASをローカルで書かせてくれるツールです。clasp自体がTypeScriptに対応しているため、Cloud Functionsのように一度buildしてからDeployするのではなくそのままDeployしてくれます。

まずclaspをグローバルでインストール。GASのプロジェクトを作成したアカウントでログインします。

$ npm i @google/clasp -g
$ clasp login

続いて先程作成したプロジェクトをローカルに持ってきます。

$ mkdir tipsBot_KRA_gas
$ cd tipsBot_KRA_gas
tipsBot_KRA_gas$ clasp clone [YOUR_SCRIPT_ID]
Warning: files in subfolder are not accounted for unless you set a '.claspignore' file.
Cloned 2 files.
└─ appsscript.json
└─ コード.js
Not ignored files:
└─ appsscript.json
└─ コード.js                                                                                                                                                                          Ignored files:
└─ .clasp.json

script IDはエディタのページの ファイル > プロジェクトのプロパティ > スクリプト ID です。プロジェクトの保存を求められるので適当に名前をつけましょう。

TypeScriptの導入

./srcフォルダを作成し、./src/main.tsで編集していきます。後はclasp側がよしなにやってくれる模様です。うれしいね!

動作確認

連携確認のため、main.tsに以下を記述します(TSを一切生かしていないとかは気にしない)。

//main.ts
function doPost(e) {
    var param = JSON.parse(e.postData.getDataAsString());

    console.log(param.text);
}

そしてpushしましょう

tipsBot_KRA_gas$ clasp push
└─ appsscript.json
└─ src/main.ts
Pushed 2 files.

ここまでやったら再びWebのエディタにアクセスしましょう。JSに変換されたdoPost関数が存在することがわかります。

Deploy

さあ、とりあえず公開しましょう。リボンの公開 > ウェブアプリケーションとして導入… からDeployします。Project versionはNew、アクセスはAnyoneにします(セキュリティ的にアレなのでこれで運用はやめようね!)。

完了するとURLをもらえます。

Cloud Functionsの編集

前回作ったCloud Functionsのプロジェクトに移動しましょう。HTTPリクエストを叩くのでrequestパッケージを追加します。TSで使うのに @types/requestも必要です。

tips_kra$ npm i request
npm WARN tips_kra@1.0.0 No description
npm WARN tips_kra@1.0.0 No repository field.

+ request@2.88.0
added 43 packages from 53 contributors and audited 141 packages in 12.18s
found 0 vulnerabilities

tips_kra$ npm i @types/request
npm WARN tips_kra@1.0.0 No description
npm WARN tips_kra@1.0.0 No repository field.

+ @types/request@2.48.4
added 3 packages from 14 contributors and audited 151 packages in 4.11s
found 0 vulnerabilities

そして./src/main.tsを以下のように書き換えます。

import { WebClient, WebAPICallResult } from '@slack/web-api';
import * as request from "request"; //追加

//略

export const tipsBot_KRA = async (req: any, res: any) => {

    //略

    if (payload.event && payload.event.type === 'app_mention') {
        //追加
        request.post({
            uri:"[YOUR_SCRIPT_URL]",
            headers: { "Content-type": "application/json" },
            json: {
                "text": payload.event.text
            },
            followAllRedirects: true,
        }, async function (error: any, response: any, body: any) {
            return res.status(200).send('OK');
        });

        const result = await web.chat.postMessage({
            text: payload.event.text.split(" ")[1],
            channel: payload.event.channel,
        });

    }
    else {
        console.log("this is not mention");
    }

    //res.status(200).send('OK'); コメントアウト
}

Deployしましょう。

tips_kra$ tsc
tips_kra$ gcloud functions deploy tipsBot_KRA --runtime nodejs8 --trigger-http

これで接続完了です!

GAS側のログはエディタの 表示 > StackDriver Logging から見られます。そのときGCPプロジェクトとの接続を求められる場合がありますが、その時は リソース > Cloud Platformプロジェクト からGCPプロジェクトの番号を入力します(番号はGCPの IAMと管理 > 設定 > プロジェクト番号)

GASでメッセージを受け取れていますね!

説明

突っ走りましたが多少コードの説明をしていきましょう。

//main.ts GASのコード
function doPost(e) {
    var param = JSON.parse(e.postData.getDataAsString());

    console.log(param.text);
}

GAS側のコードです。GASでは並列して複数の関数を作れますが、doGet()とdoPost()関数は特別で、先程公開するときに使用した”ウェブアプリケーションとして公開”のURLにアクセスしたときに呼ばれる関数になります。doGet()はGETリクエストしたときに呼ばれ、例えばHTMLをreturnしてあげればそのページが表示されます。doPost()はPOSTリクエストしたときに呼ばれます。そのデータが引数の”e”に乗ってくるわけです。

eの中身については公式に書いてあるのですが、そのとおりにしてもあまり思うように動きません。ということで上のコードが試行錯誤して得られた、上手く送られたパラメータを取得する方法となります(せっかくTSなのでinterfaceとか定義しようね!!)。

console.log()ではログを出力しています。これとは別にGASにはLogger.log()というのがあるのですが、こちらは一回実行するごとにログがリセットされるなど使い勝手が悪いので却下し、GCPのログで確認ができるconsole.log()を採用しました。Apps Scriptだけで完結したいなどの時はLoggerを使うのも良いと思います。

//index.ts Cloud Functionsのコード
import { WebClient, WebAPICallResult } from '@slack/web-api';
import * as request from "request";

// Read a token from the environment variables
const token = process.env.SLACK_TOKEN;

// Initialize
const web = new WebClient(token);

export const tipsBot_KRA = async (req: any, res: any) => {
    const payload = req.body;

    if (payload.type === 'url_verification') {
        return res.status(200).json({ 'challenge': payload.challenge });
    }

    if (payload.event && payload.event.type === 'app_mention') {

        request.post({
            uri: "[YOUR_SCRIPT_URL]",
            headers: { "Content-type": "application/json" },
            json: {
                "text": payload.event.text
            },
            followAllRedirects: true,
        }, async function (error: any, response: any, body: any) {
            return res.status(200).send('OK');
        });

        const result = await web.chat.postMessage({
            text: payload.event.text.split(" ")[1],
            channel: payload.event.channel,
        });
    }
    else {
        console.log("this is not mention");
    }
}

Cloud Functionsのコード全体像です。非常にガバガバです。俺たちは雰囲気でNode.jsを弄っている。なので言うことは特に無いです。コードのとおりです。

続けている

まだ続く…

なんかまだできていない区切りがよいので今日の分はこの辺にしておきたいと思います。

開発しながらブログを書く、というのも結構いいですね!自分の通った道を振り返って整理しながら進めるので効率は悪いですが再現性が上がります。今後もやっていこうかな。

その1

5日目 | 22日目→

コメントを残す