From 2785e14f7551257c9048804db088656eccae5e90 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sun, 2 May 2021 18:45:29 +0900 Subject: [PATCH 1/2] Add missing listener suggestion to the default unhandled error message --- slack_bolt/app/app.py | 2 - slack_bolt/app/async_app.py | 2 - slack_bolt/logger/messages.py | 120 ++- tests/slack_bolt/logger/__init__.py | 0 .../logger/test_unmatched_suggestions.py | 729 ++++++++++++++++++ 5 files changed, 847 insertions(+), 6 deletions(-) create mode 100644 tests/slack_bolt/logger/__init__.py create mode 100644 tests/slack_bolt/logger/test_unmatched_suggestions.py diff --git a/slack_bolt/app/app.py b/slack_bolt/app/app.py index 2bf8213cb..e4c6a920d 100644 --- a/slack_bolt/app/app.py +++ b/slack_bolt/app/app.py @@ -530,8 +530,6 @@ def middleware_next(): def _handle_unmatched_requests( self, req: BoltRequest, resp: BoltResponse ) -> BoltResponse: - # TODO: provide more info like suggestion of listeners - # e.g., You can handle this type of message with @app.event("app_mention") self._framework_logger.warning(warning_unhandled_request(req)) return resp diff --git a/slack_bolt/app/async_app.py b/slack_bolt/app/async_app.py index 131650262..8deac5f76 100644 --- a/slack_bolt/app/async_app.py +++ b/slack_bolt/app/async_app.py @@ -586,8 +586,6 @@ async def async_middleware_next(): def _handle_unmatched_requests( self, req: AsyncBoltRequest, resp: BoltResponse ) -> BoltResponse: - # TODO: provide more info like suggestion of listeners - # e.g., You can handle this type of message with @app.event("app_mention") self._framework_logger.warning(warning_unhandled_request(req)) return resp diff --git a/slack_bolt/logger/messages.py b/slack_bolt/logger/messages.py index 294997a65..6235765ae 100644 --- a/slack_bolt/logger/messages.py +++ b/slack_bolt/logger/messages.py @@ -4,7 +4,17 @@ from slack_sdk.web import SlackResponse from slack_bolt.request import BoltRequest - +from slack_bolt.request.payload_utils import ( + is_action, + is_event, + is_options, + is_shortcut, + is_slash_command, + is_view, + is_workflow_step_edit, + is_workflow_step_save, + is_workflow_step_execute, +) # ------------------------------- # Error @@ -94,10 +104,116 @@ def warning_unhandled_by_global_middleware( # type: ignore ) +_unhandled_request_suggestion_prefix = """ +--- +[Suggestion] You can handle this type of event with the following listener function: +""" + + +def _build_unhandled_request_suggestion(default_message: str, code_snippet: str): + return f"""{default_message}{_unhandled_request_suggestion_prefix}{code_snippet}""" + + def warning_unhandled_request( # type: ignore req: Union[BoltRequest, "AsyncBoltRequest"], # type: ignore ) -> str: # type: ignore - return f"Unhandled request ({req.body})" + default_message = f"Unhandled request ({req.body})" + if ( + is_workflow_step_edit(req.body) + or is_workflow_step_save(req.body) + or is_workflow_step_execute(req.body) + ): + # @app.step + return _build_unhandled_request_suggestion( + default_message, + f""" +from slack_bolt.workflows.step import WorkflowStep +ws = WorkflowStep( + callback_id="add_task", + edit=edit, + save=save, + execute=execute, +) +# Pass Step to set up listeners +app.step(ws) +""", + ) + if is_action(req.body): + # @app.action + action_id_or_callback_id = req.body.get("callback_id") + if req.body.get("type") == "block_actions": + action_id_or_callback_id = req.body.get("actions")[0].get("action_id") + return _build_unhandled_request_suggestion( + default_message, + f""" +@app.action("{action_id_or_callback_id}") +def handle_some_action(ack, body, logger): + ack() + logger.info(body) +""", + ) + if is_options(req.body): + # @app.options + constraints = '"action-id"' + if req.body.get("action_id") is not None: + constraints = '"' + req.body.get("action_id") + '"' + elif req.body.get("type") == "dialog_suggestion": + constraints = f"""{{"type": "dialog_suggestion", "callback_id": "{req.body.get('callback_id')}"}}""" + return _build_unhandled_request_suggestion( + default_message, + f""" +@app.options({constraints}) +def handle_some_options(ack): + ack(options=[ ... ]) +""", + ) + if is_shortcut(req.body): + # @app.shortcut + id = req.body.get("action_id") or req.body.get("callback_id") + return _build_unhandled_request_suggestion( + default_message, + f""" +@app.shortcut("{id}") +def handle_shortcuts(ack, body, logger): + ack() + logger.info(body) +""", + ) + if is_view(req.body): + # @app.view + return _build_unhandled_request_suggestion( + default_message, + f""" +@app.view("{req.body.get('view', {}).get('callback_id', 'modal-view-id')}") +def handle_view_events(ack, body, logger): + ack() + logger.info(body) +""", + ) + if is_event(req.body): + # @app.event + event_type = req.body.get('event', {}).get('type') + return _build_unhandled_request_suggestion( + default_message, + f""" +@app.event("{event_type}") +def handle_{event_type}_events(body, logger): + logger.info(body) +""", + ) + if is_slash_command(req.body): + # @app.command + command = req.body.get("command", "/your-command") + return _build_unhandled_request_suggestion( + default_message, + f""" +@app.command("{command}") +def handle_some_command(ack, body, logger): + ack() + logger.info(body) +""", + ) + return default_message def warning_did_not_call_ack(listener_name: str) -> str: diff --git a/tests/slack_bolt/logger/__init__.py b/tests/slack_bolt/logger/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_bolt/logger/test_unmatched_suggestions.py b/tests/slack_bolt/logger/test_unmatched_suggestions.py new file mode 100644 index 000000000..83a6173ec --- /dev/null +++ b/tests/slack_bolt/logger/test_unmatched_suggestions.py @@ -0,0 +1,729 @@ +from slack_bolt.request import BoltRequest +from slack_bolt.logger.messages import warning_unhandled_request + + +class TestUnmatchedPatternSuggestions: + def setup_method(self): + pass + + def teardown_method(self): + pass + + def test_unknown_patterns(self): + req: BoltRequest = BoltRequest(body={"type": "foo"}, mode="socket_mode") + message = warning_unhandled_request(req) + assert f"Unhandled request ({req.body})" == message + + def test_block_actions(self): + req: BoltRequest = BoltRequest(body=block_actions, mode="socket_mode") + message = warning_unhandled_request(req) + assert ( + f"""Unhandled request ({req.body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +@app.action("action-id-value") +def handle_some_action(ack, body, logger): + ack() + logger.info(body) +""" + == message + ) + + def test_attachment_actions(self): + req: BoltRequest = BoltRequest(body=attachment_actions, mode="socket_mode") + message = warning_unhandled_request(req) + assert ( + f"""Unhandled request ({req.body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +@app.action("pick_channel_for_fun") +def handle_some_action(ack, body, logger): + ack() + logger.info(body) +""" + == message + ) + + def test_app_mention_event(self): + req: BoltRequest = BoltRequest(body=app_mention_event, mode="socket_mode") + message = warning_unhandled_request(req) + assert ( + f"""Unhandled request ({req.body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +@app.event("app_mention") +def handle_app_mention_events(body, logger): + logger.info(body) +""" + == message + ) + + def test_commands(self): + req: BoltRequest = BoltRequest(body=slash_command, mode="socket_mode") + message = warning_unhandled_request(req) + assert ( + f"""Unhandled request ({req.body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +@app.command("/start-conv") +def handle_some_command(ack, body, logger): + ack() + logger.info(body) +""" + == message + ) + + def test_shortcut(self): + for body in [global_shortcut, message_shortcut]: + req: BoltRequest = BoltRequest(body=body, mode="socket_mode") + message = warning_unhandled_request(req) + assert ( + f"""Unhandled request ({req.body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +@app.shortcut("test-shortcut") +def handle_shortcuts(ack, body, logger): + ack() + logger.info(body) +""" + == message + ) + + def test_view(self): + for body in [view_submission, view_closed]: + req: BoltRequest = BoltRequest(body=body, mode="socket_mode") + message = warning_unhandled_request(req) + assert ( + f"""Unhandled request ({req.body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +@app.view("view-id") +def handle_view_events(ack, body, logger): + ack() + logger.info(body) +""" + == message + ) + + def test_block_suggestion(self): + req: BoltRequest = BoltRequest(body=block_suggestion, mode="socket_mode") + message = warning_unhandled_request(req) + assert ( + f"""Unhandled request ({req.body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +@app.options("the-id") +def handle_some_options(ack): + ack(options=[ ... ]) +""" + == message + ) + + def test_dialog_suggestion(self): + req: BoltRequest = BoltRequest(body=dialog_suggestion, mode="socket_mode") + message = warning_unhandled_request(req) + assert ( + f"""Unhandled request ({req.body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +@app.options({{"type": "dialog_suggestion", "callback_id": "the-id"}}) +def handle_some_options(ack): + ack(options=[ ... ]) +""" + == message + ) + + def test_step(self): + for body in [step_edit_payload, step_save_payload, step_execute_payload]: + req: BoltRequest = BoltRequest(body=body, mode="socket_mode") + message = warning_unhandled_request(req) + assert ( + f"""Unhandled request ({req.body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +from slack_bolt.workflows.step import WorkflowStep +ws = WorkflowStep( + callback_id="add_task", + edit=edit, + save=save, + execute=execute, +) +# Pass Step to set up listeners +app.step(ws) +""" + == message + ) + + +block_actions = { + "type": "block_actions", + "user": { + "id": "W111", + "username": "primary-owner", + "name": "primary-owner", + "team_id": "T111", + }, + "api_app_id": "A111", + "token": "verification_token", + "container": { + "type": "message", + "message_ts": "111.222", + "channel_id": "C111", + "is_ephemeral": True, + }, + "trigger_id": "111.222.valid", + "team": { + "id": "T111", + "domain": "workspace-domain", + "enterprise_id": "E111", + "enterprise_name": "Sandbox Org", + }, + "channel": {"id": "C111", "name": "test-channel"}, + "response_url": "https://hooks.slack.com/actions/T111/111/random-value", + "actions": [ + { + "action_id": "action-id-value", + "block_id": "b", + "text": {"type": "plain_text", "text": "Button", "emoji": True}, + "value": "click_me_123", + "type": "button", + "action_ts": "1596530385.194939", + } + ], +} + +attachment_actions = { + "type": "interactive_message", + "actions": [ + { + "name": "channel_list", + "type": "select", + "selected_options": [{"value": "C111"}], + } + ], + "callback_id": "pick_channel_for_fun", + "team": {"id": "T111", "domain": "hooli-hq"}, + "channel": {"id": "C222", "name": "triage-random"}, + "user": {"id": "U111", "name": "gbelson"}, + "action_ts": "1520966872.245369", + "message_ts": "1520965348.000538", + "attachment_id": "1", + "token": "verification_token", + "is_app_unfurl": True, + "original_message": { + "text": "", + "username": "Belson Bot", + "bot_id": "B111", + "attachments": [ + { + "callback_id": "pick_channel_for_fun", + "text": "Choose a channel", + "id": 1, + "color": "2b72cb", + "actions": [ + { + "id": "1", + "name": "channel_list", + "text": "Public channels", + "type": "select", + "data_source": "channels", + } + ], + "fallback": "Choose a channel", + } + ], + "type": "message", + "subtype": "bot_message", + "ts": "1520965348.000538", + }, + "response_url": "https://hooks.slack.com/actions/T111/111/xxxx", + "trigger_id": "111.222.valid", +} + + +app_mention_event = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63", + "type": "app_mention", + "text": "<@W111> Hi there!", + "user": "W222", + "ts": "1595926230.009600", + "team": "T111", + "channel": "C111", + "event_ts": "1595926230.009600", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1595926230, +} + +slash_command = { + "token": "fixed-verification-token", + "team_id": "T111", + "team_domain": "maria", + "channel_id": "C111", + "channel_name": "general", + "user_id": "U111", + "user_name": "rainer", + "command": "/start-conv", + "text": "title", + "response_url": "https://xxx.slack.com/commands/T111/xxx/zzz", + "trigger_id": "111.222.xxx", +} + +step_edit_payload = { + "type": "workflow_step_edit", + "token": "verification-token", + "action_ts": "1601541356.268786", + "team": { + "id": "T111", + "domain": "subdomain", + "enterprise_id": "E111", + "enterprise_name": "Org Name", + }, + "user": {"id": "W111", "username": "primary-owner", "team_id": "T111"}, + "callback_id": "copy_review", + "trigger_id": "111.222.xxx", + "workflow_step": { + "workflow_id": "12345", + "step_id": "111-222-333-444-555", + "inputs": { + "taskAuthorEmail": {"value": "seratch@example.com"}, + "taskDescription": {"value": "This is the task for you!"}, + "taskName": {"value": "The important task"}, + }, + "outputs": [ + {"name": "taskName", "type": "text", "label": "Task Name"}, + {"name": "taskDescription", "type": "text", "label": "Task Description"}, + {"name": "taskAuthorEmail", "type": "text", "label": "Task Author Email"}, + ], + }, +} + +step_save_payload = { + "type": "view_submission", + "team": { + "id": "T111", + "domain": "subdomain", + "enterprise_id": "E111", + "enterprise_name": "Org Name", + }, + "user": { + "id": "W111", + "username": "primary-owner", + "name": "primary-owner", + "team_id": "T111", + }, + "api_app_id": "A111", + "token": "verification-token", + "trigger_id": "111.222.xxx", + "view": { + "id": "V111", + "team_id": "T111", + "type": "workflow_step", + "blocks": [ + { + "type": "section", + "block_id": "intro-section", + "text": { + "type": "plain_text", + "text": "Create a task in one of the listed projects. The link to the task and other details will be available as variable data in later steps.", + }, + }, + { + "type": "input", + "block_id": "task_name_input", + "label": {"type": "plain_text", "text": "Task name"}, + "optional": False, + "element": { + "type": "plain_text_input", + "action_id": "task_name", + "placeholder": {"type": "plain_text", "text": "Write a task name"}, + }, + }, + { + "type": "input", + "block_id": "task_description_input", + "label": {"type": "plain_text", "text": "Task description"}, + "optional": False, + "element": { + "type": "plain_text_input", + "action_id": "task_description", + "placeholder": { + "type": "plain_text", + "text": "Write a description for your task", + }, + }, + }, + { + "type": "input", + "block_id": "task_author_input", + "label": {"type": "plain_text", "text": "Task author"}, + "optional": False, + "element": { + "type": "plain_text_input", + "action_id": "task_author", + "placeholder": {"type": "plain_text", "text": "Write a task name"}, + }, + }, + ], + "private_metadata": "", + "callback_id": "copy_review", + "state": { + "values": { + "task_name_input": { + "task_name": { + "type": "plain_text_input", + "value": "The important task", + } + }, + "task_description_input": { + "task_description": { + "type": "plain_text_input", + "value": "This is the task for you!", + } + }, + "task_author_input": { + "task_author": { + "type": "plain_text_input", + "value": "seratch@example.com", + } + }, + } + }, + "hash": "111.zzz", + "submit_disabled": False, + "app_id": "A111", + "external_id": "", + "app_installed_team_id": "T111", + "bot_id": "B111", + }, + "response_urls": [], + "workflow_step": { + "workflow_step_edit_id": "111.222.zzz", + "workflow_id": "12345", + "step_id": "111-222-333-444-555", + }, +} + +step_execute_payload = { + "token": "verification-token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "workflow_step_execute", + "callback_id": "copy_review", + "workflow_step": { + "workflow_step_execute_id": "zzz-execution", + "workflow_id": "12345", + "workflow_instance_id": "11111", + "step_id": "111-222-333-444-555", + "inputs": { + "taskAuthorEmail": {"value": "ksera@slack-corp.com"}, + "taskDescription": {"value": "sdfsdf"}, + "taskName": {"value": "a"}, + }, + "outputs": [ + {"name": "taskName", "type": "text", "label": "Task Name"}, + { + "name": "taskDescription", + "type": "text", + "label": "Task Description", + }, + { + "name": "taskAuthorEmail", + "type": "text", + "label": "Task Author Email", + }, + ], + }, + "event_ts": "1601541373.225894", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1601541373, +} + +global_shortcut = { + "type": "shortcut", + "token": "verification_token", + "action_ts": "111.111", + "team": { + "id": "T111", + "domain": "workspace-domain", + "enterprise_id": "E111", + "enterprise_name": "Org Name", + }, + "user": {"id": "W111", "username": "primary-owner", "team_id": "T111"}, + "callback_id": "test-shortcut", + "trigger_id": "111.111.xxxxxx", +} + +message_shortcut = { + "type": "message_action", + "token": "verification_token", + "action_ts": "1583637157.207593", + "team": { + "id": "T111", + "domain": "test-test", + "enterprise_id": "E111", + "enterprise_name": "Org Name", + }, + "user": {"id": "W111", "name": "test-test"}, + "channel": {"id": "C111", "name": "dev"}, + "callback_id": "test-shortcut", + "trigger_id": "111.222.xxx", + "message_ts": "1583636382.000300", + "message": { + "client_msg_id": "zzzz-111-222-xxx-yyy", + "type": "message", + "text": "<@W222> test", + "user": "W111", + "ts": "1583636382.000300", + "team": "T111", + "blocks": [ + { + "type": "rich_text", + "block_id": "d7eJ", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + {"type": "user", "user_id": "U222"}, + {"type": "text", "text": " test"}, + ], + } + ], + } + ], + }, + "response_url": "https://hooks.slack.com/app/T111/111/xxx", +} + +view_submission = { + "type": "view_submission", + "team": { + "id": "T111", + "domain": "workspace-domain", + "enterprise_id": "E111", + "enterprise_name": "Sandbox Org", + }, + "user": { + "id": "W111", + "username": "primary-owner", + "name": "primary-owner", + "team_id": "T111", + }, + "api_app_id": "A111", + "token": "verification_token", + "trigger_id": "111.222.valid", + "view": { + "id": "V111", + "team_id": "T111", + "type": "modal", + "blocks": [ + { + "type": "input", + "block_id": "hspI", + "label": { + "type": "plain_text", + "text": "Label", + }, + "optional": False, + "element": {"type": "plain_text_input", "action_id": "maBWU"}, + } + ], + "private_metadata": "This is for you!", + "callback_id": "view-id", + "state": { + "values": {"hspI": {"maBWU": {"type": "plain_text_input", "value": "test"}}} + }, + "hash": "1596530361.3wRYuk3R", + "title": { + "type": "plain_text", + "text": "My App", + }, + "clear_on_close": False, + "notify_on_close": False, + "close": { + "type": "plain_text", + "text": "Cancel", + }, + "submit": { + "type": "plain_text", + "text": "Submit", + }, + "previous_view_id": None, + "root_view_id": "V111", + "app_id": "A111", + "external_id": "", + "app_installed_team_id": "T111", + "bot_id": "B111", + }, + "response_urls": [], +} + +view_closed = { + "type": "view_closed", + "team": { + "id": "T111", + "domain": "workspace-domain", + "enterprise_id": "E111", + "enterprise_name": "Sandbox Org", + }, + "user": { + "id": "W111", + "username": "primary-owner", + "name": "primary-owner", + "team_id": "T111", + }, + "api_app_id": "A111", + "token": "verification_token", + "view": { + "id": "V111", + "team_id": "T111", + "type": "modal", + "blocks": [ + { + "type": "input", + "block_id": "hspI", + "label": { + "type": "plain_text", + "text": "Label", + }, + "optional": False, + "element": {"type": "plain_text_input", "action_id": "maBWU"}, + } + ], + "private_metadata": "This is for you!", + "callback_id": "view-id", + "state": {"values": {}}, + "hash": "1596530361.3wRYuk3R", + "title": { + "type": "plain_text", + "text": "My App", + }, + "clear_on_close": False, + "notify_on_close": False, + "close": { + "type": "plain_text", + "text": "Cancel", + }, + "submit": { + "type": "plain_text", + "text": "Submit", + }, + "previous_view_id": None, + "root_view_id": "V111", + "app_id": "A111", + "external_id": "", + "app_installed_team_id": "T111", + "bot_id": "B111", + }, + "response_urls": [], +} + +block_suggestion = { + "type": "block_suggestion", + "user": { + "id": "W111", + "username": "primary-owner", + "name": "primary-owner", + "team_id": "T111", + }, + "container": {"type": "view", "view_id": "V111"}, + "api_app_id": "A111", + "token": "verification_token", + "action_id": "the-id", + "block_id": "block-id", + "value": "search word", + "team": { + "id": "T111", + "domain": "workspace-domain", + "enterprise_id": "E111", + "enterprise_name": "Sandbox Org", + }, + "view": { + "id": "V111", + "team_id": "T111", + "type": "modal", + "blocks": [ + { + "type": "input", + "block_id": "5ar+", + "label": {"type": "plain_text", "text": "Label"}, + "optional": False, + "element": {"type": "plain_text_input", "action_id": "i5IpR"}, + }, + { + "type": "input", + "block_id": "es_b", + "label": {"type": "plain_text", "text": "Search"}, + "optional": False, + "element": { + "type": "external_select", + "action_id": "es_a", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + }, + }, + { + "type": "input", + "block_id": "mes_b", + "label": {"type": "plain_text", "text": "Search (multi)"}, + "optional": False, + "element": { + "type": "multi_external_select", + "action_id": "mes_a", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + }, + }, + ], + "private_metadata": "", + "callback_id": "view-id", + "state": {"values": {}}, + "hash": "111.xxx", + "title": {"type": "plain_text", "text": "My App"}, + "clear_on_close": False, + "notify_on_close": False, + "close": {"type": "plain_text", "text": "Cancel"}, + "submit": {"type": "plain_text", "text": "Submit"}, + "previous_view_id": None, + "root_view_id": "V111", + "app_id": "A111", + "external_id": "", + "app_installed_team_id": "T111", + "bot_id": "B111", + }, +} + +dialog_suggestion = { + "type": "dialog_suggestion", + "token": "verification_token", + "action_ts": "1596603332.676855", + "team": { + "id": "T111", + "domain": "workspace-domain", + "enterprise_id": "E111", + "enterprise_name": "Sandbox Org", + }, + "user": {"id": "W111", "name": "primary-owner", "team_id": "T111"}, + "channel": {"id": "C111", "name": "test-channel"}, + "name": "types", + "value": "search keyword", + "callback_id": "the-id", + "state": "Limo", +} From b6f52c930f2d940497d2a06a1a73466a939ffc7d Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 7 May 2021 16:40:29 +0900 Subject: [PATCH 2/2] Adjust the warning log message --- scripts/run_tests.sh | 4 +- slack_bolt/logger/messages.py | 77 +++++++- .../logger/test_unmatched_suggestions.py | 185 +++++++++++++++--- 3 files changed, 232 insertions(+), 34 deletions(-) diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index c9096048a..9931f8236 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -12,13 +12,13 @@ python_version=`python --version | awk '{print $2}'` if [[ $test_target != "" ]] then black slack_bolt/ tests/ && \ - pytest $1 + pytest -vv $1 else if [ ${python_version:0:3} == "3.8" ] then # pytype's behavior can be different in older Python versions black slack_bolt/ tests/ \ - && pytest \ + && pytest -vv \ && pip install -e ".[adapter]" \ && pip install -U pip setuptools wheel \ && pip install -U pytype \ diff --git a/slack_bolt/logger/messages.py b/slack_bolt/logger/messages.py index 6235765ae..12302631e 100644 --- a/slack_bolt/logger/messages.py +++ b/slack_bolt/logger/messages.py @@ -1,5 +1,5 @@ import time -from typing import Union +from typing import Union, Dict, Any, Optional from slack_sdk.web import SlackResponse @@ -110,6 +110,69 @@ def warning_unhandled_by_global_middleware( # type: ignore """ +def _build_filtered_body(body: Optional[Dict[str, Any]]) -> dict: + if body is None: + return {} + + payload_type = body.get("type") + filtered_body = {"type": payload_type} + + if "view" in body: + view = body["view"] + # view_submission, view_closed, workflow_step_save + filtered_body["view"] = { + "type": view.get("type"), + "callback_id": view.get("callback_id"), + } + + if payload_type == "block_actions": + # Block Kit Interactivity + actions = body.get("actions", []) + if len(actions) > 0 and actions[0] is not None: + filtered_body["block_id"] = actions[0].get("block_id") + filtered_body["action_id"] = actions[0].get("action_id") + if payload_type == "block_suggestion": + # Block Kit - external data source + filtered_body["block_id"] = body.get("block_id") + filtered_body["action_id"] = body.get("action_id") + filtered_body["value"] = body.get("value") + + if payload_type == "event_callback" and "event" in body: + # Events API, workflow_step_execute + event_payload = body.get("event", {}) + filtered_event = {"type": event_payload.get("type")} + if "subtype" in body["event"]: + filtered_event["subtype"] = event_payload.get("subtype") + filtered_body["event"] = filtered_event + + if "command" in body: + # Slash Commands + filtered_body["command"] = body.get("command") + + if payload_type in ["workflow_step_edit", "shortcut", "message_action"]: + # Workflow Steps, Global Shortcuts, Message Shortcuts + filtered_body["callback_id"] = body.get("callback_id") + + if payload_type == "interactive_message": + # Actions in Attachments + filtered_body["callback_id"] = body.get("callback_id") + filtered_body["actions"] = body.get("actions") + + if payload_type == "dialog_suggestion": + # Dialogs - external data source + filtered_body["callback_id"] = body.get("callback_id") + filtered_body["value"] = body.get("value") + if payload_type == "dialog_submission": + # Dialogs - clicking submit button + filtered_body["callback_id"] = body.get("callback_id") + filtered_body["submission"] = body.get("submission") + if payload_type == "dialog_cancellation": + # Dialogs - clicking cancel button + filtered_body["callback_id"] = body.get("callback_id") + + return filtered_body + + def _build_unhandled_request_suggestion(default_message: str, code_snippet: str): return f"""{default_message}{_unhandled_request_suggestion_prefix}{code_snippet}""" @@ -117,19 +180,25 @@ def _build_unhandled_request_suggestion(default_message: str, code_snippet: str) def warning_unhandled_request( # type: ignore req: Union[BoltRequest, "AsyncBoltRequest"], # type: ignore ) -> str: # type: ignore - default_message = f"Unhandled request ({req.body})" + filtered_body = _build_filtered_body(req.body) + default_message = f"Unhandled request ({filtered_body})" if ( is_workflow_step_edit(req.body) or is_workflow_step_save(req.body) or is_workflow_step_execute(req.body) ): # @app.step + callback_id = ( + filtered_body.get("callback_id") + or filtered_body.get("view", {}).get("callback_id") # type: ignore + or "your-callback-id" + ) return _build_unhandled_request_suggestion( default_message, f""" from slack_bolt.workflows.step import WorkflowStep ws = WorkflowStep( - callback_id="add_task", + callback_id="{callback_id}", edit=edit, save=save, execute=execute, @@ -192,7 +261,7 @@ def handle_view_events(ack, body, logger): ) if is_event(req.body): # @app.event - event_type = req.body.get('event', {}).get('type') + event_type = req.body.get("event", {}).get("type") return _build_unhandled_request_suggestion( default_message, f""" diff --git a/tests/slack_bolt/logger/test_unmatched_suggestions.py b/tests/slack_bolt/logger/test_unmatched_suggestions.py index 83a6173ec..eb2f0a519 100644 --- a/tests/slack_bolt/logger/test_unmatched_suggestions.py +++ b/tests/slack_bolt/logger/test_unmatched_suggestions.py @@ -17,8 +17,13 @@ def test_unknown_patterns(self): def test_block_actions(self): req: BoltRequest = BoltRequest(body=block_actions, mode="socket_mode") message = warning_unhandled_request(req) + filtered_body = { + "type": "block_actions", + "block_id": "b", + "action_id": "action-id-value", + } assert ( - f"""Unhandled request ({req.body}) + f"""Unhandled request ({filtered_body}) --- [Suggestion] You can handle this type of event with the following listener function: @@ -33,8 +38,19 @@ def handle_some_action(ack, body, logger): def test_attachment_actions(self): req: BoltRequest = BoltRequest(body=attachment_actions, mode="socket_mode") message = warning_unhandled_request(req) + filtered_body = { + "type": "interactive_message", + "callback_id": "pick_channel_for_fun", + "actions": [ + { + "name": "channel_list", + "type": "select", + "selected_options": [{"value": "C111"}], + } + ], + } assert ( - f"""Unhandled request ({req.body}) + f"""Unhandled request ({filtered_body}) --- [Suggestion] You can handle this type of event with the following listener function: @@ -48,9 +64,13 @@ def handle_some_action(ack, body, logger): def test_app_mention_event(self): req: BoltRequest = BoltRequest(body=app_mention_event, mode="socket_mode") + filtered_body = { + "type": "event_callback", + "event": {"type": "app_mention"}, + } message = warning_unhandled_request(req) assert ( - f"""Unhandled request ({req.body}) + f"""Unhandled request ({filtered_body}) --- [Suggestion] You can handle this type of event with the following listener function: @@ -64,8 +84,12 @@ def handle_app_mention_events(body, logger): def test_commands(self): req: BoltRequest = BoltRequest(body=slash_command, mode="socket_mode") message = warning_unhandled_request(req) + filtered_body = { + "type": None, + "command": "/start-conv", + } assert ( - f"""Unhandled request ({req.body}) + f"""Unhandled request ({filtered_body}) --- [Suggestion] You can handle this type of event with the following listener function: @@ -78,11 +102,33 @@ def handle_some_command(ack, body, logger): ) def test_shortcut(self): - for body in [global_shortcut, message_shortcut]: - req: BoltRequest = BoltRequest(body=body, mode="socket_mode") - message = warning_unhandled_request(req) - assert ( - f"""Unhandled request ({req.body}) + req: BoltRequest = BoltRequest(body=global_shortcut, mode="socket_mode") + message = warning_unhandled_request(req) + filtered_body = { + "type": "shortcut", + "callback_id": "test-shortcut", + } + assert ( + f"""Unhandled request ({filtered_body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +@app.shortcut("test-shortcut") +def handle_shortcuts(ack, body, logger): + ack() + logger.info(body) +""" + == message + ) + + req: BoltRequest = BoltRequest(body=message_shortcut, mode="socket_mode") + message = warning_unhandled_request(req) + filtered_body = { + "type": "message_action", + "callback_id": "test-shortcut", + } + assert ( + f"""Unhandled request ({filtered_body}) --- [Suggestion] You can handle this type of event with the following listener function: @@ -91,15 +137,18 @@ def handle_shortcuts(ack, body, logger): ack() logger.info(body) """ - == message - ) + == message + ) def test_view(self): - for body in [view_submission, view_closed]: - req: BoltRequest = BoltRequest(body=body, mode="socket_mode") - message = warning_unhandled_request(req) - assert ( - f"""Unhandled request ({req.body}) + req: BoltRequest = BoltRequest(body=view_submission, mode="socket_mode") + message = warning_unhandled_request(req) + filtered_body = { + "type": "view_submission", + "view": {"type": "modal", "callback_id": "view-id"}, + } + assert ( + f"""Unhandled request ({filtered_body}) --- [Suggestion] You can handle this type of event with the following listener function: @@ -108,14 +157,40 @@ def handle_view_events(ack, body, logger): ack() logger.info(body) """ - == message - ) + == message + ) + + req: BoltRequest = BoltRequest(body=view_closed, mode="socket_mode") + message = warning_unhandled_request(req) + filtered_body = { + "type": "view_closed", + "view": {"type": "modal", "callback_id": "view-id"}, + } + assert ( + f"""Unhandled request ({filtered_body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +@app.view("view-id") +def handle_view_events(ack, body, logger): + ack() + logger.info(body) +""" + == message + ) def test_block_suggestion(self): req: BoltRequest = BoltRequest(body=block_suggestion, mode="socket_mode") message = warning_unhandled_request(req) + filtered_body = { + "type": "block_suggestion", + "view": {"type": "modal", "callback_id": "view-id"}, + "block_id": "block-id", + "action_id": "the-id", + "value": "search word", + } assert ( - f"""Unhandled request ({req.body}) + f"""Unhandled request ({filtered_body}) --- [Suggestion] You can handle this type of event with the following listener function: @@ -129,8 +204,13 @@ def handle_some_options(ack): def test_dialog_suggestion(self): req: BoltRequest = BoltRequest(body=dialog_suggestion, mode="socket_mode") message = warning_unhandled_request(req) + filtered_body = { + "type": "dialog_suggestion", + "callback_id": "the-id", + "value": "search keyword", + } assert ( - f"""Unhandled request ({req.body}) + f"""Unhandled request ({filtered_body}) --- [Suggestion] You can handle this type of event with the following listener function: @@ -142,17 +222,20 @@ def handle_some_options(ack): ) def test_step(self): - for body in [step_edit_payload, step_save_payload, step_execute_payload]: - req: BoltRequest = BoltRequest(body=body, mode="socket_mode") - message = warning_unhandled_request(req) - assert ( - f"""Unhandled request ({req.body}) + req: BoltRequest = BoltRequest(body=step_edit_payload, mode="socket_mode") + message = warning_unhandled_request(req) + filtered_body = { + "type": "workflow_step_edit", + "callback_id": "copy_review", + } + assert ( + f"""Unhandled request ({filtered_body}) --- [Suggestion] You can handle this type of event with the following listener function: from slack_bolt.workflows.step import WorkflowStep ws = WorkflowStep( - callback_id="add_task", + callback_id="copy_review", edit=edit, save=save, execute=execute, @@ -160,8 +243,54 @@ def test_step(self): # Pass Step to set up listeners app.step(ws) """ - == message - ) + == message + ) + req: BoltRequest = BoltRequest(body=step_save_payload, mode="socket_mode") + message = warning_unhandled_request(req) + filtered_body = { + "type": "view_submission", + "view": {"type": "workflow_step", "callback_id": "copy_review"}, + } + assert ( + f"""Unhandled request ({filtered_body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +from slack_bolt.workflows.step import WorkflowStep +ws = WorkflowStep( + callback_id="copy_review", + edit=edit, + save=save, + execute=execute, +) +# Pass Step to set up listeners +app.step(ws) +""" + == message + ) + req: BoltRequest = BoltRequest(body=step_execute_payload, mode="socket_mode") + message = warning_unhandled_request(req) + filtered_body = { + "type": "event_callback", + "event": {"type": "workflow_step_execute"}, + } + assert ( + f"""Unhandled request ({filtered_body}) +--- +[Suggestion] You can handle this type of event with the following listener function: + +from slack_bolt.workflows.step import WorkflowStep +ws = WorkflowStep( + callback_id="your-callback-id", + edit=edit, + save=save, + execute=execute, +) +# Pass Step to set up listeners +app.step(ws) +""" + == message + ) block_actions = {