この記事は、リレーブログ企画「24卒リレーブログ」の記事です。
はじめに
はじめまして。
新卒1年目の後藤です。
業務の問い合わせ対応にSlackのワークフローを利用していますが、 問い合わせ内容ごとにワークフローを作成しているため、数が多くなっています。
そこで、複数のワークフローを1つにまとめるため、Slack Bolt, AWS Lambdaで条件分岐するワークフローを作ってみました。
Slackのワークフローで条件分岐があったらいいなと思いました。
公式では以下のように記載されています。
ワークフローに条件つきロジックを作成できますか? 現時点では、ワークフロービルダーで条件つきロジックは作成できません。より複雑なロジックを実行するには、カスタムファンクションを使って Slack アプリを作成する必要があります。
引用:https://slack.com/intl/ja-jp/help/articles/26800170438419
つまり、デフォルトの機能では存在していないということになります。
そこで、Slack Boltを利用しようと考えました。
Slack Boltとは、Slack アプリ開発のための公式フレームワークです。
JavaScript (Node.js), Python, Java で利用することができます。
Bolt 入門ガイドに、詳しくたくさん載っています。
以上のSlack Boltを用いることで、条件分岐ワークフローを実現できるSlackアプリを作成することが可能になります。
早速作っていきます!
Slack APIの設定 Part1
まず、最初に取り掛かるのはSlack APIの作成です。
Slack APIとは、独自のアプリケーションをSlackに導入するために作るアプリです。
Slack APIのYour Appsページ右上のCreate New Appをクリックします。
上のFrom a manifestを選択し、アプリをインストールするワークスペースを指定します。
Nextを二回選択し、Createをクリックします。
Basic Informationの下部へ行き、App nameにアプリの名前、Short descriptionにアプリの説明、Background colorで背景色を選択します。
その後、右下のSave Changesをクリックします。
以下の画像のように、左側のOAuth & Permissions内にあるScopesのAdd an OAuth Scopeからchat:writeとcommandsを追加します。
左側のメニューでIncoming Webhooksを選択し、Onにします。
OAuth & Permissions上部のOAuth TokensのRequest to Installをクリックし、コメントを記入し、Submit Requestで送信します。
承認を待ちます。
承知後、OAuth & Permissions内にあるInstall to ~を選択し、使用するワークスペースを選択します。
その後、Bot User OAuth Token(xbxo-hogehoge)が必要になるのでメモしてください。
また、左側のメニューで一番上のBasic Informationに遷移し、Signing Secretも必要になるのでShowを押してメモしてください。
メモした2つはAWS Lambdaの設定で必要になります。
一旦ここでSlack APIの設定はストップです。
Slack Bolt
ここからはSlack Boltについて説明していきます。
まずは任意のディレクトリにpipコマンドを利用してSlack Bolt をインストールし、packageフォルダを作成します。
そのフォルダをvscodeなどで開きます。
今回はPythonを使用するのでlambda_function.pyという名前でファイルを作成します。
このファイルにLambdaのコードを書いていきます。
1 2 3 4 |
cd /(任意のディレクトリ) pip install --target ./package slack_bolt cd package touch lambda_function.py |
コードは以下をコピペして貼り付けましょう。
コードの@app.command(“/sport_start”)部分にある通り、Slackのスラッシュコマンド(/sport_start)で起動するようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
import os from slack_bolt import App from slack_bolt.adapter.aws_lambda import SlackRequestHandler app = App( signing_secret=os.environ["SLACK_SIGNING_SECRET"], token=os.environ["SLACK_BOT_TOKEN"], process_before_response=True, ) SPORTS = { "soccer": { "name": "サッカー", "players": [ "サッカーマン1", "サッカーマン2", "サッカーマン3", "サッカーマン4" ] }, "baseball": { "name": "野球", "players": [ "野球マン1", "野球マン2" ] }, "basketball": { "name": "バスケットボール", "players": [ "バスケマン1", "バスケマン2", "バスケマン3" ] } } def create_modal(user_id, channel_id, selected_sport=None): blocks = [ { "type": "section", "block_id": "sport_select", "text": {"type": "mrkdwn", "text": "スポーツを選んでください。"}, "accessory": { "type": "static_select", "action_id": "sport_select", "placeholder": {"type": "plain_text", "text": "スポーツを選択"}, "options": [ {"text": {"type": "plain_text", "text": sport["name"]}, "value": key} for key, sport in SPORTS.items() ] } } ] if selected_sport: blocks.append({ "type": "section", "block_id": "player_select", "text": {"type": "mrkdwn", "text": "選手を選んでください。"}, "accessory": { "type": "static_select", "action_id": "player_select", "placeholder": {"type": "plain_text", "text": "選手を選択"}, "options": [ {"text": {"type": "plain_text", "text": player}, "value": player} for player in SPORTS[selected_sport]["players"] ] } }) return { "type": "modal", "callback_id": "sport_player_modal", "private_metadata": f"{user_id}:{channel_id}", "title": {"type": "plain_text", "text": "スポーツと選手選択"}, "blocks": blocks, "submit": {"type": "plain_text", "text": "送信"} } @app.command("/sport_start") def ask_for_sport(ack, body, client): ack() client.views_open( trigger_id=body["trigger_id"], view=create_modal(body["user_id"], body["channel_id"]) ) @app.action("sport_select") def update_player_options(ack, body, client): ack() selected_sport = body["actions"][0]["selected_option"]["value"] user_id, channel_id = body["view"]["private_metadata"].split(":") client.views_update( view_id=body["view"]["id"], view=create_modal(user_id, channel_id, selected_sport) ) @app.action("player_select") def handle_player_select(ack, body, logger): ack() logger.info(body) @app.view("sport_player_modal") def handle_submission(ack, body, client, view, say): ack() try: user_id, channel_id = view["private_metadata"].split(":") selected_sport = view["state"]["values"]["sport_select"]["sport_select"]["selected_option"]["value"] selected_player = view["state"]["values"]["player_select"]["player_select"]["selected_option"]["value"] message = f"<@{user_id}>さんが好きなスポーツは{SPORTS[selected_sport]['name']}で、好きな選手は{selected_player}です。" say(text=message, channel=channel_id) except Exception as e: print(f"Error in handle_submission: {str(e)}") def lambda_handler(event, context): slack_handler = SlackRequestHandler(app=app) return slack_handler.handle(event, context) |
Lambdaのコードを作成したら、エクスプローラーなどで、作成したものをzipファイルに固めます。
ここまででSlack Boltはおしまいです。
コードの変更はAWS Lambdaでもできるので変更したい場合はあとでも大丈夫です。
AWS Lambda
AWS Lambdaのページに行き、関数を作成します。
以下画像のように設定を行い、右下の関数の作成を押すと関数が作成されます。
※hogehogeは関数名なので各自適した名前にしてください。
作成した関数を選択し、以下画像の右下の黄色の部分の.zipファイルをクリックし、先ほどzip化したものをアップロードし保存をクリックします。
すると、コードが展開されます。
次にコードではなく設定の関数URLを開きます。
関数URLを作成をクリックします。
NONEを選択して保存しましょう。
NONEは誰からでもアクセス可能なため、サービス運用には向いていません。
サービス運用する場合は認証された呼び出し元のみがアクセス可能なAWS_IAMを選択しましょう。
※NONEとAWS_IAMについてはLambda 関数 URL へのアクセスの制御で詳しく説明されているのでそちらを参考にしてください。
作成した関数URLをメモしておきましょう。
「環境変数」を選択し、「編集」をクリックします。
環境変数の追加を選択すると増やすことが出来るので2つ追加します。
コードの中にSLACK_SIGNING_SECRETとSLACK_BOT_TOKENがあるので、それらの設定をします。
キーと値は以下のものを記載します。
キー | 値 |
SLACK_SIGNING_SECRET | Slack APIの設定でメモしたBasic InformationのSigning Secret |
SLACK_BOT_TOKEN | Slack APIの設定でメモしたOAuth & PermissionsのBot User OAuth Token |
これでAWS Lambdaの設定は終わりです!
Slack APIの設定 Part2
左側のメニューでInteractivity & Shortcutsを選択し、OffをOnにします。
Request URLに先ほどメモした関数URLを記入します。
その後、右下のSave Changesをクリックします。
左側のメニューでSlash Commandsを選択してCreate New Commandをクリックします。
以下の画像ように入力します。
Commandは(/sport_start)です。
Request URLは先ほどAWS Lambdaでメモした関数URLです。
Short Descriptionには説明を書いておきましょう。
右下のsaveで保存します。
Slack
最後にSlackを開いてこのアプリを追加したチャンネルに行きます。
追加したチャンネルのインテグレーションに追加したAppがあるか確認してください。
無い場合は、Slack画面左側の…(その他)の自動化を選択し、Appで作成したアプリ名を検索します。
作成したアプリを選択し、画面上部のアプリ名をクリックするとチャンネルにこのアプリを追加するがあるのでこちらでチャンネルにアプリを追加してください。
作成したSlackのスラッシュコマンド(/sport_start)をSlackのチャットに入力すると以下のようになります。
送信を押すとメッセージが送信されました!
応用編
以下のコードのように選択肢をどんどん追加することができます。
主な追加箇所は以下です。
・SPORTSにポジションをそれぞれ追加
・if selected_playerを追加
・@app.action(“player_select”)を追加
また、スラッシュコマンドも(/sport_start)ではなく、目的に応じたものに変更すると使いやすくなると思います。
質問と選択肢も自分が必要としているものに変更しましょう!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
import os from slack_bolt import App from slack_bolt.adapter.aws_lambda import SlackRequestHandler app = App( signing_secret=os.environ["SLACK_SIGNING_SECRET"], token=os.environ["SLACK_BOT_TOKEN"], process_before_response=True, ) SPORTS = { "soccer": { "name": "サッカー", "players": { "サッカーマン1": ["FW", "MF"], "サッカーマン2": ["DF", "GK"], "サッカーマン3": ["MF", "DF","GK"], "サッカーマン4": ["FW", "MF"] } }, "baseball": { "name": "野球", "players": { "野球マン1": ["ピッチャー","ファースト"], "野球マン2": ["キャッチャー","ショート","セカンド"] } }, "basketball": { "name": "バスケットボール", "players": { "バスケマン1": ["PG","SG"], "バスケマン2": ["SF","PF"], "バスケマン3": ["C","PG","SF"] } } } def create_modal(user_id, channel_id, selected_sport=None, selected_player=None): blocks = [ { "type": "section", "block_id": "sport_select", "text": {"type": "mrkdwn", "text": "スポーツを選んでください。"}, "accessory": { "type": "static_select", "action_id": "sport_select", "placeholder": {"type": "plain_text", "text": "スポーツを選択"}, "options": [ {"text": {"type": "plain_text", "text": sport["name"]}, "value": key} for key, sport in SPORTS.items() ] } } ] if selected_sport: blocks.append({ "type": "section", "block_id": "player_select", "text": {"type": "mrkdwn", "text": "選手を選んでください。"}, "accessory": { "type": "static_select", "action_id": "player_select", "placeholder": {"type": "plain_text", "text": "選手を選択"}, "options": [ {"text": {"type": "plain_text", "text": player}, "value": player} for player in SPORTS[selected_sport]["players"].keys() ] } }) if selected_player: blocks.append({ "type": "section", "block_id": "position_select", "text": {"type": "mrkdwn", "text": "ポジションを選んでください。"}, "accessory": { "type": "static_select", "action_id": "position_select", "placeholder": {"type": "plain_text", "text": "ポジションを選択"}, "options": [ {"text": {"type": "plain_text", "text": position}, "value": position} for position in SPORTS[selected_sport]["players"][selected_player] ] } }) return { "type": "modal", "callback_id": "sport_player_position_modal", "private_metadata": f"{user_id}:{channel_id}:{selected_sport or ''}:{selected_player or ''}", "title": {"type": "plain_text", "text": "スポーツと選手とポジション選択"}, "blocks": blocks, "submit": {"type": "plain_text", "text": "送信"} } @app.command("/sport_start") def ask_for_sport(ack, body, client): ack() client.views_open( trigger_id=body["trigger_id"], view=create_modal(body["user_id"], body["channel_id"]) ) @app.action("sport_select") def update_player_options(ack, body, client): ack() selected_sport = body["actions"][0]["selected_option"]["value"] user_id, channel_id, _, _ = body["view"]["private_metadata"].split(":") client.views_update( view_id=body["view"]["id"], view=create_modal(user_id, channel_id, selected_sport) ) @app.action("player_select") def update_position_options(ack, body, client): ack() selected_player = body["actions"][0]["selected_option"]["value"] user_id, channel_id, selected_sport, _ = body["view"]["private_metadata"].split(":") client.views_update( view_id=body["view"]["id"], view=create_modal(user_id, channel_id, selected_sport, selected_player) ) @app.action("position_select") def handle_position_select(ack, body, logger): ack() logger.info(body) @app.view("sport_player_position_modal") def handle_submission(ack, body, client, view, say): ack() try: user_id, channel_id, selected_sport, _ = view["private_metadata"].split(":") selected_player = view["state"]["values"]["player_select"]["player_select"]["selected_option"]["value"] selected_position = view["state"]["values"]["position_select"]["position_select"]["selected_option"]["value"] message = f"<@{user_id}>さんが選んだスポーツは{SPORTS[selected_sport]['name']}で、選んだ選手は{selected_player}、選んだポジションは{selected_position}です。" say(text=message, channel=channel_id) except Exception as e: print(f"Error in handle_submission: {str(e)}") def lambda_handler(event, context): slack_handler = SlackRequestHandler(app=app) return slack_handler.handle(event, context) |
おわりに
今回、Slackに条件分岐ワークフローを実装しました。
調べてみても出てこなかったので一から作ってみました。
Slack BoltとAWS Lambdaはほとんど触ったことがなかったので、大変でした。
今回、触ったことにより少しは詳しくなれたと思います。
メッセージを送信するだけではなく、スプレッドシートに記載する機能やフォームを変更するなどの他の機能を追加できると便利になるので引き続き勉強していこうと思います。
ありがとうございました。
次回は、佐藤さんです。
どんな記事になるのかワクワクですね♪