Skip to content

Commit 94219c2

Browse files
Introduce slash command matcher (#4717)
With the Unified Slack app we now have two ways of calling commands. 1. Legacy one when command invoked directly: /escalate 2. Unified one: /grafana escalate On top of that we have different slach commands for each environment: /escalate-local, /escalate-dev, etc. It was leading to a weird command to escalate via Unified App in dev u need to type: /grafana-dev escalate-develop. To support both, I introduced a matcher function for SlashCommandRoutes. It allows to simplify handling of such cases without complex workarounds in an EventAPIEndpoint. # What this PR does ## Which issue(s) this PR closes Related to [issue link here] <!-- *Note*: If you want the issue to be auto-closed once the PR is merged, change "Related to" to "Closes" in the line above. If you have more than one GitHub issue that this PR closes, be sure to preface each issue link with a [closing keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue). This ensures that the issue(s) are auto-closed once the PR has been merged. --> ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes.
1 parent 4877b9d commit 94219c2

File tree

5 files changed

+22
-5
lines changed

5 files changed

+22
-5
lines changed

engine/apps/slack/scenarios/paging.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from apps.slack.constants import DIVIDER, PRIVATE_METADATA_MAX_LENGTH
1616
from apps.slack.errors import SlackAPIChannelNotFoundError
1717
from apps.slack.scenarios import scenario_step
18+
from apps.slack.slash_command import SlashCommand
1819
from apps.slack.types import (
1920
Block,
2021
BlockActionType,
@@ -115,7 +116,13 @@ def get_current_items(
115116
class StartDirectPaging(scenario_step.ScenarioStep):
116117
"""Handle slash command invocation and show initial dialog."""
117118

118-
command_name = [settings.SLACK_DIRECT_PAGING_SLASH_COMMAND]
119+
@staticmethod
120+
def matcher(slash_command: SlashCommand) -> bool:
121+
# Check if command is /escalate. It's a legacy command we keep for smooth transition.
122+
is_legacy_command = slash_command.command == settings.SLACK_DIRECT_PAGING_SLASH_COMMAND
123+
# Check if command is /grafana escalate. It's a new command from unified app.
124+
is_unified_app_command = slash_command.is_grafana_command and slash_command.subcommand == "escalate"
125+
return is_legacy_command or is_unified_app_command
119126

120127
def process_scenario(
121128
self,
@@ -995,8 +1002,8 @@ def _generate_input_id_prefix() -> str:
9951002
},
9961003
{
9971004
"payload_type": PayloadType.SLASH_COMMAND,
998-
"command_name": StartDirectPaging.command_name,
9991005
"step": StartDirectPaging,
1006+
"matcher": StartDirectPaging.matcher,
10001007
},
10011008
{
10021009
"payload_type": PayloadType.VIEW_SUBMISSION,

engine/apps/slack/slash_command.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ def __init__(self, command, args):
2222
@property
2323
def subcommand(self):
2424
"""
25-
Return first arg as subcommand
25+
Return first arg as action subcommand: part of command which defines action
26+
Example: /grafana escalate -> escalate
2627
"""
2728
return self.args[0] if len(self.args) > 0 else None
2829

@@ -34,3 +35,7 @@ def parse(payload: SlashCommandPayload):
3435
command = payload["command"].lstrip("/")
3536
args = payload["text"].split()
3637
return SlashCommand(command, args)
38+
39+
@property
40+
def is_grafana_command(self):
41+
return self.command in ["grafana", "grafana-dev", "grafana-ops", "grafana-prod"]

engine/apps/slack/tests/test_slash_command.py

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def test_parse():
1414
assert slash_command.command == "grafana"
1515
assert slash_command.args == ["escalate"]
1616
assert slash_command.subcommand == "escalate"
17+
assert slash_command.is_grafana_command
1718

1819

1920
def test_parse_command_without_subcommand():

engine/apps/slack/types/scenario_routes.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import typing
22

3+
from apps.slack.slash_command import SlashCommand
4+
35
from .common import EventType, PayloadType
46

57
if typing.TYPE_CHECKING:
68
from apps.slack.scenarios.scenario_step import ScenarioStep
79
from apps.slack.types import BlockActionType, InteractiveMessageActionType
810

11+
MatcherType = typing.Callable[[SlashCommand], bool]
12+
913

1014
class ScenarioRoute:
1115
class _Base(typing.TypedDict):
@@ -32,7 +36,7 @@ class MessageActionScenarioRoute(_Base):
3236

3337
class SlashCommandScenarioRoute(_Base):
3438
payload_type: typing.Literal[PayloadType.SLASH_COMMAND]
35-
command_name: typing.List[str]
39+
matcher: MatcherType
3640

3741
class ViewSubmissionScenarioRoute(_Base):
3842
payload_type: typing.Literal[PayloadType.VIEW_SUBMISSION]

engine/apps/slack/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ def post(self, request):
361361
cmd = SlashCommand.parse(payload)
362362
# Check both command and subcommand for backward compatibility
363363
# So both /grafana escalate and /escalate will work.
364-
if cmd.command in route["command_name"] or cmd.subcommand in route["command_name"]:
364+
if route["matcher"](cmd):
365365
Step = route["step"]
366366
logger.info("Routing to {}".format(Step))
367367
step = Step(slack_team_identity, organization, user)

0 commit comments

Comments
 (0)