0
1

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のモーダルからlambda関数を実行できるようにする

Posted at

やりたいこと

slackのモーダルからlambda関数を実行できるようにします
out.gif

やったこと

今回のコードでは下記アプローチでモーダルを起動できるようにします。

  • グローバルショートカットで起動
  • 全てAPIを投げることで実行
    • views.openとviews.update(とchat.postmessage)
  • views.pushではなく、views.updateを使う
    • pushの場合、複数のダイアログを開いたときに、閉じる操作を明示的に行う必要があり手間
    • というかダイアログを閉じさせるやり方がよくわからなかった。。。

# コード

import base64
import json
import os
import urllib.parse
import boto3
import requests

headers = {
    "content-type": "application/json",
    "Authorization": f"Bearer {os.environ['token']}",
}


def post_text(text):

    r = requests.post(
        url="https://slack.com/api/chat.postMessage",
        headers=headers,
        json={"channel": "#general", "text": text},
    )
    return r


def open_view(trigger_id, view):

    r = requests.post(
        url="https://slack.com/api/views.open",
        headers=headers,
        json={"trigger_id": trigger_id, "view": view},
    )
    return r


def update_view(hash, view_id, view):

    r = requests.post(
        url="https://slack.com/api/views.update",
        headers=headers,
        json={"hash": hash, "view": view, "view_id": view_id},
    )
    return r


def parse_value(d):
    if "selected_option" in d:
        return d["selected_option"]["value"]
    elif "selected_date" in d:
        return d["selected_date"]


def handler(event, context):
    decoded = base64.b64decode(event["body"]).decode()
    param = urllib.parse.parse_qs(decoded)
    payload = json.loads(param["payload"][0])
    print(payload)

    if payload["type"] == "shortcut":
        # ショートカット起動時

        # userごとに実行可能なタスクを制御したい場合はuser_idが使えそう
        user_id = payload["user"]["id"]

        view = {
            "type": "modal",
            "callback_id": "modal-select-task",
            "title": {"type": "plain_text", "text": "実行するタスクを選択"},
            "blocks": [
                {
                    "type": "section",
                    "block_id": "ecs-task-block",
                    "text": {"type": "mrkdwn", "text": "select action"},
                    "accessory": {
                        "type": "static_select",
                        "placeholder": {"type": "plain_text", "text": "Select an item"},
                        "options": [
                            {
                                "text": {"type": "plain_text", "text": "taskA"},
                                "value": "taskA",
                            },
                            {
                                "text": {"type": "plain_text", "text": "taskB"},
                                "value": "taskB",
                            },
                            {
                                "text": {"type": "plain_text", "text": "taskC"},
                                "value": "taskC",
                            },
                        ],
                        "action_id": "select-task",
                    },
                }
            ],
        }
        trigger_id = payload["trigger_id"]
        r = open_view(trigger_id, view)
    elif (
        payload["type"] == "block_actions"
        and payload["actions"][0]["action_id"] == "select-task"
    ):
        # タスクを選んだ時
        select = payload["actions"][0]["selected_option"]["value"]
        view = {
            "type": "modal",
            "callback_id": "modal-parameter-task",
            "title": {"type": "plain_text", "text": "パラメータを指定してください"},
            "submit": {"type": "plain_text", "text": "選択"},
            "blocks": [
                {
                    "type": "section",
                    "text": {"type": "mrkdwn", "text": f"実行タスク: `{select}`"},
                },
                {
                    "type": "section",
                    "block_id": "date",
                    "text": {"type": "mrkdwn", "text": "date"},
                    "accessory": {"type": "datepicker", "action_id": "date"},
                },
                {
                    "type": "section",
                    "block_id": "parameter1",
                    "text": {
                        "type": "mrkdwn",
                        "text": "Pick an item from the dropdown list",
                    },
                    "accessory": {
                        "type": "static_select",
                        "placeholder": {"type": "plain_text", "text": "Select an item"},
                        "options": [
                            {
                                "text": {"type": "plain_text", "text": "value-0"},
                                "value": "value-0",
                            },
                            {
                                "text": {"type": "plain_text", "text": "value-1"},
                                "value": "value-1",
                            },
                            {
                                "text": {"type": "plain_text", "text": "value-2"},
                                "value": "value-2",
                            },
                        ],
                        "action_id": "parameter1",
                    },
                },
            ],
        }
        trigger_id = payload["trigger_id"]
        view_id = payload["container"]["view_id"]
        hash = payload["view"]["hash"]
        r = update_view(hash, view_id, view)
    elif payload["type"] == "view_submission":
        # 送信を押した時
        user_id = payload["user"]["id"]
        values = payload["view"]["state"]["values"]

        # block-idとaction-idが一致している前提のコード
        params = {k: parse_value(values[k][k]) for k in values.keys()}

        # この辺りに実行したい内容を記載 

        # 実行ユーザの情報を投稿
        text = f"""実行者: <@{user_id}>
{json.dumps(params, indent=2, ensure_ascii=False)}
"""
        r = post_text(text)
    else:
        # 他の通信は無視
        r = None

    if r:
        print(r.text)

    return {"statusCode": 200}

説明

Lambda関連

上記のコードはAWS Lambda上で実行させることを前提にしています。
外部ライブラリ(requests)を使用しているため、zipでアップロードするかECR経由でコンテナとしてアップロードする必要があります。

筆者は後者のやり方を利用しました。
この辺りは解説記事等があるかと思いますので省略し、Dockerfileだけ記載しておきます。

FROM public.ecr.aws/lambda/python:3.8
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py ./
CMD ["app.handler"]

slack関連

前提

まずは公式のドキュメントを読むことをお勧めします
https://api.slack.com/surfaces/modals/using#pushing_response

事前準備

スクリーンショット 2021-09-11 15.29.06.png

  • Interactive Components
    • slackアプリの設定のInteractivity & Shortcutsを有効にして、RequestURLを取得。
      ショートカット起動時とモーダル操作時に通信がそのURLに飛んでくるので、API Gateway -> Lambdaでその通信を受け取れるようにしておく
  • Permissions
    • botユーザのトークンが必要なので、有効化しておく。モーダル表示は権限なしで実行可能なので、それ以外のタスクで必要なものだけ有効にしておく。
    • 例: 最後に何かしら投稿させたいなら chat:write とか。
  • Bots
    • Permissionsを有効にした時点で有効になってるはず。。。

コードについて

viewの作り方

公式のwebツールを見ながら生成すれば問題なく作れるはず
https://app.slack.com/block-kit-builder/

データの取得の仕方

payloadの中身を見れば下記がわかるので、条件分岐や値の保持に使う。
結構ネストが深いところにあるので頑張る

  • どの操作が成された時の通信かを把握する
  • stateを見てモーダルの選択状況を取得しているか把握する

個人的なハマりポイント

  • 全ての操作に対して通信が飛んでくるので、不要な通信を無視するコード書く必要がある
    • モーダル閉じるときは飛んでこない
  • モーダルに備わっている送信ボタンに対して、views.updateを適用してはいけない
    • モーダルが閉じちゃうので、うんともすんとも言わなくなる
    • 送信ボタンは最後に押させるようなフローにするべき
    • なお、views.pushも使えない
0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?