やりたいこと
slackのモーダルからlambda関数を実行できるようにします
やったこと
今回のコードでは下記アプローチでモーダルを起動できるようにします。
- グローバルショートカットで起動
- 全て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
事前準備
- Interactive Components
- slackアプリの設定のInteractivity & Shortcutsを有効にして、RequestURLを取得。
ショートカット起動時とモーダル操作時に通信がそのURLに飛んでくるので、API Gateway -> Lambdaでその通信を受け取れるようにしておく
- slackアプリの設定のInteractivity & Shortcutsを有効にして、RequestURLを取得。
- Permissions
- botユーザのトークンが必要なので、有効化しておく。モーダル表示は権限なしで実行可能なので、それ以外のタスクで必要なものだけ有効にしておく。
- 例: 最後に何かしら投稿させたいなら chat:write とか。
- Bots
- Permissionsを有効にした時点で有効になってるはず。。。
コードについて
viewの作り方
公式のwebツールを見ながら生成すれば問題なく作れるはず
https://app.slack.com/block-kit-builder/
データの取得の仕方
payloadの中身を見れば下記がわかるので、条件分岐や値の保持に使う。
結構ネストが深いところにあるので頑張る
- どの操作が成された時の通信かを把握する
- stateを見てモーダルの選択状況を取得しているか把握する
個人的なハマりポイント
- 全ての操作に対して通信が飛んでくるので、不要な通信を無視するコード書く必要がある
- モーダル閉じるときは飛んでこない
- モーダルに備わっている送信ボタンに対して、views.updateを適用してはいけない
- モーダルが閉じちゃうので、うんともすんとも言わなくなる
- 送信ボタンは最後に押させるようなフローにするべき
- なお、views.pushも使えない