ãã®èšäºã¯ããªã¬ãŒããã°äŒç»ã24åãªã¬ãŒããã°ãã®èšäºã§ãã ã¯ããã« ã¯ãããŸããŠã æ°å1幎ç®ã®åŸè€ã§ãã æ¥åã®åãåãã察å¿ã«Slackã®ã¯ãŒã¯ãããŒãå©çšããŠããŸããã åãåããå
容ããšã«ã¯ãŒã¯ãããŒãäœæããŠãããããæ°ãå€ããªã£ãŠããŸãã ããã§ãè€æ°ã®ã¯ãŒã¯ãããŒãïŒã€ã«ãŸãšããããã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ã§ ãããã£ã°ã«ãŒãã«åå ãã