Slack Appに挑戦(3) - Event APIとBotUser

「SlackBotを真面目に学ぶ」の第3回
今回は、EventAPIを使ってSlackからのイベントを受け取る方法です。

1回目　https://qiita.com/kanaxx/items/a12a523ca3143b5822b8
2回目　https://qiita.com/kanaxx/items/f2025cbc961de2c6bc5b

#はじめに
前回までで、SlackBotから、Incoming Webhookを使って一方的にメッセージを送りつけたり、スラッシュコマンドに反応して仕事をすることはできるようになりました。まだ、@で呼びかけてもらうことはできません。今回は```Event API```を受け取る仕組みを実装して、@で呼びかけてもらえるようになります。

##環境
+ Heroku
  + PHP 7.3
  + Laravel 6.0.3
  + Clear DB
+ Windows
  + PHP 7.2.11
  + Laravel 6.0.3
  + Maria DB  Ver 15.1

いつもどおり、基本的にはWindowsでの作業です。

前回作った```Signing Secret```によるRequest Verificationが適用済みの環境でのサンプルです。今回公開するソースコードにはVerificationに関するものは含まれておりません。インターネット上のサービスを公開するときには、各自の判断のもと自己責任でお願いします。

##EventAPIとは何か？
EventAPIは、Slack内で起きたイベントをトリガーに、発生した内容をApp指定のURLに送る仕組みです。インターネット経由でコールバックを受ける仕組みです。Slackが送信元になり、HTTPSで待ち受ける形式です。

公式ドキュメントに書いてあるとおり、こちら側（your server）には以下の動きを期待されています。
>1. A user creates a circumstance that triggers an event subscription to your application
2. Your server receives a payload of JSON describing that event
3. Your server acknowledges receipt of the event
4. Your business logic decides what to do about that event
5. Your server carries out that decision

###お作法
SlackからEventを受け取ったら、3秒以内に2xx系のレスポンスを返却する必要があります。これはSlash Commandと同じです。時間のかかる仕事はイベントの処理と分離してレスポンスだけ先に返すような設計が必要です。なお、3秒以内にレスポンスがない場合には、エラーとして扱われ、3回までイベントが再送されてきます。
あと、Slashコマンドと違い、EventAPIではメッセージを送り返すことができません（```response_url```が含まれていないので）。EventAPIで何か仕事をしたあとに返却したい場合には、第1回でやったIncoming Webhook（Channel固定のもの）に返すか、WEBAPIを使ってメッセージの送信をします。

#やってみよう


##Event APIのエンドポイントを作成
Event APIのエンドポイントと認められるには、最低限、以下の仕様を満たす必要があります。

**SlackからのHTTP(POST)を受け取り、パラメータに含まれているchallengeの値をそのまま返す。**

まずは、最低限の要件を満たすLaravelのコントローラを作ります。

###Controller

artisanコマンドで作って、eventメソッドを実装します。

```
php artisan make:controller SlackEventController
Controller created successfully.
```

```php:app/Http/Controller/SlackEventController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Log;

class SlackEventController extends Controller
{
    public function event(Request $request){
        //challengeを返す実装
        if( $request->input('type') == 'url_verification'){
            return $request->input('challenge');
        }

        return '';
    }

}
```

###routeの設定

```php:routes/api.php
Route::any('/event', 'SlackEventController@event');

```

ここまでの成果をHerokuにデプロイします。以下のURLにアクセスして有効であることを確認しておきます。

https://slalack-bot.herokuapp.com/api/event

```json
{"ok":false,"error":"signature error"}
```

## Event APIの設定

次は、このURLに対してSlackからEventを飛ばす設定をします。

### Event APIのエンドポイントの設定
AppのBasic Informationから、```Event Subscription```をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/0ceb5b15-94d6-fee8-f701-3712265b6ec2.png)

Enable EventsのスイッチをONに切り替えて、Request URLにエンドポイントのURLを入れます。URLを入力するとすぐにSlackからアクセスが発生し、URLが有効かどうか判定されます。Slackが送った```challenge```の値を返していない場合には、下のようになり有効になりません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/82b80e9b-81ee-304d-35e2-ee4092ed7d99.png)

有効な場合には、verifiedという緑チェックがつきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/bd47edec-99e2-d945-6b5b-ad24fa1e44c4.png)

これで、Slack内で発生したイベントをHerokuで受け取ることができます。

###受信するイベントの設定
Appで受け取るイベントを決めます（Subscribeと呼びます）。ここで設定したイベントが発生した場合だけ、SlackからRequest URLにリクエストが送られます。イベントの送信は1時間あたり30000回という制限値がありますので、必要なものだけを設定しましょう。

>**Rate limiting**
We don't want to flood your servers with events it can't handle.
Event deliveries currently max out at 30,000 per workspace per 60 minutes. If your app would receive more than one workspace's 30,000 events in a 60 minute window, you'll receive app_rate_limited events describing the conditions every minute.
引用
https://api.slack.com/events-api#rate_limiting

実際にサブスクライブするときは、```Add Wordspace Event```ボタンをクリックしてイベント情報をセットするだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/a713bd57-d9dd-300e-11f7-a36cf5d4ab26.png)

今回は、```reaction_added```だけを設定しました。BotがいるSlackワークスペース内のどこかでリアクションが発生するとEventが飛んできます。

##いざ実行

Slack内で```reaction_added```イベントが発生するように、リアクションを付けます。どこでも大丈夫です。今回は\#randomチャネルでリアクションを付けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/80359042-dc9b-a4e3-85da-b95168c3dc39.png)



Event APIのエンドポイントに設定した、SlackEventControllerのeventメソッドが呼ばれます。現在の実装では、パラメータのログがSlackに飛んでくるだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/a088084c-42e1-1af5-eea1-474b7b800a10.png)

届きましたね。これで、Eventのデータ構造がわかります。
requestの:one:event.typeが発生したイベントの種類で、:two: event.reactionがリアクションしたアイコンの種類ですね。


#イベントからメッセージを返す
今のままだと、イベントをひたすら受け止めるだけなので、イベントに反応してみます。Slash Commandは呼びかけに返すことができるのですが、EventAPIではできないので、普通にIncoming Webhookで受け取ったイベントに反応してみます。

##Webhookの設定
第一回でやった内容と同じく、Incoming Webhookを設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/d90bc1d3-b48a-3ac3-5960-b0e2e4a477ac.png)

発行されたURLを覚えておいてください。

##Controllerを修正
SlackEventControllerのeventメソッドが、typeが```url_verification``` にしか対応していなかったので、event_callbackの分岐点を作り、event.typeの内容によってやることを変えました。
その中で、WebhookのHTTPSリクエストを発行するためGuzzileクライアントを作り、メッセージを送っています。

```php:app/Http/Controller/SlackEventController.php
    public function event(Request $request){
        //challengeを返す実装
        if( $request->input('type') == 'url_verification'){
            return $request->input('challenge');
        }

        \Log::channel('slack')->info($request->all());

        if( $request->input('type') == 'event_callback'){
            if( $request->input('event.type') == 'reaction_added'){

                $emoji = $request->input('event.reaction');
                $user = $request->input('event.user', 'ななし');
                $channel = $request->input('event.item.channel');

                try {
                    //変更必要
                    $webhookToRandom = 'https://hooks.slack.com/services/xxx/yyy/zzz';
                    $guzzile = new \GuzzleHttp\Client();
                    $option = [];
                    $option['json']=[
                        'text'=> "$user さんが $channel にリアクションしました！" . ":$emoji:",
                    ];
                    $response = $guzzile->post($webhookToRandom, $option);
                    $body = $response->getBody();
                    $data = (String)$body;
                    $code = $response->getStatusCode();
                    
                    \Log::channel('slack')->info('webhook:' . $code . " " . $data);
                } catch ( Exception $ex ) {
                    \Log::channel('slack')->info($ex);
                }
                
            }
        }
        //返したメッセージは使われない
        return 'ok';
    }
```

そんなに難しくないですね。
では、Herokuにデプロイします。

##さて、実行

\#randomチャネルのメッセージに対して、複数のアカウントからリアクションしてみます。
すると、Botのアカウントからメッセージが送られてきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/666c7423-75d4-c442-de4d-38833fd741f1.png)

見てのとおり、```User```と```Channel```の丈夫は、Slackで扱う内部的なIDで受け渡されます。そのままメッセージにしても伝わりにくいです。この課題は次回に持ち越しです。


#@の呼びかけに反応する

次は@でボットに呼びかけたときに反応するやつです

##Bot Userを定義
Slack Appには1つまで```Bot User```を追加できるようになっています。Bot Userは普通のユーザのように振る舞います。@をタイプすると、ヒントが出てきたりします。

左側のBot Userから
![2019-10-05_00h58_29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/0f21677b-00ce-9cae-289b-7a6dbd47fe63.png)

Botの表示名とユーザ名を設定します。
![2019-10-05_00h59_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/d2e9a6c9-976b-047c-fc60-a67f1e2c1b21.png)

Appの横の:heavy_plus_sign:マークから、Bot Userを
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/6e39cdc6-5530-2d51-90ce-56e7397ff70f.png)


##サブスクライブするイベントの変更
```Add Bot User Event```から```app_mention```を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/b9c9636e-ade5-44cd-2aa9-c636a451ec5c.png)

先ほどのWorkspaceイベントと違うので注意

##Controller修正

event_callbackの分岐の下に、```app_mention```の分岐を作ります。実装の内容は受けた内容をログに出力するだけです。

```php:app/Http/Controller/SlackEventController.php

    public function event(Request $request){
        //challengeを返す実装
        if( $request->input('type') == 'url_verification'){
            return $request->input('challenge');
        }

        \Log::channel('slack')->info($request->all());

        if( $request->input('type') == 'event_callback'){
            if( $request->input('event.type') == 'reaction_added'){

                //省略

            }else if( $request->input('event.type') == 'app_mention'){
                $user = $request->input('event.user', 'ななし');
                $channel = $request->input('event.channel');
                $text = $request->input('event.text');

                $log = 'メンション来ましたー' . PHP_EOL .
                 'ユーザ：'.$user . PHP_EOL .
                 'チェンネル：'.$channel . PHP_EOL .
                 'メッセージ：'.$text . PHP_EOL ;
                 
                
                 \Log::info($log);
                 \Log::channel('slack')->info($log);
            }
        }
        return 'ok';
    }
```

##よびかけ実行

ワークスペースにBotUserが追加されていると、@でBotUser候補が出てくるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/814f9be0-8005-9e7b-4c8d-884fc377ff94.png)

呼びかけてみましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/94169e7e-ca02-77db-45ad-993405234eb5.png)

eventメソッドが呼ばれて、```Log::channel('slack')->info($request->all());```が実行され、ログが届きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/c58a5e4b-a505-9599-efa1-705bc80206cc.png)

Eventのデータ構造を確認します。
:one:のtypeがapp_mentionになっていて、 :two:のtext変数から実際の呼びかけの内容が受け取れます。

###ログの確認

ログも確認しておきます。

まずは、**\Log::info($log);**で送ったログ

```
2019-10-05T07:36:26.422867+00:00 app[web.1]: [05-Oct-2019 16:36:26 Asia/Tokyo] [2019-10-05 16:36:26] production.INFO: メンション来ましたー
2019-10-05T07:36:26.422959+00:00 app[web.1]: ユーザ：UDX2143CM
2019-10-05T07:36:26.423053+00:00 app[web.1]: チェンネル：CDWNT347M
2019-10-05T07:36:26.423264+00:00 app[web.1]: メッセージ：<@UN9R2L9PV> おーい、おーい。
2019-10-05T07:36:26.423354+00:00 app[web.1]: 聞こえてますかー:robot_face:
```

textの中にあるユーザ名（今回はBotUserの名前）が<@slackのユーザID>の形で送られてきています。

次に、**\Log::channel('slack')->info($log);**で送ったログ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/892e96ae-bc53-6b21-f1d0-a82c9704effd.png)

絵文字```:robot_face:```や@メンション部分```<@UN9R2L9PV>```が、人間の目で読める形に変換されて送られてきます。


#まとめ
やっとBotらしくなってきました。ただ、この3回分の成果では過去ログを探して何かやることはできないんですね。なので、次回はWebAPIとUserToken, BotTokenあたりをやってみます。

#参考URL
Slack Event API
https://api.slack.com/events-api

Slack APIでbotを作る7: Events API編
https://www.dkrk-blog.net/slack/slack_api07