この記事は、リレーブログ企画「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のコードを書いていきます。 cd /(任意のディレクトリ) pip install --target ./package slack_bolt cd package touch lambda_function.py コードは以下をコピペして貼り付けましょう。 コードの @app.command(“/sport_start”) 部分にある通り、Slackのスラッシュコマンド(/sport_start)で起動するようになっています。 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)ではなく、目的に応じたものに変更すると使いやすくなると思います。 質問と選択肢も自分が必要としているものに変更しましょう! 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はほとんど触ったことがなかったので、大変でした。 今回、触ったことにより少しは詳しくなれたと思います。 メッセージを送信するだけではなく、スプレッドシートに記載する機能やフォームを変更するなどの他の機能を追加できると便利になるので引き続き勉強していこうと思います。 ありがとうございました。 次回は、佐藤さんです。 どんな記事になるのかワクワクですね♪ ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する