文系エンジニアの勉強記録

文系エンジニアが日々勉強していることを備忘録として投稿していきます

GoogleAppScriptを使ってSlackAppを作成する。

どうも、GoogleAppScriptにハマり始めた社会人二年目文系エンジニアです。 今回はGoogleAppScriptを使用し、Slack内で使用するアプリ(SlackApp)を作成しようと思います。 今回はSlack内のショートカットからModalを出すところまで実装したいと思います。

ここで扱わない項目

Slack側の設定(別途投稿する予定) claspの使い方、Install方法

前提条件

  1. 以下Slack側の設定が終わっている
  2. SlackAppの新規作成
  3. InteractiveComponentsの設定でInteractivityをOnにする。
  4. Shortcutの設定

  5. claspがインストールされている

作成するModal

書籍のレビューを投稿するModalを作成していきます。 ModalはSlack内のShortcutから表示します。 作成するModalのイメージはこんな感じです。

f:id:wattanX:20201024193949p:plain

使用技術

  • TypeScript
  • Clasp
  • GoogleAppScript
  • SlackApp

ModalのPayload作成

Modalを表示するためには、Slackからのリクエストに対してView情報(Payload)をResponseとして返す必要があります。 View情報には以下の項目があります。

  • Blocks 入力項目のコンポーネント 作成するModalのイメージでいうと以下の部分。 f:id:wattanX:20201024194011p:plain

  • Block elements ボタンやプルダウンのような部品 作成するModalのイメージでいうと以下の部分。

f:id:wattanX:20201024194033p:plain

  • Composition objects BlockやBlock elementsに組み合わせて使う単純なJSONオブジェクト LabelやPlaceholder等に利用される。

これらを組み合わせてView情報をResponseとして返すことでModalを表示できるようになっています。 View情報はJSON形式になっており、Slack Block-kit-builderを使用することで簡単に作成ができるようになっています。

block-kit-builder

作成するModalをBlock-kit-builderで作成すると以下のPayloadが作成されます。

{
    "type": "modal",
    "title": {
        "type": "plain_text",
        "text": "Test",
        "emoji": true
    },
    "submit": {
        "type": "plain_text",
        "text": "投稿する",
        "emoji": true
    },
    "close": {
        "type": "plain_text",
        "text": "キャンセル",
        "emoji": true
    },
    "blocks": [
        {
            "type": "divider"
        },
        {
            "type": "input",
            "block_id": "title",
            "element": {
                "type": "plain_text_input",
                "action_id": "title_id"
            },
            "label": {
                "type": "plain_text",
                "text": "タイトル",
                "emoji": true
            }
        },
        {
            "type": "divider"
        },
        {
            "type": "input",
            "block_id": "select_review",
            "element": {
                "type": "static_select",
                "action_id": "selected_id",
                "placeholder": {
                    "type": "plain_text",
                    "text": "Select an item",
                    "emoji": true
                },
                "options": [
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "★★★★★",
                            "emoji": true
                        },
                        "value": "5"
                    },
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "★★★★☆",
                            "emoji": true
                        },
                        "value": "4"
                    },
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "★★★☆☆",
                            "emoji": true
                        },
                        "value": "3"
                    },
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "★★☆☆☆",
                            "emoji": true
                        },
                        "value": "2"
                    },
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "★☆☆☆☆",
                            "emoji": true
                        },
                        "value": "1"
                    }
                ]
            },
            "label": {
                "type": "plain_text",
                "text": "評価",
                "emoji": true
            }
        },
        {
            "type": "input",
            "block_id": "review",
            "element": {
                "type": "plain_text_input",
                "action_id": "review_id",
                "multiline": true
            },
            "label": {
                "type": "plain_text",
                "text": "レビュー",
                "emoji": true
            }
        }
    ]
}

上記のPayloadをResponseとして返すように実装すると以下のようになります。

View情報をSlackに返す

WebhookUrlやSlackのAccessTokenはGoogleAppScript内の「スクリプトのプロパティ」に定義する。

export class Properties {
    private webhookURl: string;
    private accessToken: string;

    constructor() {
        this.webhookURl = PropertiesService.getScriptProperties().getProperty('WEB_HOOK_URL');
        this.accessToken = PropertiesService.getScriptProperties().getProperty('SLACK_ACCESS_TOKEN');
    }

    public GetAccessToken(): string {
        return this.accessToken;
    }

    public GetWebhookUrl(): string{
        return this.webhookURl;
    }
}
function doPost(e) {
    let params = e.parameter;
    let data = params.payload;
    let json = JSON.parse(decodeURIComponent(data));
    let trigger_id = json.trigger_id;
    let properties = new Properties();
    if (json.type === 'shortcut') {
        let url = 'https://slack.com/api/views.open';
        let token = properties.GetAccessToken();
        let viewData = {
            "trigger_id": trigger_id,
            "token": token,
            "view": JSON.stringify({
                "type": "modal",
                "callback_id": "test",
                "title": {
                    "type": "plain_text",
                    "text": "Test",
                    "emoji": true
                },
                "submit": {
                    "type": "plain_text",
                    "text": "投稿する",
                    "emoji": true
                },
                "close": {
                    "type": "plain_text",
                    "text": "キャンセル",
                    "emoji": true
                },
                "blocks": [
                    {
                        "type": "divider"
                    },
                    {
                        "type": "input",
                        "block_id": "title",
                        "element": {
                            "type": "plain_text_input",
                            "action_id": "title_id"
                        },
                        "label": {
                            "type": "plain_text",
                            "text": "タイトル",
                            "emoji": true
                        }
                    },
                    {
                        "type": "divider"
                    },
                    {
                        "type": "input",
                        "block_id": "select_review",
                        "element": {
                            "type": "static_select",
                            "action_id": "selected_id",
                            "placeholder": {
                                "type": "plain_text",
                                "text": "Select an item",
                                "emoji": true
                            },
                            "options": [
                                {
                                    "text": {
                                        "type": "plain_text",
                                        "text": "★★★★★",
                                        "emoji": true
                                    },
                                    "value": "5"
                                },
                                {
                                    "text": {
                                        "type": "plain_text",
                                        "text": "★★★★☆",
                                        "emoji": true
                                    },
                                    "value": "4"
                                },
                                {
                                    "text": {
                                        "type": "plain_text",
                                        "text": "★★★☆☆",
                                        "emoji": true
                                    },
                                    "value": "3"
                                },
                                {
                                    "text": {
                                        "type": "plain_text",
                                        "text": "★★☆☆☆",
                                        "emoji": true
                                    },
                                    "value": "2"
                                },
                                {
                                    "text": {
                                        "type": "plain_text",
                                        "text": "★☆☆☆☆",
                                        "emoji": true
                                    },
                                    "value": "1"
                                }
                            ]
                        },
                        "label": {
                            "type": "plain_text",
                            "text": "評価",
                            "emoji": true
                        }
                    },
                    {
                        "type": "input",
                        "block_id": "review",
                        "element": {
                            "type": "plain_text_input",
                            "action_id": "review_id",
                            "multiline": true
                        },
                        "label": {
                            "type": "plain_text",
                            "text": "レビュー",
                            "emoji": true
                        }
                    }
                ]
            })
        };
        let header = {
            "Authorization": "Bearer" + token
        };
        let options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
            method: "post",
            headers: header,
            payload: viewData
        };
        UrlFetchApp.fetch(url, options);
        return ContentService.createTextOutput();
    }
    else if (json.type === 'view_submission') {
        // 次回投稿予定
    }
}

ここまででModalの表示までできるようになっているはずです。 Modalから実際にレビューを投稿できるようにするには、もう少し処理が必要なので次回投稿します。