5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

今回作るもの

/modal というSlashコマンドを実行すると Block Kit Builder で作られたモーダルを表示して、入力された値をサーバーに送るSlackアプリを作ります。
ezgif.com-video-to-gif (3).gif

参考資料

開発環境

  • PHP 7.2 (Laravel 6.16)
  • GAE Standard (外部からアクセスできるサーバーであればなんでも可)
  • Slack (フリープランでも可)

実装方法

Slackアプリの用意

モーダルを出す方法がメインなので細かくは書きません。
細かい部分は 運用中のブログ にまとめてるので気になる方はご覧ください〜

アプリの作成

https://api.slack.com/apps から Create New App を選択して作っていきます。
slack-app-create.jpg

Slashコマンドの登録

適当にSlashコマンドを登録します。
slash-install.jpg

権限の設定

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

interactivity の設定

ダイアログに値を入力された後、その値が送られる向き先の設定をする必要があります。
interactivity.jpg

ワークスペースにインストール

Install App からインストールします。
表示に従っていればインストールできると思いますが、一点初回インストールの場合は下記のApp Display Name を設定しないと「インストールできるボットユーザーがありません」と怒られてしまいます。
apphome.jpg

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

コーディング

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

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

Route::post('/modal', 'SlackController@modal')->name('modal');

な感じでSlackControllerにリクエストが飛んだ後の処理を書いていきます

SlackController.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
                    ]
                ]
            ]
        ];
    }

ちなみに以下のライブラリを使用しています。
GuzzleHttp
Google StackDriver Logging

メイン処理ですが Slack API の view.open というAPIを利用しています。

$url = 'https://slack.com/api/views.open';

ダイアログの中身は Block Kit Builder で作ったものを使っていますが、細かい部分を修正しているので紹介します。

block_id と action_id は設定しておいたほうがいい

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

optional の場所を間違えないで

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

正しく view.open が実行されていれば /modal を実行した時にダイアログは表示されるようになっているはずです。

モーダル経由で入力値を送られてきたときの処理

Route::post('/interactiveMessage', 'SlackController@interactiveMessage')->name('interactiveMessage');

な感じでSlackControllerにリクエストが飛んだ後の処理を書いていきます

SlackController.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;
    
}

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

早期レスポンスをしておいたほうが良い
// 早期レスポンス
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 を使ってレスポンスを返しました。

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

積極的に綺麗なモーダルを作って行こうな

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

Twitter もやっているので、なにかありましたらフォロー&アドバイス頂けると嬉しいです

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

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?