From 37187ef18a8ee618569c1db6d5a76005602c8f1f Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Tue, 12 Jul 2022 12:56:27 +0400 Subject: [PATCH] Manual incidents for all teams (#194) * Fix creation of manual incident via submenu * Remove legacy finish_configuration_attachments * Add manual incidents for all teams * Fix manual incident creation from slash command * Fix slack title template * Get rid of migration --- .../renderers/web_renderer.py | 2 +- .../templaters/slack_templater.py | 54 ++ .../templaters/web_templater.py | 2 +- .../alerts/models/alert_receive_channel.py | 4 +- .../slack/scenarios/invited_to_channel.py | 42 ++ .../apps/slack/scenarios/manual_incident.py | 687 ++++++++++++++++++ engine/apps/slack/scenarios/public_menu.py | 536 -------------- .../apps/slack/scenarios/resolution_note.py | 143 ++++ engine/apps/slack/scenarios/scenario_step.py | 74 -- engine/apps/slack/views.py | 6 +- engine/config_integrations/manual.py | 43 +- 11 files changed, 949 insertions(+), 644 deletions(-) create mode 100644 engine/apps/slack/scenarios/invited_to_channel.py create mode 100644 engine/apps/slack/scenarios/manual_incident.py delete mode 100644 engine/apps/slack/scenarios/public_menu.py diff --git a/engine/apps/alerts/incident_appearance/renderers/web_renderer.py b/engine/apps/alerts/incident_appearance/renderers/web_renderer.py index f7eecbab..e68d453c 100644 --- a/engine/apps/alerts/incident_appearance/renderers/web_renderer.py +++ b/engine/apps/alerts/incident_appearance/renderers/web_renderer.py @@ -14,7 +14,7 @@ class AlertWebRenderer(AlertBaseRenderer): "title": str_or_backup(templated_alert.title, "Alert"), "message": str_or_backup(templated_alert.message, ""), "image_url": str_or_backup(templated_alert.image_url, None), - "source_link": str_or_backup(templated_alert.image_url, None), + "source_link": str_or_backup(templated_alert.source_link, None), } return rendered_alert diff --git a/engine/apps/alerts/incident_appearance/templaters/slack_templater.py b/engine/apps/alerts/incident_appearance/templaters/slack_templater.py index 6c6e3efe..48246cd2 100644 --- a/engine/apps/alerts/incident_appearance/templaters/slack_templater.py +++ b/engine/apps/alerts/incident_appearance/templaters/slack_templater.py @@ -1,3 +1,5 @@ +from django.apps import apps + from apps.alerts.incident_appearance.templaters.alert_templater import AlertTemplater @@ -12,3 +14,55 @@ class AlertSlackTemplater(AlertTemplater): if templated_alert.title: templated_alert.title = templated_alert.title.replace("\n", "").replace("\r", "") return templated_alert + + def render(self): + """ + Overriden render method to modify payload of manual integration alerts + """ + self._modify_payload_for_manual_integration_if_needed() + return super().render() + + def _modify_payload_for_manual_integration_if_needed(self): + """ + Modifies payload of alerts made from manual incident integration. + It is needed to simplify templates. + """ + payload = self.alert.raw_request_data + # First check if payload look like payload from manual incident integration and was not modified before. + if "view" in payload and "private_metadata" in payload.get("view", {}) and "oncall" not in payload: + AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel") + # If so - check it with db query. + if self.alert.group.channel.integration == AlertReceiveChannel.INTEGRATION_MANUAL: + metadata = payload.get("view", {}).get("private_metadata", {}) + payload["oncall"] = {} + if "message" in metadata: + # If alert was made from message + domain = payload.get("team", {}).get("domain", "unknown") + channel_id = metadata.get("channel_id", "unknown") + message = metadata.get("message", {}) + message_ts = message.get("ts", "unknown") + message_text = message.get("text", "unknown") + payload["oncall"]["permalink"] = f"https://{domain}.slack.com/archives/{channel_id}/p{message_ts}" + payload["oncall"]["author_username"] = metadata.get("author_username", "Unknown") + payload["oncall"]["title"] = "Message from @" + payload["oncall"]["author_username"] + payload["oncall"]["message"] = message_text + else: + # If alert was made via slash command + message_text = ( + payload.get("view", {}) + .get("state", {}) + .get("values", {}) + .get("MESSAGE_INPUT", {}) + .get("FinishCreateIncidentViewStep", {}) + .get("value", "unknown") + ) + payload["oncall"]["permalink"] = None + payload["oncall"]["title"] = self.alert.title + payload["oncall"]["message"] = message_text + created_by = self.alert.integration_unique_data.get("created_by", None) + username = payload.get("user", {}).get("name", None) + author_username = created_by or username or "unknown" + payload["oncall"]["author_username"] = author_username + + self.alert.raw_request_data = payload + self.alert.save(update_fields=["raw_request_data"]) diff --git a/engine/apps/alerts/incident_appearance/templaters/web_templater.py b/engine/apps/alerts/incident_appearance/templaters/web_templater.py index 913e2857..fa7c29aa 100644 --- a/engine/apps/alerts/incident_appearance/templaters/web_templater.py +++ b/engine/apps/alerts/incident_appearance/templaters/web_templater.py @@ -32,5 +32,5 @@ class AlertWebTemplater(AlertTemplater): def _slack_format_for_web(self, data): sf = self.slack_formatter - sf.hyperlink_mention_format = "[title](url)" + sf.hyperlink_mention_format = "[{title}]({url})" return sf.format(data) diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index a2a9dad7..f408efce 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -448,7 +448,9 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): def get_or_create_manual_integration(cls, defaults, **kwargs): try: alert_receive_channel = cls.objects.get( - organization=kwargs["organization"], integration=kwargs["integration"] + organization=kwargs["organization"], + integration=kwargs["integration"], + team=kwargs["team"], ) except cls.DoesNotExist: kwargs.update(defaults) diff --git a/engine/apps/slack/scenarios/invited_to_channel.py b/engine/apps/slack/scenarios/invited_to_channel.py new file mode 100644 index 00000000..976884c1 --- /dev/null +++ b/engine/apps/slack/scenarios/invited_to_channel.py @@ -0,0 +1,42 @@ +import logging + +from django.utils import timezone + +from apps.slack.scenarios import scenario_step +from apps.slack.slack_client import SlackClientWithErrorHandling + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +class InvitedToChannelStep(scenario_step.ScenarioStep): + tags = [ + scenario_step.ScenarioStep.TAG_TRIGGERED_BY_SYSTEM, + ] + + def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): + if payload["event"]["user"] == slack_team_identity.bot_user_id: + channel_id = payload["event"]["channel"] + slack_client = SlackClientWithErrorHandling(slack_team_identity.bot_access_token) + channel = slack_client.api_call("conversations.info", channel=channel_id)["channel"] + + slack_team_identity.cached_channels.update_or_create( + slack_id=channel["id"], + defaults={ + "name": channel["name"], + "is_archived": channel["is_archived"], + "is_shared": channel["is_shared"], + "last_populated": timezone.now().date(), + }, + ) + else: + logger.info("Other user was invited to a channel with a bot.") + + +STEPS_ROUTING = [ + { + "payload_type": scenario_step.PAYLOAD_TYPE_EVENT_CALLBACK, + "event_type": scenario_step.EVENT_TYPE_MEMBER_JOINED_CHANNEL, + "step": InvitedToChannelStep, + }, +] diff --git a/engine/apps/slack/scenarios/manual_incident.py b/engine/apps/slack/scenarios/manual_incident.py new file mode 100644 index 00000000..0833cfa5 --- /dev/null +++ b/engine/apps/slack/scenarios/manual_incident.py @@ -0,0 +1,687 @@ +import json +from uuid import uuid4 + +from django.apps import apps +from django.conf import settings + +from apps.alerts.models import AlertReceiveChannel +from apps.slack.scenarios import scenario_step +from apps.slack.slack_client.exceptions import SlackAPIException + +MANUAL_INCIDENT_TEAM_SELECT_ID = "manual_incident_team_select" +MANUAL_INCIDENT_ORG_SELECT_ID = "manual_incident_org_select" +MANUAL_INCIDENT_ROUTE_SELECT_ID = "manual_incident_route_select" +MANUAL_INCIDENT_TITLE_INPUT_ID = "manual_incident_title_input" +MANUAL_INCIDENT_MESSAGE_INPUT_ID = "manual_incident_message_input" + +DEFAULT_TEAM_VALUE = "default_team" + + +class StartCreateIncidentFromMessage(scenario_step.ScenarioStep): + """ + StartCreateIncidentFromMessage triggers creation of a manual incident from the slack message via submenu + """ + + callback_id = [ + "incident_create", + "incident_create_staging", + "incident_create_develop", + ] + + def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): + input_id_prefix = _generate_input_id_prefix() + + channel_id = payload["channel"]["id"] + try: + image_url = payload["message"]["files"][0]["permalink"] + except KeyError: + image_url = None + private_metadata = { + "channel_id": channel_id, + "image_url": image_url, + "message": { + "user": payload["message"].get("user"), + "text": payload["message"].get("text"), + "ts": payload["message"].get("ts"), + }, + "input_id_prefix": input_id_prefix, + "with_title_and_message_inputs": False, + "submit_routing_uid": FinishCreateIncidentFromMessage.routing_uid(), + } + + blocks = _get_manual_incident_initial_form_fields( + slack_team_identity, slack_user_identity, input_id_prefix, payload + ) + view = _get_manual_incident_form_view( + FinishCreateIncidentFromMessage.routing_uid(), blocks, json.dumps(private_metadata) + ) + self._slack_client.api_call( + "views.open", + trigger_id=payload["trigger_id"], + view=view, + ) + + +class FinishCreateIncidentFromMessage(scenario_step.ScenarioStep): + """ + FinishCreateIncidentFromMessage creates a manual incident from the slack message via submenu + """ + + def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): + Alert = apps.get_model("alerts", "Alert") + + private_metadata = json.loads(payload["view"]["private_metadata"]) + + channel_id = private_metadata["channel_id"] + + input_id_prefix = private_metadata["input_id_prefix"] + selected_organization = _get_selected_org_from_payload(payload, input_id_prefix) + selected_team = _get_selected_team_from_payload(payload, input_id_prefix) + selected_route = _get_selected_route_from_payload(payload, input_id_prefix) + + user = slack_user_identity.get_user(selected_organization) + alert_receive_channel = AlertReceiveChannel.get_or_create_manual_integration( + organization=selected_organization, + team=selected_team, + integration=AlertReceiveChannel.INTEGRATION_MANUAL, + deleted_at=None, + defaults={ + "author": user, + "verbal_name": f"Manual incidents ({selected_team.name if selected_team else 'General'} team)", + }, + ) + + author_username = slack_user_identity.slack_verbal + try: + permalink = self._slack_client.api_call( + "chat.getPermalink", + channel=private_metadata["channel_id"], + message_ts=private_metadata["message"]["ts"], + ) + permalink = permalink.get("permalink", None) + except SlackAPIException: + permalink = None + title = "Message from {}".format(author_username) + message = private_metadata["message"]["text"] + + # Deprecated, use custom oncall property instead. + # update private metadata in payload to use it in alert rendering + payload["view"]["private_metadata"] = private_metadata + payload["view"]["private_metadata"]["author_username"] = author_username + # Custom oncall property in payload to simplify rendering + payload["oncall"] = {} + payload["oncall"]["title"] = title + payload["oncall"]["message"] = message + payload["oncall"]["author_username"] = author_username + payload["oncall"]["permalink"] = permalink + Alert.create( + title=title, + message=message, + image_url=private_metadata["image_url"], + # Link to the slack message is not here bc it redirects to browser + link_to_upstream_details=None, + alert_receive_channel=alert_receive_channel, + raw_request_data=payload, + integration_unique_data={"created_by": user.get_user_verbal_for_team_for_slack()}, + force_route_id=selected_route.pk, + ) + + try: + self._slack_client.api_call( + "chat.postEphemeral", + channel=channel_id, + user=slack_user_identity.slack_id, + text=":white_check_mark: Alert successfully submitted", + ) + except SlackAPIException as e: + if e.response["error"] == "channel_not_found" or e.response["error"] == "user_not_in_channel": + self._slack_client.api_call( + "chat.postEphemeral", + channel=slack_user_identity.im_channel_id, + user=slack_user_identity.slack_id, + text=":white_check_mark: Alert successfully submitted", + ) + else: + raise e + + +class StartCreateIncidentFromSlashCommand(scenario_step.ScenarioStep): + """ + StartCreateIncidentFromSlashCommand triggers creation of a manual incident from the slack message via slash command + """ + + command_name = [settings.SLACK_SLASH_COMMAND_NAME] + TITLE_INPUT_BLOCK_ID = "TITLE_INPUT" + MESSAGE_INPUT_BLOCK_ID = "MESSAGE_INPUT" + + def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): + input_id_prefix = _generate_input_id_prefix() + + try: + channel_id = payload["event"]["channel"] + except KeyError: + channel_id = payload["channel_id"] + + private_metadata = { + "channel_id": channel_id, + "input_id_prefix": input_id_prefix, + "with_title_and_message_inputs": True, + "submit_routing_uid": FinishCreateIncidentFromSlashCommand.routing_uid(), + } + + blocks = _get_manual_incident_initial_form_fields( + slack_team_identity, slack_user_identity, input_id_prefix, payload, with_title_and_message_inputs=True + ) + view = _get_manual_incident_form_view( + FinishCreateIncidentFromSlashCommand.routing_uid(), blocks, json.dumps(private_metadata) + ) + + self._slack_client.api_call( + "views.open", + trigger_id=payload["trigger_id"], + view=view, + ) + + +class FinishCreateIncidentFromSlashCommand(scenario_step.ScenarioStep): + """ + FinishCreateIncidentFromSlashCommand creates a manual incident from the slack message via slash message + """ + + def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): + Alert = apps.get_model("alerts", "Alert") + + title = _get_title_from_payload(payload) + message = _get_message_from_payload(payload) + + private_metadata = json.loads(payload["view"]["private_metadata"]) + + channel_id = private_metadata["channel_id"] + + input_id_prefix = private_metadata["input_id_prefix"] + selected_organization = _get_selected_org_from_payload(payload, input_id_prefix) + selected_team = _get_selected_team_from_payload(payload, input_id_prefix) + selected_route = _get_selected_route_from_payload(payload, input_id_prefix) + + user = slack_user_identity.get_user(selected_organization) + alert_receive_channel = AlertReceiveChannel.get_or_create_manual_integration( + organization=selected_organization, + team=selected_team, + integration=AlertReceiveChannel.INTEGRATION_MANUAL, + deleted_at=None, + defaults={ + "author": user, + "verbal_name": f"Manual incidents ({selected_team.name if selected_team else 'General'} team)", + }, + ) + + author_username = slack_user_identity.slack_verbal + + try: + self._slack_client.api_call( + "chat.postEphemeral", + channel=channel_id, + user=slack_user_identity.slack_id, + text=":white_check_mark: Alert *{}* successfully submitted".format(title), + ) + except SlackAPIException as e: + if e.response["error"] == "channel_not_found": + self._slack_client.api_call( + "chat.postEphemeral", + channel=slack_user_identity.im_channel_id, + user=slack_user_identity.slack_id, + text=":white_check_mark: Alert *{}* successfully submitted".format(title), + ) + else: + raise e + + # Deprecated, use custom oncall property instead. + # Update private metadata to use it in rendering: + payload["view"]["private_metadata"] = private_metadata + # Custom oncall property to simplify rendering + payload["oncall"] = {} + payload["oncall"]["title"] = title + payload["oncall"]["message"] = message + payload["oncall"]["author_username"] = author_username + payload["oncall"]["permalink"] = None + + Alert.create( + title=title, + message=message, + image_url=None, + link_to_upstream_details=None, + alert_receive_channel=alert_receive_channel, + raw_request_data=payload, + integration_unique_data={ + "created_by": author_username, + }, + force_route_id=selected_route.pk, + ) + + +# OnChange steps responsible for rerendering manual incident creation form on change values in selects. +# They are works both with incident creation from submenu and slack command. + + +class OnOrgChange(scenario_step.ScenarioStep): + def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): + private_metadata = json.loads(payload["view"]["private_metadata"]) + with_title_and_message_inputs = private_metadata.get("with_title_and_message_inputs", False) + submit_routing_uid = private_metadata.get("submit_routing_uid") + old_input_id_prefix, new_input_id_prefix, new_private_metadata = _get_and_change_input_id_prefix_from_metadata( + private_metadata + ) + + selected_organization = _get_selected_org_from_payload(payload, old_input_id_prefix) + # Set selected team to default because org is changed. + selected_team = None + + user = slack_user_identity.get_user(selected_organization) + manual_integration = AlertReceiveChannel.get_or_create_manual_integration( + organization=selected_organization, + team=selected_team, + integration=AlertReceiveChannel.INTEGRATION_MANUAL, + deleted_at=None, + defaults={ + "author": user, + "verbal_name": f"Manual incidents ({selected_team.name if selected_team else 'General'} team)", + }, + ) + selected_route = manual_integration.default_channel_filter + + organization_select = _get_organization_select( + slack_team_identity, slack_user_identity, selected_organization, new_input_id_prefix + ) + team_select = _get_team_select(slack_user_identity, selected_organization, selected_team, new_input_id_prefix) + route_select = _get_route_select(manual_integration, selected_route, new_input_id_prefix) + + blocks = [organization_select, team_select, route_select] + if with_title_and_message_inputs: + blocks.extend([_get_title_input(payload), _get_message_input(payload)]) + view = _get_manual_incident_form_view(submit_routing_uid, blocks, json.dumps(new_private_metadata)) + self._slack_client.api_call( + "views.update", + trigger_id=payload["trigger_id"], + view=view, + view_id=payload["view"]["id"], + ) + + +class OnTeamChange(scenario_step.ScenarioStep): + def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): + private_metadata = json.loads(payload["view"]["private_metadata"]) + with_title_and_message_inputs = private_metadata.get("with_title_and_message_inputs", False) + submit_routing_uid = private_metadata.get("submit_routing_uid") + old_input_id_prefix, new_input_id_prefix, new_private_metadata = _get_and_change_input_id_prefix_from_metadata( + private_metadata + ) + + selected_organization = _get_selected_org_from_payload(payload, old_input_id_prefix) + selected_team = _get_selected_team_from_payload(payload, old_input_id_prefix) + + user = slack_user_identity.get_user(selected_organization) + manual_integration = AlertReceiveChannel.get_or_create_manual_integration( + organization=selected_organization, + team=selected_team, + integration=AlertReceiveChannel.INTEGRATION_MANUAL, + deleted_at=None, + defaults={ + "author": user, + "verbal_name": f"Manual incidents ({selected_team.name if selected_team else 'General'} team)", + }, + ) + initial_route = manual_integration.default_channel_filter + + organization_select = _get_organization_select( + slack_team_identity, slack_user_identity, selected_organization, new_input_id_prefix + ) + team_select = _get_team_select(slack_user_identity, selected_organization, selected_team, new_input_id_prefix) + route_select = _get_route_select(manual_integration, initial_route, new_input_id_prefix) + + blocks = [organization_select, team_select, route_select] + if with_title_and_message_inputs: + blocks.extend([_get_title_input(payload), _get_message_input(payload)]) + view = _get_manual_incident_form_view(submit_routing_uid, blocks, json.dumps(new_private_metadata)) + self._slack_client.api_call( + "views.update", + trigger_id=payload["trigger_id"], + view=view, + view_id=payload["view"]["id"], + ) + + +class OnRouteChange(scenario_step.ScenarioStep): + """ + OnRouteChange is just a plug to handle change of value on route select + """ + + def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): + pass + + +def _get_manual_incident_form_view(routing_uid, blocks, private_metatada): + view = { + "type": "modal", + "callback_id": routing_uid, + "title": { + "type": "plain_text", + "text": "Create an Incident", + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": True, + }, + "submit": { + "type": "plain_text", + "text": "Submit", + }, + "blocks": blocks, + "private_metadata": private_metatada, + } + + return view + + +def _get_manual_incident_initial_form_fields( + slack_team_identity, slack_user_identity, input_id_prefix, payload, with_title_and_message_inputs=False +): + initial_organization = ( + slack_team_identity.organizations.filter(users__slack_user_identity=slack_user_identity) + .order_by("pk") + .distinct() + .first() + ) + + organization_select = _get_organization_select( + slack_team_identity, slack_user_identity, initial_organization, input_id_prefix + ) + + initial_team = None # means default team + team_select = _get_team_select(slack_user_identity, initial_organization, initial_team, input_id_prefix) + + user = slack_user_identity.get_user(initial_organization) + manual_integration = AlertReceiveChannel.get_or_create_manual_integration( + organization=initial_organization, + team=initial_team, + integration=AlertReceiveChannel.INTEGRATION_MANUAL, + deleted_at=None, + defaults={ + "author": user, + "verbal_name": f"Manual incidents ({initial_team.name if initial_team else 'General'} team)", + }, + ) + + initial_route = manual_integration.default_channel_filter + route_select = _get_route_select(manual_integration, initial_route, input_id_prefix) + blocks = [organization_select, team_select, route_select] + if with_title_and_message_inputs: + title_input = _get_title_input(payload) + message_input = _get_message_input(payload) + blocks.append(title_input) + blocks.append(message_input) + return blocks + + +def _get_organization_select(slack_team_identity, slack_user_identity, value, input_id_prefix): + organizations = slack_team_identity.organizations.filter( + users__slack_user_identity=slack_user_identity, + ).distinct() + organizations_options = [] + initial_option_idx = 0 + for idx, org in enumerate(organizations): + if org == value: + initial_option_idx = idx + organizations_options.append( + { + "text": { + "type": "plain_text", + "text": f"{org.org_title}", + "emoji": True, + }, + "value": f"{org.pk}", + } + ) + + organization_select = { + "type": "section", + "text": {"type": "mrkdwn", "text": "Select an organization"}, + "block_id": input_id_prefix + MANUAL_INCIDENT_ORG_SELECT_ID, + "accessory": { + "type": "static_select", + "placeholder": {"type": "plain_text", "text": "Select an organization", "emoji": True}, + "options": organizations_options, + "action_id": OnOrgChange.routing_uid(), + "initial_option": organizations_options[initial_option_idx], + }, + } + + return organization_select + + +def _get_selected_org_from_payload(payload, input_id_prefix): + Organization = apps.get_model("user_management", "Organization") + selected_org_id = payload["view"]["state"]["values"][input_id_prefix + MANUAL_INCIDENT_ORG_SELECT_ID][ + OnOrgChange.routing_uid() + ]["selected_option"]["value"] + org = Organization.objects.filter(pk=selected_org_id).first() + return org + + +def _get_team_select(slack_user_identity, organization, value, input_id_prefix): + teams = organization.teams.filter( + users__slack_user_identity=slack_user_identity, + ).distinct() + team_options = [] + # Adding pseudo option for default team + initial_option_idx = 0 + team_options.append( + { + "text": { + "type": "plain_text", + "text": f"General", + "emoji": True, + }, + "value": DEFAULT_TEAM_VALUE, + } + ) + for idx, team in enumerate(teams): + if team == value: + # Add 1 because default team option was added before cycle, so option indicies are shifted + initial_option_idx = idx + 1 + team_options.append( + { + "text": { + "type": "plain_text", + "text": f"{team.name}", + "emoji": True, + }, + "value": f"{team.pk}", + } + ) + + team_select = { + "type": "section", + "text": {"type": "mrkdwn", "text": "Select a team"}, + "block_id": input_id_prefix + MANUAL_INCIDENT_TEAM_SELECT_ID, + "accessory": { + "type": "static_select", + "placeholder": {"type": "plain_text", "text": "Select a team", "emoji": True}, + "options": team_options, + "action_id": OnTeamChange.routing_uid(), + "initial_option": team_options[initial_option_idx], + }, + } + return team_select + + +def _get_selected_team_from_payload(payload, input_id_prefix): + Team = apps.get_model("user_management", "Team") + selected_team_id = payload["view"]["state"]["values"][input_id_prefix + MANUAL_INCIDENT_TEAM_SELECT_ID][ + OnTeamChange.routing_uid() + ]["selected_option"]["value"] + if selected_team_id == DEFAULT_TEAM_VALUE: + return None + team = Team.objects.filter(pk=selected_team_id).first() + return team + + +def _get_route_select(integration, value, input_id_prefix): + route_options = [] + initial_option_idx = 0 + for idx, route in enumerate(integration.channel_filters.all()): + filtering_term = f'"{route.filtering_term}"' + if route.is_default: + filtering_term = "default" + if value == route: + initial_option_idx = idx + route_options.append( + { + "text": { + "type": "plain_text", + "text": f"{filtering_term}", + "emoji": True, + }, + "value": f"{route.pk}", + } + ) + route_select = { + "type": "section", + "text": {"type": "mrkdwn", "text": "Select a route"}, + "block_id": input_id_prefix + MANUAL_INCIDENT_ROUTE_SELECT_ID, + "accessory": { + "type": "static_select", + "placeholder": {"type": "plain_text", "text": "Select a route", "emoji": True}, + "options": route_options, + "initial_option": route_options[initial_option_idx], + "action_id": OnRouteChange.routing_uid(), + }, + } + return route_select + + +def _get_selected_route_from_payload(payload, input_id_prefix): + ChannelFilter = apps.get_model("alerts", "ChannelFilter") + selected_org_id = payload["view"]["state"]["values"][input_id_prefix + MANUAL_INCIDENT_ROUTE_SELECT_ID][ + OnRouteChange.routing_uid() + ]["selected_option"]["value"] + channel_filter = ChannelFilter.objects.filter(pk=selected_org_id).first() + return channel_filter + + +def _get_and_change_input_id_prefix_from_metadata(metadata): + old_input_id_prefix = metadata["input_id_prefix"] + new_input_id_prefix = _generate_input_id_prefix() + metadata["input_id_prefix"] = new_input_id_prefix + return old_input_id_prefix, new_input_id_prefix, metadata + + +def _get_title_input(payload): + title_input_block = { + "type": "input", + "block_id": MANUAL_INCIDENT_TITLE_INPUT_ID, + "label": { + "type": "plain_text", + "text": "Title:", + }, + "element": { + "type": "plain_text_input", + "action_id": FinishCreateIncidentFromSlashCommand.routing_uid(), + "placeholder": { + "type": "plain_text", + "text": " ", + }, + }, + } + if payload.get("text", None) is not None: + title_input_block["element"]["initial_value"] = payload["text"] + return title_input_block + + +def _get_title_from_payload(payload): + title = payload["view"]["state"]["values"][MANUAL_INCIDENT_TITLE_INPUT_ID][ + FinishCreateIncidentFromSlashCommand.routing_uid() + ]["value"] + return title + + +def _get_message_input(payload): + message_input_block = { + "type": "input", + "block_id": MANUAL_INCIDENT_MESSAGE_INPUT_ID, + "label": { + "type": "plain_text", + "text": "Message:", + }, + "element": { + "type": "plain_text_input", + "action_id": FinishCreateIncidentFromSlashCommand.routing_uid(), + "multiline": True, + "placeholder": { + "type": "plain_text", + "text": " ", + }, + }, + "optional": True, + } + if payload.get("message", {}).get("text") is not None: + message_input_block["element"]["initial_value"] = payload["message"]["text"] + return message_input_block + + +def _get_message_from_payload(payload): + message = ( + payload["view"]["state"]["values"][MANUAL_INCIDENT_MESSAGE_INPUT_ID][ + FinishCreateIncidentFromSlashCommand.routing_uid() + ]["value"] + or "" + ) + return message + + +# _generate_input_id_prefix returns uniq str to not to preserve input's values between view update +# https://api.slack.com/methods/views.update#markdown +def _generate_input_id_prefix(): + return str(uuid4()) + + +STEPS_ROUTING = [ + { + "payload_type": scenario_step.PAYLOAD_TYPE_MESSAGE_ACTION, + "message_action_callback_id": StartCreateIncidentFromMessage.callback_id, + "step": StartCreateIncidentFromMessage, + }, + { + "payload_type": scenario_step.PAYLOAD_TYPE_BLOCK_ACTIONS, + "block_action_type": scenario_step.BLOCK_ACTION_TYPE_STATIC_SELECT, + "block_action_id": OnOrgChange.routing_uid(), + "step": OnOrgChange, + }, + { + "payload_type": scenario_step.PAYLOAD_TYPE_BLOCK_ACTIONS, + "block_action_type": scenario_step.BLOCK_ACTION_TYPE_STATIC_SELECT, + "block_action_id": OnTeamChange.routing_uid(), + "step": OnTeamChange, + }, + { + "payload_type": scenario_step.PAYLOAD_TYPE_BLOCK_ACTIONS, + "block_action_type": scenario_step.BLOCK_ACTION_TYPE_STATIC_SELECT, + "block_action_id": OnRouteChange.routing_uid(), + "step": OnRouteChange, + }, + { + "payload_type": scenario_step.PAYLOAD_TYPE_VIEW_SUBMISSION, + "view_callback_id": FinishCreateIncidentFromMessage.routing_uid(), + "step": FinishCreateIncidentFromMessage, + }, + { + "payload_type": scenario_step.PAYLOAD_TYPE_SLASH_COMMAND, + "command_name": StartCreateIncidentFromSlashCommand.command_name, + "step": StartCreateIncidentFromSlashCommand, + }, + { + "payload_type": scenario_step.PAYLOAD_TYPE_VIEW_SUBMISSION, + "view_callback_id": FinishCreateIncidentFromSlashCommand.routing_uid(), + "step": FinishCreateIncidentFromSlashCommand, + }, +] diff --git a/engine/apps/slack/scenarios/public_menu.py b/engine/apps/slack/scenarios/public_menu.py deleted file mode 100644 index c11a7f75..00000000 --- a/engine/apps/slack/scenarios/public_menu.py +++ /dev/null @@ -1,536 +0,0 @@ -import json -import logging - -from django.apps import apps -from django.conf import settings -from django.http import JsonResponse -from django.utils import timezone - -from apps.slack.scenarios import scenario_step -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import SlackAPIException - -from .step_mixins import CheckAlertIsUnarchivedMixin - -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) - - -class InvitedToChannelStep(scenario_step.ScenarioStep): - - tags = [ - scenario_step.ScenarioStep.TAG_TRIGGERED_BY_SYSTEM, - ] - - def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): - if payload["event"]["user"] == slack_team_identity.bot_user_id: - channel_id = payload["event"]["channel"] - slack_client = SlackClientWithErrorHandling(slack_team_identity.bot_access_token) - channel = slack_client.api_call("conversations.info", channel=channel_id)["channel"] - - slack_team_identity.cached_channels.update_or_create( - slack_id=channel["id"], - defaults={ - "name": channel["name"], - "is_archived": channel["is_archived"], - "is_shared": channel["is_shared"], - "last_populated": timezone.now().date(), - }, - ) - else: - logger.info("Other user was invited to a channel with a bot.") - - -class CloseEphemeralButtonStep(scenario_step.ScenarioStep): - - random_prefix_for_routing = "qwe2id" - - def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): - return JsonResponse({"response_type": "ephemeral", "delete_original": True}) - - -class CreateIncidentManuallyStep(scenario_step.ScenarioStep): - command_name = [settings.SLACK_SLASH_COMMAND_NAME] - tags = [ - scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, - ] - - TITLE_INPUT_BLOCK_ID = "TITLE_INPUT" - MESSAGE_INPUT_BLOCK_ID = "MESSAGE_INPUT" - - def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): - try: - channel_id = payload["event"]["channel"] - except KeyError: - channel_id = payload["channel_id"] - - blocks = self.get_create_incident_blocks(payload, slack_team_identity, slack_user_identity) - - view = { - "type": "modal", - "callback_id": FinishCreateIncidentViewStep.routing_uid(), - "title": { - "type": "plain_text", - "text": "Create an Incident", - }, - "close": { - "type": "plain_text", - "text": "Cancel", - "emoji": True, - }, - "submit": { - "type": "plain_text", - "text": "Submit", - }, - "blocks": blocks, - "private_metadata": json.dumps({"channel_id": channel_id}), - } - self._slack_client.api_call( - "views.open", - trigger_id=payload["trigger_id"], - view=view, - ) - - def get_create_incident_blocks(self, payload, slack_team_identity, slack_user_identity): - blocks = [] - organization_selection_block = self.get_select_organization_route_element( - slack_team_identity, slack_user_identity - ) - title_incident_block = { - "type": "input", - "block_id": self.TITLE_INPUT_BLOCK_ID, - "label": { - "type": "plain_text", - "text": "Title:", - }, - "element": { - "type": "plain_text_input", - "action_id": FinishCreateIncidentViewStep.routing_uid(), - "placeholder": { - "type": "plain_text", - "text": " ", - }, - }, - } - if payload.get("text", None) is not None: - title_incident_block["element"]["initial_value"] = payload["text"] - message_incident_block = { - "type": "input", - "block_id": self.MESSAGE_INPUT_BLOCK_ID, - "label": { - "type": "plain_text", - "text": "Message:", - }, - "element": { - "type": "plain_text_input", - "action_id": FinishCreateIncidentViewStep.routing_uid(), - "multiline": True, - "placeholder": { - "type": "plain_text", - "text": " ", - }, - }, - "optional": True, - } - if payload.get("message", {}).get("text") is not None: - message_incident_block["element"]["initial_value"] = payload["message"]["text"] - - blocks.append(organization_selection_block) - blocks.append(title_incident_block) - blocks.append(message_incident_block) - return blocks - - -class FinishCreateIncidentViewStep(scenario_step.ScenarioStep): - - tags = [ - scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, - ] - - def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): - AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel") - ChannelFilter = apps.get_model("alerts", "ChannelFilter") - - Alert = apps.get_model("alerts", "Alert") - payload_values = payload["view"]["state"]["values"] - title = payload_values[CreateIncidentManuallyStep.TITLE_INPUT_BLOCK_ID][self.routing_uid()]["value"] - text = payload_values[CreateIncidentManuallyStep.MESSAGE_INPUT_BLOCK_ID][self.routing_uid()]["value"] or "" - - private_metadata = json.loads(payload["view"]["private_metadata"]) - # update private metadata in payload to use it in alert rendering - payload["view"]["private_metadata"] = private_metadata - - channel_id = private_metadata["channel_id"] - - alert_receive_channel = AlertReceiveChannel.get_or_create_manual_integration( - organization=self.organization, - integration=AlertReceiveChannel.INTEGRATION_MANUAL, - deleted_at=None, - defaults={"author": self.user}, - ) - try: - self._slack_client.api_call( - "chat.postEphemeral", - channel=channel_id, - user=slack_user_identity.slack_id, - text=":white_check_mark: Alert *{}* successfully submitted".format(title), - ) - except SlackAPIException as e: - if e.response["error"] == "channel_not_found": - self._slack_client.api_call( - "chat.postEphemeral", - channel=slack_user_identity.im_channel_id, - user=slack_user_identity.slack_id, - text=":white_check_mark: Alert *{}* successfully submitted".format(title), - ) - else: - raise e - user_verbal = self.user.get_user_verbal_for_team_for_slack() - channel_filter_pk = payload["view"]["state"]["values"][ - scenario_step.ScenarioStep.SELECT_ORGANIZATION_AND_ROUTE_BLOCK_ID - ][scenario_step.ScenarioStep.SELECT_ORGANIZATION_AND_ROUTE_BLOCK_ID]["selected_option"]["value"].split("-")[1] - channel_filter = ChannelFilter.objects.get(pk=channel_filter_pk) - Alert.create( - title=title, - message="{} created by {}".format( - text, - user_verbal, - ), - image_url=None, - link_to_upstream_details=None, - alert_receive_channel=alert_receive_channel, - raw_request_data=payload, - integration_unique_data={ - "created_by": user_verbal, - }, - force_route_id=channel_filter.pk, - ) - - -class CreateIncidentSubmenuStep(scenario_step.ScenarioStep): - callback_id = [ - "incident_create", - "incident_create_staging", - "incident_create_develop", - ] - tags = [ - scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, - ] - - def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): - try: - image_url = payload["message"]["files"][0]["permalink"] - except KeyError: - image_url = None - channel_id = payload["channel"]["id"] - - private_metadata = { - "channel_id": channel_id, - "image_url": image_url, - "message": { - "user": payload["message"].get("user"), - "text": payload["message"].get("text"), - "ts": payload["message"].get("ts"), - }, - } - - organization_selection_block = self.get_select_organization_route_element( - slack_team_identity, slack_user_identity - ) - view = { - "type": "modal", - "callback_id": FinishCreateIncidentSubmenuStep.routing_uid(), - "title": { - "type": "plain_text", - "text": "Create an Incident", - }, - "close": { - "type": "plain_text", - "text": "Cancel", - "emoji": True, - }, - "submit": { - "type": "plain_text", - "text": "Submit", - }, - "blocks": [organization_selection_block], - "private_metadata": json.dumps(private_metadata), - } - self._slack_client.api_call( - "views.open", - trigger_id=payload["trigger_id"], - view=view, - ) - - -class FinishCreateIncidentSubmenuStep(scenario_step.ScenarioStep): - - tags = [ - scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, - ] - - def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): - AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel") - Alert = apps.get_model("alerts", "Alert") - - private_metadata = json.loads(payload["view"]["private_metadata"]) - # update private metadata in payload to use it in alert rendering - payload["view"]["private_metadata"] = private_metadata - - channel_id = private_metadata["channel_id"] - author = private_metadata["message"]["user"] - - alert_receive_channel = AlertReceiveChannel.get_or_create_manual_integration( - organization=self.organization, - integration=AlertReceiveChannel.INTEGRATION_MANUAL, - deleted_at=None, - defaults={"author": self.user}, - ) - - author_username = "Unknown" - if author: - try: - author_username = self._slack_client.api_call( - "users.info", - user=author, - ) - author_username = author_username.get("user", {}).get("real_name", None) - except SlackAPIException: - pass - payload["view"]["private_metadata"]["author_username"] = author_username - - try: - permalink = self._slack_client.api_call( - "chat.getPermalink", - channel=private_metadata["channel_id"], - message_ts=private_metadata["message"]["ts"], - ) - permalink = permalink.get("permalink", None) - except SlackAPIException: - permalink = None - - permalink = "<{}|Original message...>".format(permalink) if permalink is not None else "" - Alert.create( - title="Message from {}".format(author_username), - message="{}\n{}".format(private_metadata["message"]["text"], permalink), - image_url=private_metadata["image_url"], - # Link to the slack message is not here bc it redirects to browser - link_to_upstream_details=None, - alert_receive_channel=alert_receive_channel, - raw_request_data=payload, - integration_unique_data={"created_by": self.user.get_user_verbal_for_team_for_slack()}, - ) - try: - self._slack_client.api_call( - "chat.postEphemeral", - channel=channel_id, - user=slack_user_identity.slack_id, - text=":white_check_mark: Alert successfully submitted", - ) - except SlackAPIException as e: - if e.response["error"] == "channel_not_found" or e.response["error"] == "user_not_in_channel": - self._slack_client.api_call( - "chat.postEphemeral", - channel=slack_user_identity.im_channel_id, - user=slack_user_identity.slack_id, - text=":white_check_mark: Alert successfully submitted", - ) - else: - raise e - - -class AddToResolutionoteStep(CheckAlertIsUnarchivedMixin, scenario_step.ScenarioStep): - callback_id = [ - "add_resolution_note", - "add_resolution_note_staging", - "add_resolution_note_develop", - ] - tags = [ - scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, - ] - - def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): - SlackMessage = apps.get_model("slack", "SlackMessage") - ResolutionNoteSlackMessage = apps.get_model("alerts", "ResolutionNoteSlackMessage") - ResolutionNote = apps.get_model("alerts", "ResolutionNote") - SlackUserIdentity = apps.get_model("slack", "SlackUserIdentity") - - try: - channel_id = payload["channel"]["id"] - except KeyError: - raise Exception("Channel was not found") - - if self.organization and self.organization.general_log_channel_id is None: - try: - return self._slack_client.api_call( - "chat.postEphemeral", - channel=channel_id, - user=slack_user_identity.slack_id, - attachments=CreateIncidentSubmenuStep.finish_configuration_attachments(self.organization), - ) - except SlackAPIException as e: - if e.response["error"] == "channel_not_found" or e.response["error"] == "user_not_in_channel": - return self._slack_client.api_call( - "chat.postEphemeral", - channel=slack_user_identity.im_channel_id, - user=slack_user_identity.slack_id, - attachments=CreateIncidentSubmenuStep.finish_configuration_attachments(self.organization), - ) - else: - raise e - - warning_text = "Unable to add this message to resolution note, this command works only in incident threads." - - try: - slack_message = SlackMessage.objects.get( - slack_id=payload["message"]["thread_ts"], - _slack_team_identity=slack_team_identity, - channel_id=channel_id, - ) - except KeyError: - self.open_warning_window(payload, warning_text) - return - except SlackMessage.DoesNotExist: - self.open_warning_window(payload, warning_text) - return - - try: - alert_group = slack_message.get_alert_group() - except SlackMessage.alert.RelatedObjectDoesNotExist as e: - self.open_warning_window(payload, warning_text) - print( - f"Exception: tried to add message from thread to Resolution Note: " - f"Slack Team Identity pk: {self.slack_team_identity.pk}, " - f"Slack Message id: {slack_message.slack_id}" - ) - raise e - - if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group): - return - - if payload["message"]["type"] == "message" and "user" in payload["message"]: - message_ts = payload["message_ts"] - thread_ts = payload["message"]["thread_ts"] - - result = self._slack_client.api_call( - "chat.getPermalink", - channel=channel_id, - message_ts=message_ts, - ) - permalink = None - if result["permalink"] is not None: - permalink = result["permalink"] - - if payload["message"]["ts"] in [ - message.ts - for message in alert_group.resolution_note_slack_messages.filter(added_to_resolution_note=True) - ]: - warning_text = "Unable to add the same message again." - self.open_warning_window(payload, warning_text) - return - - elif len(payload["message"]["text"]) > 2900: - warning_text = ( - "Unable to add the message to Resolution note: the message is too long ({}). " - "Max length - 2900 symbols.".format(len(payload["message"]["text"])) - ) - self.open_warning_window(payload, warning_text) - return - - else: - try: - resolution_note_slack_message = ResolutionNoteSlackMessage.objects.get( - ts=message_ts, thread_ts=thread_ts - ) - except ResolutionNoteSlackMessage.DoesNotExist: - text = payload["message"]["text"] - text = text.replace("```", "") - slack_message = SlackMessage.objects.get( - slack_id=thread_ts, - _slack_team_identity=slack_team_identity, - channel_id=channel_id, - ) - alert_group = slack_message.get_alert_group() - author_slack_user_identity = SlackUserIdentity.objects.get( - slack_id=payload["message"]["user"], slack_team_identity=slack_team_identity - ) - author_user = self.organization.users.get(slack_user_identity=author_slack_user_identity) - resolution_note_slack_message = ResolutionNoteSlackMessage( - alert_group=alert_group, - user=author_user, - added_by_user=self.user, - text=text, - slack_channel_id=channel_id, - thread_ts=thread_ts, - ts=message_ts, - permalink=permalink, - ) - resolution_note_slack_message.added_to_resolution_note = True - resolution_note_slack_message.save() - resolution_note = resolution_note_slack_message.get_resolution_note() - if resolution_note is None: - ResolutionNote( - alert_group=alert_group, - author=resolution_note_slack_message.user, - source=ResolutionNote.Source.SLACK, - resolution_note_slack_message=resolution_note_slack_message, - ).save() - else: - resolution_note.recreate() - alert_group.drop_cached_after_resolve_report_json() - alert_group.schedule_cache_for_web() - try: - self._slack_client.api_call( - "reactions.add", - channel=channel_id, - name="memo", - timestamp=resolution_note_slack_message.ts, - ) - except SlackAPIException: - pass - - self._update_slack_message(alert_group) - else: - warning_text = "Unable to add this message to resolution note." - self.open_warning_window(payload, warning_text) - return - - -STEPS_ROUTING = [ - { - "payload_type": scenario_step.PAYLOAD_TYPE_SLASH_COMMAND, - "command_name": CreateIncidentManuallyStep.command_name, - "step": CreateIncidentManuallyStep, - }, - { - "payload_type": scenario_step.PAYLOAD_TYPE_EVENT_CALLBACK, - "event_type": scenario_step.EVENT_TYPE_MEMBER_JOINED_CHANNEL, - "step": InvitedToChannelStep, - }, - { - "payload_type": scenario_step.PAYLOAD_TYPE_INTERACTIVE_MESSAGE, - "action_type": scenario_step.ACTION_TYPE_BUTTON, - "action_name": CloseEphemeralButtonStep.routing_uid(), - "step": CloseEphemeralButtonStep, - }, - { - "payload_type": scenario_step.PAYLOAD_TYPE_VIEW_SUBMISSION, - "view_callback_id": FinishCreateIncidentViewStep.routing_uid(), - "step": FinishCreateIncidentViewStep, - }, - { - "payload_type": scenario_step.PAYLOAD_TYPE_VIEW_SUBMISSION, - "view_callback_id": FinishCreateIncidentSubmenuStep.routing_uid(), - "step": FinishCreateIncidentSubmenuStep, - }, - { - "payload_type": scenario_step.PAYLOAD_TYPE_MESSAGE_ACTION, - "message_action_callback_id": CreateIncidentSubmenuStep.callback_id, - "step": CreateIncidentSubmenuStep, - }, - { - "payload_type": scenario_step.PAYLOAD_TYPE_MESSAGE_ACTION, - "message_action_callback_id": AddToResolutionoteStep.callback_id, - "step": AddToResolutionoteStep, - }, -] diff --git a/engine/apps/slack/scenarios/resolution_note.py b/engine/apps/slack/scenarios/resolution_note.py index f8bea2c3..e979cb96 100644 --- a/engine/apps/slack/scenarios/resolution_note.py +++ b/engine/apps/slack/scenarios/resolution_note.py @@ -15,6 +15,144 @@ from .step_mixins import CheckAlertIsUnarchivedMixin logger = logging.getLogger(__name__) +class AddToResolutionNoteStep(CheckAlertIsUnarchivedMixin, scenario_step.ScenarioStep): + callback_id = [ + "add_resolution_note", + "add_resolution_note_staging", + "add_resolution_note_develop", + ] + tags = [ + scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, + ] + + def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): + SlackMessage = apps.get_model("slack", "SlackMessage") + ResolutionNoteSlackMessage = apps.get_model("alerts", "ResolutionNoteSlackMessage") + ResolutionNote = apps.get_model("alerts", "ResolutionNote") + SlackUserIdentity = apps.get_model("slack", "SlackUserIdentity") + + try: + channel_id = payload["channel"]["id"] + except KeyError: + raise Exception("Channel was not found") + + warning_text = "Unable to add this message to resolution note, this command works only in incident threads." + + try: + slack_message = SlackMessage.objects.get( + slack_id=payload["message"]["thread_ts"], + _slack_team_identity=slack_team_identity, + channel_id=channel_id, + ) + except KeyError: + self.open_warning_window(payload, warning_text) + return + except SlackMessage.DoesNotExist: + self.open_warning_window(payload, warning_text) + return + + try: + alert_group = slack_message.get_alert_group() + except SlackMessage.alert.RelatedObjectDoesNotExist as e: + self.open_warning_window(payload, warning_text) + print( + f"Exception: tried to add message from thread to Resolution Note: " + f"Slack Team Identity pk: {self.slack_team_identity.pk}, " + f"Slack Message id: {slack_message.slack_id}" + ) + raise e + + if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group): + return + + if payload["message"]["type"] == "message" and "user" in payload["message"]: + message_ts = payload["message_ts"] + thread_ts = payload["message"]["thread_ts"] + + result = self._slack_client.api_call( + "chat.getPermalink", + channel=channel_id, + message_ts=message_ts, + ) + permalink = None + if result["permalink"] is not None: + permalink = result["permalink"] + + if payload["message"]["ts"] in [ + message.ts + for message in alert_group.resolution_note_slack_messages.filter(added_to_resolution_note=True) + ]: + warning_text = "Unable to add the same message again." + self.open_warning_window(payload, warning_text) + return + + elif len(payload["message"]["text"]) > 2900: + warning_text = ( + "Unable to add the message to Resolution note: the message is too long ({}). " + "Max length - 2900 symbols.".format(len(payload["message"]["text"])) + ) + self.open_warning_window(payload, warning_text) + return + + else: + try: + resolution_note_slack_message = ResolutionNoteSlackMessage.objects.get( + ts=message_ts, thread_ts=thread_ts + ) + except ResolutionNoteSlackMessage.DoesNotExist: + text = payload["message"]["text"] + text = text.replace("```", "") + slack_message = SlackMessage.objects.get( + slack_id=thread_ts, + _slack_team_identity=slack_team_identity, + channel_id=channel_id, + ) + alert_group = slack_message.get_alert_group() + author_slack_user_identity = SlackUserIdentity.objects.get( + slack_id=payload["message"]["user"], slack_team_identity=slack_team_identity + ) + author_user = self.organization.users.get(slack_user_identity=author_slack_user_identity) + resolution_note_slack_message = ResolutionNoteSlackMessage( + alert_group=alert_group, + user=author_user, + added_by_user=self.user, + text=text, + slack_channel_id=channel_id, + thread_ts=thread_ts, + ts=message_ts, + permalink=permalink, + ) + resolution_note_slack_message.added_to_resolution_note = True + resolution_note_slack_message.save() + resolution_note = resolution_note_slack_message.get_resolution_note() + if resolution_note is None: + ResolutionNote( + alert_group=alert_group, + author=resolution_note_slack_message.user, + source=ResolutionNote.Source.SLACK, + resolution_note_slack_message=resolution_note_slack_message, + ).save() + else: + resolution_note.recreate() + alert_group.drop_cached_after_resolve_report_json() + alert_group.schedule_cache_for_web() + try: + self._slack_client.api_call( + "reactions.add", + channel=channel_id, + name="memo", + timestamp=resolution_note_slack_message.ts, + ) + except SlackAPIException: + pass + + self._update_slack_message(alert_group) + else: + warning_text = "Unable to add this message to resolution note." + self.open_warning_window(payload, warning_text) + return + + class UpdateResolutionNoteStep(scenario_step.ScenarioStep): def process_signal(self, alert_group, resolution_note): if resolution_note.deleted_at: @@ -625,4 +763,9 @@ STEPS_ROUTING = [ "block_action_id": AddRemoveThreadMessageStep.routing_uid(), "step": AddRemoveThreadMessageStep, }, + { + "payload_type": scenario_step.PAYLOAD_TYPE_MESSAGE_ACTION, + "message_action_callback_id": AddToResolutionNoteStep.callback_id, + "step": AddToResolutionNoteStep, + }, ] diff --git a/engine/apps/slack/scenarios/scenario_step.py b/engine/apps/slack/scenarios/scenario_step.py index f2b51173..b49fc4c5 100644 --- a/engine/apps/slack/scenarios/scenario_step.py +++ b/engine/apps/slack/scenarios/scenario_step.py @@ -135,28 +135,6 @@ class ScenarioStep(object): channel = user.im_channel_id return channel - @staticmethod - def finish_configuration_attachments(organization): - text = ( - f"A few steps left to finish configuration!\n" - f"Go to your <{organization.web_link}?page=slack|OnCall workspace> and select default channel " - f"for your incidents!" - ) - return [ - { - "color": "#008000", - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": text, - }, - } - ], - } - ] - @classmethod def routing_uid(cls): return cls.random_prefix_for_routing + cls.__name__ @@ -431,55 +409,3 @@ class ScenarioStep(object): element["initial_option"] = initial_option return element - - def get_select_organization_route_element(self, slack_team_identity, slack_user_identity): - AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel") - - organizations = slack_team_identity.organizations.filter( - users__slack_user_identity=slack_user_identity - ).distinct() - organizations_options = [] - - for organization in organizations: - manual_integration = AlertReceiveChannel.get_or_create_manual_integration( - organization=organization, - integration=AlertReceiveChannel.INTEGRATION_MANUAL, - deleted_at=None, - defaults={"author": self.user}, - ) - - for route in manual_integration.channel_filters.all(): - filtering_term = f'"{route.filtering_term}"' - if route.is_default: - filtering_term = "default" - organizations_options.append( - { - "text": { - "type": "plain_text", - "text": f"{organization.org_title}: {filtering_term}", - "emoji": True, - }, - "value": f"{organization.pk}-{route.pk}", - } - ) - - organization_selection_block = { - "type": "input", - "block_id": ScenarioStep.SELECT_ORGANIZATION_AND_ROUTE_BLOCK_ID, - "element": { - "type": "static_select", - "placeholder": { - "type": "plain_text", - "text": "Select organization", - }, - "action_id": ScenarioStep.SELECT_ORGANIZATION_AND_ROUTE_BLOCK_ID, - "options": organizations_options, - "initial_option": organizations_options[0], - }, - "label": { - "type": "plain_text", - "text": "Select organization and route:", - "emoji": True, - }, - } - return organization_selection_block diff --git a/engine/apps/slack/views.py b/engine/apps/slack/views.py index 5990b0b6..a9818c0c 100644 --- a/engine/apps/slack/views.py +++ b/engine/apps/slack/views.py @@ -16,11 +16,12 @@ from apps.auth_token.auth import PluginAuthentication from apps.base.utils import live_settings from apps.slack.scenarios.alertgroup_appearance import STEPS_ROUTING as ALERTGROUP_APPEARANCE_ROUTING from apps.slack.scenarios.distribute_alerts import STEPS_ROUTING as DISTRIBUTION_STEPS_ROUTING +from apps.slack.scenarios.invited_to_channel import STEPS_ROUTING as INVITED_TO_CHANNEL_ROUTING +from apps.slack.scenarios.manual_incident import STEPS_ROUTING as MANUAL_INCIDENT_ROUTING # Importing routes from scenarios from apps.slack.scenarios.onboarding import STEPS_ROUTING as ONBOARDING_STEPS_ROUTING from apps.slack.scenarios.profile_update import STEPS_ROUTING as PROFILE_UPDATE_ROUTING -from apps.slack.scenarios.public_menu import STEPS_ROUTING as PUBLIC_MENU_ROUTING from apps.slack.scenarios.resolution_note import STEPS_ROUTING as RESOLUTION_NOTE_ROUTING from apps.slack.scenarios.scenario_step import ( EVENT_SUBTYPE_BOT_MESSAGE, @@ -57,7 +58,7 @@ from .models import SlackActionRecord, SlackMessage, SlackTeamIdentity, SlackUse SCENARIOS_ROUTES = [] # Add all other routes here SCENARIOS_ROUTES.extend(ONBOARDING_STEPS_ROUTING) SCENARIOS_ROUTES.extend(DISTRIBUTION_STEPS_ROUTING) -SCENARIOS_ROUTES.extend(PUBLIC_MENU_ROUTING) +SCENARIOS_ROUTES.extend(INVITED_TO_CHANNEL_ROUTING) SCENARIOS_ROUTES.extend(SCHEDULES_ROUTING) SCENARIOS_ROUTES.extend(SLACK_CHANNEL_INTEGRATION_ROUTING) SCENARIOS_ROUTES.extend(ALERTGROUP_APPEARANCE_ROUTING) @@ -65,6 +66,7 @@ SCENARIOS_ROUTES.extend(RESOLUTION_NOTE_ROUTING) SCENARIOS_ROUTES.extend(SLACK_USERGROUP_UPDATE_ROUTING) SCENARIOS_ROUTES.extend(CHANNEL_ROUTING) SCENARIOS_ROUTES.extend(PROFILE_UPDATE_ROUTING) +SCENARIOS_ROUTES.extend(MANUAL_INCIDENT_ROUTING) logger = logging.getLogger(__name__) diff --git a/engine/config_integrations/manual.py b/engine/config_integrations/manual.py index bf1825de..43f4852b 100644 --- a/engine/config_integrations/manual.py +++ b/engine/config_integrations/manual.py @@ -12,39 +12,28 @@ is_demo_alert_enabled = False description = None # Default templates -slack_title = """{% set metadata = payload.view.private_metadata %} -{%-if "message" in metadata -%} -{% set title = "Message from @" + metadata.author_username %} -{%- else -%} -{% set title = payload.view.state["values"].TITLE_INPUT.FinishCreateIncidentViewStep.value %} -{%- endif -%} -*<{{ grafana_oncall_link }}|#{{ grafana_oncall_incident_id }} {{ title }}>* via {{ integration_name }} +slack_title = """\ +*<{{ grafana_oncall_link }}|#{{ grafana_oncall_incident_id }} {{ payload.oncall.title }}>* via {{ integration_name }} {% if source_link %} (*<{{ source_link }}|source>*) -{%- endif %} +{% endif %} """ -slack_message = """{% set metadata = payload.view.private_metadata %} -{% if "message" in metadata -%} -{{ metadata.message.text }} +slack_message = """{{ payload.oncall.message }} - -{%- else -%} -{{ payload.view.state["values"].MESSAGE_INPUT.FinishCreateIncidentViewStep.value }} - -created by {{ payload.user.name }} -{%- endif -%}""" +created by {{ payload.oncall.author_username }} +""" slack_image_url = None -web_title = """{% set metadata = payload.view.private_metadata %} -{%-if "message" in metadata -%} -{{ "Message from @" + metadata.author_username }} -{%- else -%} -{{ payload.view.state["values"].TITLE_INPUT.FinishCreateIncidentViewStep.value }} -{%- endif -%}""" +web_title = "{{ payload.oncall.title }}" -web_message = slack_message +web_message = """{{ payload.oncall.message }} +{% if source_link %} +<{{ source_link }} | Link to the original message > +{% endif %} +created by {{ payload.oncall.author_username }} +""" web_image_url = slack_image_url @@ -62,11 +51,7 @@ telegram_message = slack_message telegram_image_url = slack_image_url -source_link = """\ -{% set metadata = payload.view.private_metadata %} -{%- if "message" in metadata %} -https://{{ payload.team.domain }}.slack.com/archives/{{ payload.channel.id }}/{{ payload.message.ts }} -{% endif -%}""" +source_link = "{{ payload.oncall.permalink }}" grouping_id = """{{ payload }}"""