From 805d4421bd5db97dc11da745d5dcc6edb639f212 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Wed, 5 Jun 2024 13:51:26 +0800 Subject: [PATCH] Support grafana escalate (#4453) # What this PR does This PR adds support for **/grafana escalate** command alongside with **/escalate.** --- engine/apps/slack/slash_command.py | 36 ++++++++ .../tests/test_interactive_api_endpoint.py | 84 ++++++++++++++++++- engine/apps/slack/tests/test_slash_command.py | 31 +++++++ engine/apps/slack/views.py | 6 +- engine/settings/base.py | 2 +- 5 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 engine/apps/slack/slash_command.py create mode 100644 engine/apps/slack/tests/test_slash_command.py diff --git a/engine/apps/slack/slash_command.py b/engine/apps/slack/slash_command.py new file mode 100644 index 0000000000..89131d5bae --- /dev/null +++ b/engine/apps/slack/slash_command.py @@ -0,0 +1,36 @@ +from apps.slack.types.interaction_payloads import SlashCommandPayload + + +class SlashCommand: + """ + SlashCommand represents slack slash command. + + Attributes: + command -- command itself + args -- list of args passed to command + Examples: + /grafana escalate + SlashCommand(command='grafana', args=['escalate']) + """ + + def __init__(self, command, args): + # command itself + self.command = command + # list of args passed to command + self.args = args + + @property + def subcommand(self): + """ + Return first arg as subcommand + """ + return self.args[0] if len(self.args) > 0 else None + + @staticmethod + def parse(payload: SlashCommandPayload): + """ + Parse slack slash command payload and return SlashCommand object + """ + command = payload["command"].lstrip("/") + args = payload["text"].split() + return SlashCommand(command, args) diff --git a/engine/apps/slack/tests/test_interactive_api_endpoint.py b/engine/apps/slack/tests/test_interactive_api_endpoint.py index a43e4050c2..2c117f78bd 100644 --- a/engine/apps/slack/tests/test_interactive_api_endpoint.py +++ b/engine/apps/slack/tests/test_interactive_api_endpoint.py @@ -7,7 +7,7 @@ from rest_framework.test import APIClient from apps.slack.scenarios.manage_responders import ManageRespondersUserChange -from apps.slack.scenarios.paging import OnPagingTeamChange +from apps.slack.scenarios.paging import OnPagingTeamChange, StartDirectPaging from apps.slack.scenarios.schedules import EditScheduleShiftNotifyStep from apps.slack.scenarios.shift_swap_requests import AcceptShiftSwapRequestStep from apps.slack.types import PayloadType @@ -272,3 +272,85 @@ def test_accept_shift_swap_request( assert response.status_code == status.HTTP_200_OK mock_process_scenario.assert_called_once_with(slack_user_identity, slack_team_identity, payload) + + +@patch("apps.slack.views.SlackEventApiEndpointView.verify_signature", return_value=True) +@patch.object(StartDirectPaging, "process_scenario") +@pytest.mark.django_db +def test_grafana_escalate( + mock_process_scenario, + _mock_verify_signature, + make_organization, + make_slack_user_identity, + make_user, + slack_team_identity, +): + """ + Check StartDirectPaging.process_scenario gets called when a user types /grafana escalate. + UnifiedSlackApp commands are prefixed with /grafana. + """ + organization = make_organization(slack_team_identity=slack_team_identity) + slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity, slack_id=SLACK_USER_ID) + make_user(organization=organization, slack_user_identity=slack_user_identity) + + payload = { + "token": "gIkuvaNzQIHg97ATvDxqgjtO", + "team_id": slack_team_identity.slack_id, + "team_domain": "example", + "enterprise_id": "E0001", + "enterprise_name": "Globular%20Construct%20Inc", + "channel_id": "C2147483705", + "channel_name": "test", + "user_id": slack_user_identity.slack_id, + "user_name": "Steve", + "command": "/grafana", + "text": "escalate", + "response_url": "https://hooks.slack.com/commands/1234/5678", + "trigger_id": "13345224609.738474920.8088930838d88f008e0", + "api": "api_value", + } + response = _make_request(payload) + + assert response.status_code == status.HTTP_200_OK + mock_process_scenario.assert_called_once_with(slack_user_identity, slack_team_identity, payload) + + +@patch("apps.slack.views.SlackEventApiEndpointView.verify_signature", return_value=True) +@patch.object(StartDirectPaging, "process_scenario") +@pytest.mark.django_db +def test_escalate( + mock_process_scenario, + _mock_verify_signature, + make_organization, + make_slack_user_identity, + make_user, + slack_team_identity, +): + """ + Check StartDirectPaging.process_scenario gets called when a user types /escalate. + /escalate was used before Unified Slack App + """ + organization = make_organization(slack_team_identity=slack_team_identity) + slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity, slack_id=SLACK_USER_ID) + make_user(organization=organization, slack_user_identity=slack_user_identity) + + payload = { + "token": "gIkuvaNzQIHg97ATvDxqgjtO", + "team_id": slack_team_identity.slack_id, + "team_domain": "example", + "enterprise_id": "E0001", + "enterprise_name": "Globular%20Construct%20Inc", + "channel_id": "C2147483705", + "channel_name": "test", + "user_id": slack_user_identity.slack_id, + "user_name": "Steve", + "command": "/escalate", + "text": "", + "response_url": "https://hooks.slack.com/commands/1234/5678", + "trigger_id": "13345224609.738474920.8088930838d88f008e0", + "api": "api_value", + } + response = _make_request(payload) + + assert response.status_code == status.HTTP_200_OK + mock_process_scenario.assert_called_once_with(slack_user_identity, slack_team_identity, payload) diff --git a/engine/apps/slack/tests/test_slash_command.py b/engine/apps/slack/tests/test_slash_command.py new file mode 100644 index 0000000000..a821a8a44d --- /dev/null +++ b/engine/apps/slack/tests/test_slash_command.py @@ -0,0 +1,31 @@ +from apps.slack.slash_command import SlashCommand + + +def test_parse(): + payload = { + "command": "/grafana", + "text": "escalate", + "trigger_id": "trigger_id", + "user_id": "user_id", + "user_name": "user_name", + "api_app_id": "api_app_id", + } + slash_command = SlashCommand.parse(payload) + assert slash_command.command == "grafana" + assert slash_command.args == ["escalate"] + assert slash_command.subcommand == "escalate" + + +def test_parse_command_without_subcommand(): + payload = { + "command": "/escalate", + "text": "", + "trigger_id": "trigger_id", + "user_id": "user_id", + "user_name": "user_name", + "api_app_id": "api_app_id", + } + slash_command = SlashCommand.parse(payload) + assert slash_command.command == "escalate" + assert slash_command.args == [] + assert slash_command.subcommand is None diff --git a/engine/apps/slack/views.py b/engine/apps/slack/views.py index d1948f89e4..e921c60925 100644 --- a/engine/apps/slack/views.py +++ b/engine/apps/slack/views.py @@ -41,6 +41,7 @@ from .errors import SlackAPITokenError from .installation import SlackInstallationExc, uninstall_slack_integration from .models import SlackMessage, SlackTeamIdentity, SlackUserIdentity +from .slash_command import SlashCommand SCENARIOS_ROUTES: ScenarioRoute.RoutingSteps = [] SCENARIOS_ROUTES.extend(ONBOARDING_STEPS_ROUTING) @@ -354,7 +355,10 @@ def post(self, request): # Slash commands have to "type" if payload_command and route_payload_type == PayloadType.SLASH_COMMAND: - if payload_command in route["command_name"]: + cmd = SlashCommand.parse(payload) + # Check both command and subcommand for backward compatibility + # So both /grafana escalate and /escalate will work. + if cmd.command in route["command_name"] or cmd.subcommand in route["command_name"]: Step = route["step"] logger.info("Routing to {}".format(Step)) step = Step(slack_team_identity, organization, user) diff --git a/engine/settings/base.py b/engine/settings/base.py index eef9d16ee3..8e26146579 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -678,7 +678,7 @@ class BrokerTypes: SLACK_CLIENT_OAUTH_ID = os.environ.get("SLACK_CLIENT_OAUTH_ID") SLACK_CLIENT_OAUTH_SECRET = os.environ.get("SLACK_CLIENT_OAUTH_SECRET") -SLACK_DIRECT_PAGING_SLASH_COMMAND = os.environ.get("SLACK_DIRECT_PAGING_SLASH_COMMAND", "/escalate") +SLACK_DIRECT_PAGING_SLASH_COMMAND = os.environ.get("SLACK_DIRECT_PAGING_SLASH_COMMAND", "/escalate").lstrip("/") # Controls if slack integration can be installed/uninstalled. SLACK_INTEGRATION_MAINTENANCE_ENABLED = os.environ.get("SLACK_INTEGRATION_MAINTENANCE_ENABLED", False)