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.
This commit is contained in:
Innokentii Konstantinov 2024-07-24 17:53:06 +08:00 committed by GitHub
parent 4877b9d927
commit 94219c25bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 22 additions and 5 deletions

View file

@ -15,6 +15,7 @@ from apps.slack.chatops_proxy_routing import make_private_metadata, make_value
from apps.slack.constants import DIVIDER, PRIVATE_METADATA_MAX_LENGTH
from apps.slack.errors import SlackAPIChannelNotFoundError
from apps.slack.scenarios import scenario_step
from apps.slack.slash_command import SlashCommand
from apps.slack.types import (
Block,
BlockActionType,
@ -115,7 +116,13 @@ def get_current_items(
class StartDirectPaging(scenario_step.ScenarioStep):
"""Handle slash command invocation and show initial dialog."""
command_name = [settings.SLACK_DIRECT_PAGING_SLASH_COMMAND]
@staticmethod
def matcher(slash_command: SlashCommand) -> bool:
# Check if command is /escalate. It's a legacy command we keep for smooth transition.
is_legacy_command = slash_command.command == settings.SLACK_DIRECT_PAGING_SLASH_COMMAND
# Check if command is /grafana escalate. It's a new command from unified app.
is_unified_app_command = slash_command.is_grafana_command and slash_command.subcommand == "escalate"
return is_legacy_command or is_unified_app_command
def process_scenario(
self,
@ -995,8 +1002,8 @@ STEPS_ROUTING: ScenarioRoute.RoutingSteps = [
},
{
"payload_type": PayloadType.SLASH_COMMAND,
"command_name": StartDirectPaging.command_name,
"step": StartDirectPaging,
"matcher": StartDirectPaging.matcher,
},
{
"payload_type": PayloadType.VIEW_SUBMISSION,

View file

@ -22,7 +22,8 @@ class SlashCommand:
@property
def subcommand(self):
"""
Return first arg as subcommand
Return first arg as action subcommand: part of command which defines action
Example: /grafana escalate -> escalate
"""
return self.args[0] if len(self.args) > 0 else None
@ -34,3 +35,7 @@ class SlashCommand:
command = payload["command"].lstrip("/")
args = payload["text"].split()
return SlashCommand(command, args)
@property
def is_grafana_command(self):
return self.command in ["grafana", "grafana-dev", "grafana-ops", "grafana-prod"]

View file

@ -14,6 +14,7 @@ def test_parse():
assert slash_command.command == "grafana"
assert slash_command.args == ["escalate"]
assert slash_command.subcommand == "escalate"
assert slash_command.is_grafana_command
def test_parse_command_without_subcommand():

View file

@ -1,11 +1,15 @@
import typing
from apps.slack.slash_command import SlashCommand
from .common import EventType, PayloadType
if typing.TYPE_CHECKING:
from apps.slack.scenarios.scenario_step import ScenarioStep
from apps.slack.types import BlockActionType, InteractiveMessageActionType
MatcherType = typing.Callable[[SlashCommand], bool]
class ScenarioRoute:
class _Base(typing.TypedDict):
@ -32,7 +36,7 @@ class ScenarioRoute:
class SlashCommandScenarioRoute(_Base):
payload_type: typing.Literal[PayloadType.SLASH_COMMAND]
command_name: typing.List[str]
matcher: MatcherType
class ViewSubmissionScenarioRoute(_Base):
payload_type: typing.Literal[PayloadType.VIEW_SUBMISSION]

View file

@ -361,7 +361,7 @@ class SlackEventApiEndpointView(APIView):
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"]:
if route["matcher"](cmd):
Step = route["step"]
logger.info("Routing to {}".format(Step))
step = Step(slack_team_identity, organization, user)