Slackでモーダル（ダイアログ）を使ってデータを送信する

最近、弱々エンジニア会に参加しました。
記事貢献を兼ねて、久しぶりのQiita投稿です！
今回はSlackでSlackAppを使って、モーダル（ダイアログ）使ったデータの入出力を行う方法を解説していきたいと思います。

## 今回作るもの
**/modal** というSlashコマンドを実行すると [Block Kit Builder](https://api.slack.com/tools/block-kit-builder) で作られたモーダルを表示して、入力された値をサーバーに送るSlackアプリを作ります。
![ezgif.com-video-to-gif (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99737/df0573cd-9832-661b-a7a7-0dd26130d6fb.gif)

## 参考資料
https://api.slack.com/surfaces/modals/using

## 開発環境
- PHP 7.2 （Laravel 6.16）
- GAE Standard （外部からアクセスできるサーバーであればなんでも可）
- Slack **（フリープランでも可）**

## 実装方法
### Slackアプリの用意
モーダルを出す方法がメインなので細かくは書きません。
細かい部分は [運用中のブログ](https://belltree.life/slack-bot-hands-on/) にまとめてるので気になる方はご覧ください〜

#### アプリの作成
https://api.slack.com/apps から Create New App を選択して作っていきます。
<img width="1395" alt="slack-app-create.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99737/12418b47-cea9-3ec7-16c8-dd0aad4d82d7.jpeg">

#### Slashコマンドの登録
適当にSlashコマンドを登録します。
<img width="997" alt="slash-install.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99737/048f0295-317f-9fd2-8235-1a9900c0646a.jpeg">

#### 権限の設定
今回のサンプルであれば **commands** のみで作成できます。
※ Slashコマンドを登録すると勝手に入ると思います。
いけなかったら教えてください＼(^o^)／

#### interactivity の設定
ダイアログに値を入力された後、その値が送られる向き先の設定をする必要があります。
<img width="1003" alt="interactivity.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99737/4197c2df-babe-41ad-5c15-765f6445af4f.jpeg">

#### ワークスペースにインストール
Install App からインストールします。
表示に従っていればインストールできると思いますが、一点初回インストールの場合は下記のApp Display Name を設定しないと「インストールできるボットユーザーがありません」と怒られてしまいます。
<img width="978" alt="apphome.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99737/1cd7f28e-06a7-2bb9-c2ad-9b2a47d49638.jpeg">

以上でSlackの設定は終了です！（結構細かったかもしれん

### コーディング
ようやく本編です。Laravelで書いていきます。
トークンとかは分けるべきなんですが、都合上まとめてますので良い感じで整理してください。

#### /modal が実行されたときの処理

```php
Route::post('/modal', 'SlackController@modal')->name('modal');
```
な感じでSlackControllerにリクエストが飛んだ後の処理を書いていきます

<details><summary>SlackController.php（折り畳み）</summary><div>

```php
<?php

namespace App\Http\Controllers;

use GuzzleHttp\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;

class SlackController extends Controller
{
    const BOT_TOKEN = 'xoxb-xxxxxxxx'

    /**
     * /modal のSlashコマンドを実行すると呼ばれる場所
     *
     * @param Request $request
     * @return Response
     */
    function modal (Request $request) {
        $url = 'https://slack.com/api/views.open';
        $token = self::BOT_TOKEN;
        $view = $this->getModalContent();
        $trigger_id = $request->input('trigger_id');

        $params = [
            'view' => \GuzzleHttp\json_encode($view),
            'trigger_id' => $trigger_id
        ];

        $headers = [
            'Content-type' => 'application/json',
            'Authorization'  =>  'Bearer ' . $token
        ];

        $client = new Client();
        $response = $client->request(
            'POST',
            $url, // URLを設定
            [
                'headers' => $headers,
                'json' => $params
            ] // パラメーターがあれば設定
        );

        $log = \GuzzleHttp\json_decode($response->getBody()->getContents(), true);
        Log::info(print_r($log, true));

        return response('',200);
    }

    /**
     * ダイアログのテンプレートを作る
     *
     * @return array
     */
    function getModalContent () {
        return [
            "type" => "modal",
            "title" => [
                "type" => "plain_text",
                "text" => "メンバー登録",
                "emoji" => true
            ],
            "submit" => [
                "type" => "plain_text",
                "text" => "登録",
                "emoji" => true
            ],
            "close" => [
                "type" => "plain_text",
                "text" => "キャンセル",
                "emoji" => true
            ],
            "blocks" => [
                [
                    "type" => "input",
                    "block_id" => "name",
                    "element" => [
                        "type" => "plain_text_input",
                        "action_id" => "氏名",
                        "placeholder" => [
                            "type" => "plain_text",
                            "text" => "田中 太郎"
                        ],
                    ],
                    "label" => [
                        "type" => "plain_text",
                        "text" => "氏名"
                    ]
                ],
                [
                    "type" => "input",
                    "block_id" => "mail",
                    "element" => [
                        "type" => "plain_text_input",
                        "action_id" => "メールアドレス",
                        "placeholder" => [
                            "type" => "plain_text",
                            "text" => "xxx@gmail.com"
                        ],
                    ],
                    "label" => [
                        "type" => "plain_text",
                        "text" => "メールアドレス"
                    ]
                ],
                [
                    "type" => "input",
                    "block_id" => "language",
                    "optional" => true,
                    "element" => [
                        "type" => "checkboxes",
                        "action_id" => "得意言語",
                        "options" => [
                            [
                                "text" => [
                                    "type" => "plain_text",
                                    "text" => "PHP",
                                    "emoji" => true
                                ],
                                "value" => "value-0"
                            ],
                            [
                                "text" => [
                                    "type" => "plain_text",
                                    "text" => "Ruby",
                                    "emoji" => true
                                ],
                                "value" => "value-1"
                            ],
                            [
                                "text" => [
                                    "type" => "plain_text",
                                    "text" => "Javascript/Node.js",
                                    "emoji" => true
                                ],
                                "value" => "value-2"
                            ],
                            [
                                "text" => [
                                    "type" => "plain_text",
                                    "text" => "Python",
                                    "emoji" => true
                                ],
                                "value" => "value-3"
                            ]
                        ]
                    ],
                    "label" => [
                        "type" => "plain_text",
                        "text" => "得意言語",
                        "emoji" => true
                    ]
                ]
            ]
        ];
    }
```
</div></details>

ちなみに以下のライブラリを使用しています。
[GuzzleHttp](http://docs.guzzlephp.org/en/stable/) 
[Google StackDriver Logging](https://cloud.google.com/logging/docs/reference/libraries?hl=ja)


メイン処理ですが Slack API の [view.open](https://api.slack.com/methods/views.open) というAPIを利用しています。

```php
$url = 'https://slack.com/api/views.open';
```
ダイアログの中身は [Block Kit Builder](https://api.slack.com/tools/block-kit-builder) で作ったものを使っていますが、細かい部分を修正しているので紹介します。

##### block_id と action_id は設定しておいたほうがいい
デフォルトでは付いていないのですが、 block_id と action_id はちゃんと設定したほうがいいです。
理由は次に紹介するパラメータの受け取り時にどの値なのかわからなくなりパラメータの取得が面倒になります。
それぞれ日本語も入れられたので結果を返す時にちゃっかり使っています。

##### optional の場所を間違えないで
地味にやらかしたのが optional の場所。
optionalを付けることで任意の値として設定できるようになりますが、 element の中 に入れてしまっていたので何回か詰まっていました。正しくは **element の外** です。 

正しく [view.open](https://api.slack.com/methods/views.open) が実行されていれば /modal を実行した時にダイアログは表示されるようになっているはずです。

#### モーダル経由で入力値を送られてきたときの処理
```php
Route::post('/interactiveMessage', 'SlackController@interactiveMessage')->name('interactiveMessage');
```
な感じでSlackControllerにリクエストが飛んだ後の処理を書いていきます

<details><summary>SlackController.php（折り畳み）</summary><div>

```php
<?php

namespace App\Http\Controllers;

use GuzzleHttp\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;

class SlackController extends Controller
{
    const BOT_TOKEN = 'xoxb-xxxxxxxx';

    // 先程のControllerに追記する形で
    
    /**
     * モーダルから送られてくるリクエストを受け取る
     *
     * @param Request $request
     * @return Response
     */
    function interactiveMessage (Request $request) {
        // 早期レスポンス
        response('',200)->send();

        $payload = $request->input('payload');
        $postData = \GuzzleHttp\json_decode($payload, true);

        Log::info(print_r($postData, true));

        switch ($postData['type']) {
            case 'view_submission':
                // submit された時

                /*
                 * なんらかの処理
                 */

                $text = '登録しました！';
                $user =  $postData['user']['id'];

                $attachments = $this->makeAttachments($postData['view']['state']['values']);

                Log::info(print_r($attachments, true));

                // メッセージを送る
                // modalを使う場合、response_urlを取得できなかったので強制的にメッセージ投稿
                $response = $this->postEphemeral($user, $text, $attachments);

                $log = \GuzzleHttp\json_decode($response->getBody()->getContents(), true);
                Log::info(print_r($log, true));
                break;
            case 'block_actions':
                // チェックボックスなどが更新された時
                break;
        }
    }

    /**
     * 「あなただけに表示されています」のメッセージを送る
     *
     * @param $user
     * @param $text
     * @param $attachments
     * @return mixed
     */
    function postEphemeral ($user, $text, $attachments = []) {
        $url = 'https://slack.com/api/chat.postEphemeral';
        $token = self::BOT_TOKEN;

        $params = [
            'token' => $token,
            'attachments' => \GuzzleHttp\json_encode($attachments),
            'channel' => $user,
            'text' => $text,
            'user' => $user,
        ];

        Log::info(print_r($params, true));

        $client = new Client();
        $response = $client->request(
            'POST',
            $url,
            ['query' => $params]
        );

        return $response;
    }

    /**
     * 回答を受け取った結果をまとめる
     *
     * @param $values
     * @return array
     */
    function makeAttachments ($values) {
        $text =  "";
        foreach ($values as $key => $value) {
            foreach ($value as $name => $detail) {
                switch ($detail['type']) {
                    case 'plain_text_input':
                        $text .= $name . ' : ' . $detail['value'] . "\n";
                        break;
                    case 'checkboxes':
                        $checkedValue = '';
                        if (isset($detail['selected_options'])) {
                            foreach ($detail['selected_options'] as $selected_option) {
                                $checkedValue .= $selected_option['text']['text'] . " ";
                            }
                        } else {
                            $checkedValue .=  '指定なし';
                        }

                        $text .= $name . ' : ' . $checkedValue . "\n";
                        break;
                }
            }
        }
        $attachments[] = [
            'text' => $text,
        ];
        return $attachments;
    
}

```
</div></details>

こちらも同様に注意すべきポイントを紹介します。

##### 早期レスポンスをしておいたほうが良い
```php
// 早期レスポンス
response('',200)->send();
```
モーダルからの値を送った後、3秒以内にレスポンスを返さないとエラーが返る仕様になっています。
DB更新処理など少し時間がかかる処理を挟む場合、早期レスポンスでまずダイアログを閉じてしまってから処理を行いましょう。

##### モーダルは色々なtypeで送られてくるのでtypeによる振り分けが必要
モーダルで「登録」を押した時、 `type = 'view_submission'` でリクエストが飛びます。
しかし、それ以外にもセレクトボックスを更新した場合も `type = 'block_actions'` でリクエストが飛びます。
https://api.slack.com/surfaces/modals/using#response_actions

そのため、typeによってうまくハンドリングしないとエラーになってしまうのでそこんとこうまく書きましょう。

##### モーダルの処理が完了した後は完了メッセージはない？
分からなかった点ですが、普通であれば response_url が返ってくるのですが、モーダルの場合は response_url が返ってこなかったので完了メッセージを送ることが出来ませんでした（詳しい方教えて下さい）
完了メッセージがないのは気持ち悪いので、今回は [chat.postEphemeral](https://api.slack.com/methods/chat.postEphemeral) を使ってレスポンスを返しました。

```php
$url = 'https://slack.com/api/chat.postEphemeral';
```

## 積極的に綺麗なモーダルを作って行こうな
以前は [dialog.open](https://api.slack.com/methods/dialog.open) というAPIを使ってダイアログを表示するのが主流でしたが、新しい機能として **modal** が作られ、まだまだ参考資料が少ない状態です。
複数モーダルに渡るパラメータの入力なども可能なので、今回の記事は本当に基礎の基礎のような記事です。
この記事を参考にもっと色々な使い方が増えていってほしいと思います。

[Twitter](https://twitter.com/BellTreeSzk) もやっているので、なにかありましたらフォロー＆アドバイス頂けると嬉しいです

ここまで読んで頂きありがとうございました〜〜