今更ながらSlackBotを作ってみた

今回は、SlackBotを作ってみます。
いまさらながらな感はありますが、Slackアプリをスマホに常時入れているし、LINEと違って、いつでもメッセージを送信できるからです。(LINEの場合好きな時に送るには有料あるいはTrial版である必要があったります）

今回はまずはエコーバックするところまでです。

（補足）
ただし、1点だけ、残念なことがありまして、AWS Lambdaでも同じように動くようにしたかったのですが、現時点では不十分です。SlackからのWebhook呼び出しに対する応答時間の制限が短いためです。うまくやればできるのかもしれませんが、まだAWSの勉強不足のため、また今度にしたいと思います。
ですので、とりあえず、ローカルに立ち上げるSwagger環境を前提とします。

Swagger環境は、以下の投稿を参考にしてください。
　[SwaggerでLambdaのデバッグ環境を作る(1)](https://qiita.com/poruruba/items/e3a46787c79517a815f9)

以下、参考情報です。

slack api
　https://api.slack.com/

Node Slack SDK
　https://github.com/slackapi/node-slack-sdk

以下、投稿の続編です。
　[今更ながらSlackBotを作ってみた：Slach Commands](https://qiita.com/poruruba/items/b386247d8ace78488f67)
　[今更ながらSlackBotを作ってみた：Interactive Components](https://qiita.com/poruruba/items/759f0eaba798d90419ea)
　[今更ながらSlackBotを作ってみた：ダイアログ](https://qiita.com/poruruba/items/d0e0852715687ea10bcf)
　[今更ながらSlackBotを作ってみた：Incoming Webhooks](https://qiita.com/poruruba/items/b78fb83ae969e9e03b75)

#ワークスペースを作成する

まずは、まだワークスペースを作成していない場合は作成しておきます。

slack
　https://slack.com/intl/ja-jp/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/09543a02-aec5-981c-ce32-ebaa6a616021.png)

右上のワークスペースボタンから、「ワークスペースを作成する」を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/2cdf49ab-3a60-5178-0de2-12435903ff73.png)

ご自身のメールアドレスを入力して、Next→ を押下

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/47944cc6-e979-9a6a-a545-13f3a9f7e2d7.png)

そうすると、メールアドレスに、3+3の確認コードの数字が送られてきますので、入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/cff762c3-75ae-4b05-3886-530ea96cdc74.png)

たとえば、TestTream とでもしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/3879d537-eb82-522e-dacc-b907a4391967.png)

TestProjectとでもしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/4d072628-0df6-e0cd-903d-0fd9862c41a5.png)

あとでも招待できるので、skip for now します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/bd07133b-7a30-f741-ee45-081561e4173e.png)

できあがりました。
「新規登録する」ボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/19724b1d-e26b-713e-e2b0-dbefc4865b7f.png)

表示させたい自分の名前と、次にログインするときのパスワードを入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/f835860c-405e-232f-bcd3-86a64b82c775.png)

SlackのURLは、自動的に割り振られていますが、以降使うので、（他人と重ならなければ）変えることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/e49d5a02-36dd-978f-2b9c-26b2f9448aea.png)

招待は後回しに、「完了」ボタンを押下で完了です。

#Botを作成する

さっそくBotを作成していきます。
まずは、slack apiを開きます。
　https://api.slack.com/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/0d7939a9-b095-d12d-9ee7-bd5c1b246845.png)

次にページの右上にある「Your Apps」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/f387b74e-0153-9209-1fbe-c561d8add809.png)


そして、「Create New App」ボタンを押下します。
AppNameには適当に「TestBot」とでもしておきます。
Development Slack Workspaceには、先ほど作成したワークスペースを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/45ee1549-7773-2f1c-e24b-ab6fdaecdad9.png)

作成が完了し、以下のページが表示されます。Setting－Basic Informationです。
以降このページを中心に設定を進めます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a7161cca-e51b-7362-a445-5efaf14ee18f.png)

作ったAppをBotにするため、「Bots」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/e0ccbc98-dbf0-d375-daed-716b2d9ea4a1.png)

「Add a Bot User」ボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/88dd557f-fc9a-85ec-a40d-16e798070b27.png)

DisplayNameに「TestBot」、Default usernameには「testbot」とでもしておきます。
Always Show My Bots as Onlineは、常時サーバを立ち上げるので、Onにしておきましょう。
最後に「Add Bot User」ボタンを押下します。

今のままだと何もできないので、メッセージ送信できるようにします。

Setting－Basic Information に戻ります。
Add features and functionality を選択します。
「Permmissioins」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/ff9067db-ba5a-51e0-9879-2d09e3628f39.png)

ScopesのSelect Permission Scopesのところで、以下を選択します。
「Send message as user」

最後に、「Save Changes」ボタンを押下します。
同じページの最初に戻って、「Install App to Workspace」を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/af053fa8-4465-3807-36ff-307db8d70e79.png)

そうすると、許可を確認する画面が表示されます。「許可する」を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/9d3c080b-f50d-b0c7-91a6-e83e716ffa61.png)

これで、ワークスペース「TestTeam」に「TestBot」が追加されました。

#WebAPIの種類

Slackにはメッセージ通信するAPIとしていくつかの種類があります。
以下、私なりの理解です。

* Incoming Webhook
　いつでも好きな時に、特定チャネルまたは特定ユーザにメッセージを送信します。（受信ではない）

* Slash Command
　「/」(スラッシュ)に続けて入力するコマンドを作ります。

* RTM(Real Time Messaging) API
　WebSocketを使って、リアルタイムにイベントを受信したりメッセージを送信します。

* Event API
　メッセージ投稿やメンバー追加などのイベント発生時に通知を受け取り、メッセージで応答します。
 
* Interactive Components
　上記のメッセージ送受信に加えて、ボタンや選択肢などのリッチなUIを提供します。（ダイアログもこれに該当？）

今回、次回以降の記事で、ひとつひとつ説明していきます。(RTM APIを除く)

#エコーバックするBotにする

まずは、一番単純なエコーバックするBotを作成しましょう。
先に、エコーバックするためのサーバを立ち上げます。

その前に、メモっておく必要がある文字列が2つあります。
まずは、Settings－Basic Informationの、App Credentialsにある「Verification Token」です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/39fc9163-6ff4-29ce-a87e-1f5adb49a8ec.png)

次に、Features － OAuth&Permissions にある「Bot User OAuth Access Token」です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/6a13d403-33a9-de47-414f-62b3a1def1f6.png)

それでは、サーバの立ち上げを始めます。

以下のnpmモジュールを使います。
・@slack/web-api
・node-fetch
・dotenv

以下の2つのエンドポイントを作成します。
この2つの違いは、入出力パラメータのフォーマットの違いです。メッセージを送受信する方法はいくつかあるのですが、それぞれによって微妙にフォーマットが違うのです。

```swagger.yaml

  /slack-testbot:
    post:
      x-swagger-router-controller: routing
      operationId: slack-testbot
      produces:
        - text/plain
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: string

  /slack-testbot-cmd:
    post:
      x-swagger-router-controller: routing
      operationId: slack-testbot-cmd
      consumes:
        - application/x-www-form-urlencoded
      produces:
        - text/plain
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: string
・・・
```

エンドポイントから実装へのルーティングです。

```controllers\routing.js
・・・
/* 関数を以下に追加する */
const func_table = {
//  "test-func" : require('./test_func').handler,
  "slack-testbot" : require('./slack_testbot').handler,
  "slack-testbot-cmd" : require('./slack_testbot').handler,
};
・・・
```

エンドポイントに対応する実装です。

```controllers\slack_testbot\index.js
'use strict';

const SLACK_VERIFICATION_TOKEN = process.env.SLACK_VERIFICATION_TOKEN;
const SLACK_ACCESS_TOKEN = process.env.SLACK_ACCESS_TOKEN;

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const SlackUtils = require(HELPER_BASE + 'slack-utils');
const app = new SlackUtils(SLACK_VERIFICATION_TOKEN, SLACK_ACCESS_TOKEN);

exports.handler = app.lambda();
```

Slackボットのためのユーティリティです。

```controllers\helpers\slack-utils.js
'use strict';

const fetch = require('node-fetch');
const { WebClient } = require('@slack/web-api');

class SlackUtils{
    constructor(verification_token, access_token){
        this.web = new WebClient(access_token);
        this.verification_token = verification_token;
        this.map = new Map();
    }

    async initialize(){
        if( this.my_bot_id )
            return Promise.resolve();

        return this.web.auth.test()
        .then(result =>{
            this.my_user_id = result.user_id;
            console.log('userid=' + this.my_user_id);
            return this.web.users.info({user: result.user_id})
        })
        .then(result =>{
            this.my_app_id = result.user.profile.api_app_id;
            console.log('appid=' + this.my_app_id);
            return this.web.bots.info({bot: result.user.profile.bot_id})
        })
        .then(result =>{
            this.my_bot_id = result.bot.id;
            console.log('botid=' + this.my_bot_id);
        })
        .catch(error =>{
            console.log(error);
        });
    }

    postMessage(message){
        return this.web.chat.postMessage(message);
    }

    dialogOpen(options){
        return this.web.dialog.open(options);
    }

    incomingMessage(webhook_url, body){
        return this.responseMessage(webhook_url, body);        
    }

    responseMessage(response_url, body){
        return fetch(response_url, {
            method : 'POST',
            body : JSON.stringify(body),
            headers: { "Content-Type" : "application/json; charset=utf-8" } 
        })
        .then((response) => {
            if( response.status != 200 )
                throw 'status is not 200';
            return response.text();
        });
    }

    ackResponse(){
        var response = {
            statusCode: 200,
            headers: { "Content-Type": "text/plain" },
            body: ""
        };
        return response;
    }

    message(handler){
        this.map.set('message', handler);
    }

    action(handler){
        this.map.set('action', handler);
    }

    response(handler){
        this.map.set('response', handler);
    }

    command(handler){
        this.map.set('command', handler);
    }

    submission(handler){
        this.map.set('submission', handler);
    }

    cancellation(handler){
        this.map.set('cancellation', handler);
    }

    lambda(){
        return async (event, context, callback) => {
            await this.initialize();
            
            var body = JSON.parse(event.body);
            if( body.payload )
                body = JSON.parse(body.payload);
            
            if( body.token != this.verification_token )
                return;
            
            if( body.type == 'url_verification' ){
                var response = {
                    statusCode: 200,
                    headers: { "Content-Type": "text/plain" },
                    body: body.challenge
                };
                return response;                
            }

            console.log('body.user_id', body.user_id);
            if( body.event )
                console.log('body.event.user', body.event.user);
            if( body.user )
                console.log('body.user.id', body.user.id);
            console.log(JSON.stringify(body));

            if( body.user_id == this.my_user_id || (body.event && body.event.user == this.my_user_id ) )
                return this.ackResponse();

            var type = 'message';
            if( body.event && body.event.message )
                type = 'response';
            if( body.command )
                type = 'command';
            if( body.type == 'block_actions' )
                type = "action";
            if( body.type == 'dialog_submission')
                type = "submission";
            if( body.type == 'dialog_cancellation')
                type = "cancellation";

            var handler = this.map.get(type);
            if( handler )
                handler(body, this.web);
            else
                console.log(type + ' is not defined.');

            callback(null, this.ackResponse());
        }
    }
};

module.exports = SlackUtils;
```

環境変数に、各人のVerification TokenとBot User OAuth Access Tokenを設定します。

```.env
SLACK_VERIFICATION_TOKEN="【Verification Token】"
SLACK_ACCESS_TOKEN="【Bot User OAuth Access Token】"
```

さっそく立ち上げておきます。

エコーバックのトリガとしては、Event APIを使っていきます。
以下の手順で有効にします。

・Features－Event Subscriptions を選択し、Enable EventsをOnにします。
・そして、Request URLのところに、先ほど立ち上げたサーバのエンドポイントをフルパスで指定します。/slack-testbotの方です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/c279c8bc-dff5-f676-b748-a2939d4ec073.png)

無事に、「Verified」となりましたでしょうか？slack-utils.jsがVerifyに必要な処理をしています。

次に、Subscribe to Bot Events に、「message.im」を選択し、最後に右下の「Save Changes」を押下します。
これにより、メッセージを受信したときに、Eventが発火されるようになります。
そこら辺のハンドリングは、slack-utils.jsがうまくやってくれるようにしておきました。

発火されたEventは、app.message()に指定したコールバックを呼び出します。
したがって、このコールバック関数内に、処理したい内容(今回はエコーバック)を記述します。
index.jsを少し追記します。

```controllers\slack_testbot\index.js
'use strict';

const SLACK_VERIFICATION_TOKEN = process.env.SLACK_VERIFICATION_TOKEN;
const SLACK_ACCESS_TOKEN = process.env.SLACK_ACCESS_TOKEN;

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const SlackUtils = require(HELPER_BASE + 'slack-utils');
const app = new SlackUtils(SLACK_VERIFICATION_TOKEN, SLACK_ACCESS_TOKEN);

// 追記ここから
app.message(async (body, web) =>{
    if(body.event.text)
        app.postMessage({ channel: body.event.channel, text: body.event.text + " です。", as_user: true });
});
// 追記ここまで

exports.handler = app.lambda();
```

ちょっと補足します。

受信しているメッセージは以下を参考にしてください。

slack api：Event API：Receiving Events
　https://api.slack.com/events-api#receiving_events
slack api：message.im
　https://api.slack.com/events/message.im

返信している内容は以下を参考にしてください。

slack api：Reference: Message payloads
　https://api.slack.com/reference/messaging/payload

#動作確認

スマホで動作確認してみましょう。
以降は、AndroidのSlackでの画面です。

左上隅をタッチします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/ef8906ec-1392-a088-ce68-17a9bace653c.png)

ワークスペースを追加 を選択します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/96cfa5f2-07a1-88b0-846f-cf38f5bc8b03.png)

ワークスペース「TestTeam」を作った人と同じであれば、そのワークスペースが表れていると思います。
サインインを押下します。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/f4eaad7a-4682-79ba-3842-c12f8f01d3ea.png)

さっきのパスワードを入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a94482ce-c047-7f9e-aaf8-249a0263fd48.png)

ログインできました。
左上隅をタッチし、TestBotを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/594d4590-81d6-444f-cd1c-b598e21e9a0d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/482debca-1e35-e0cf-733b-c6c24d76852f.png)
 
TestBotが開きましたので、「あかさたな」と投稿してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/388bbf91-fef6-8a28-111b-fe19e69f79d7.png)

「あかさたな です」と返ってくれば成功です。

# 終わりに

今回はここまでとします。
次回から、ボタンや選択肢を表示してインタラクションを高めたり、ダイアログボックスを表示したり、スラッシュコマンドを追加したりしてみます。

SlackのAPIは多数あり、わかりにくいです。
ですので、もしかしたら使い方が間違っているかもしれませんので、ご指摘いただけると助かります。

以上