# What this PR does - Stops writing `SlackMessage.organization` + removes references to this field. [As we discussed](https://raintank-corp.slack.com/archives/C083TU81TCH/p1733315887463279?thread_ts=1733311105.095309&cid=C083TU81TCH), we do not need this field on this model/table, `SlackMessage._slack_team_identity` is sufficient (`organization` will be dropped in a separate PR) - Adds a data migration script which: - drops orphaned `SlackMessage` records; ie. ones which, even after the [`engine/apps/slack/migrations/0007_migrate_slackmessage_channel_id.py`](https://github.com/grafana/oncall/blob/dev/engine/apps/slack/migrations/0007_migrate_slackmessage_channel_id.py) migration, still don't have a `SlackMessage.channel` id filled in (we discussed + agreed on dropping these records [here](https://raintank-corp.slack.com/archives/C083TU81TCH/p1733329914516859?thread_ts=1733311105.095309&cid=C083TU81TCH)) - fills in empty `SlackMessage.slack_team_identity` values (from `slack_message.channel.slack_team_identity`) ### Other notes On the `organization` topic. We store records in `SlackMessage` for two purposes (AFAICT), and in both cases, we have references back to the `organization`: - alert groups - `slack_message.alert_group.channel.organization` - shift swap requests - `shift_swap_request.schedule.organization` ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] 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.
184 lines
7.1 KiB
Python
184 lines
7.1 KiB
Python
import json
|
|
import logging
|
|
|
|
from apps.alerts.models import AlertGroup
|
|
from apps.api.permissions import user_is_authorized
|
|
from apps.slack.models import SlackChannel, SlackMessage, SlackTeamIdentity
|
|
from apps.slack.types import EventPayload
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AlertGroupActionsMixin:
|
|
"""
|
|
Mixin for alert group actions (ack, resolve, etc.). Intended to be used as a mixin along with ScenarioStep.
|
|
"""
|
|
|
|
def get_alert_group(self, slack_team_identity: SlackTeamIdentity, payload: EventPayload) -> AlertGroup:
|
|
"""
|
|
Get AlertGroup instance on Slack message button click or select menu change.
|
|
"""
|
|
|
|
alert_group = (
|
|
self._get_alert_group_from_action(payload) # Try to get alert_group_pk from PRESSED button
|
|
or self._get_alert_group_from_message(payload) # Try to use alert_group_pk from ANY button in message
|
|
or self._get_alert_group_from_slack_message_in_db(slack_team_identity, payload) # Fetch message from DB
|
|
)
|
|
assert alert_group is not None, "AlertGroup not found"
|
|
|
|
# Repair alert group if Slack message is orphaned
|
|
if alert_group.slack_message is None:
|
|
self._repair_alert_group(slack_team_identity, alert_group, payload)
|
|
|
|
return alert_group
|
|
|
|
def is_authorized(self, alert_group: AlertGroup) -> bool:
|
|
"""
|
|
Customize ScenarioStep.is_authorized method to check for alert group permissions.
|
|
"""
|
|
|
|
return (
|
|
self.user is not None
|
|
and self.user.organization == alert_group.channel.organization
|
|
and user_is_authorized(self.user, self.REQUIRED_PERMISSIONS)
|
|
)
|
|
|
|
def _repair_alert_group(
|
|
self,
|
|
slack_team_identity: SlackTeamIdentity,
|
|
alert_group: AlertGroup,
|
|
payload: EventPayload,
|
|
) -> None:
|
|
"""
|
|
There's a possibility that OnCall failed to create a `SlackMessage` instance for an `AlertGroup`,
|
|
but the message was sent to Slack. This method creates `SlackMessage` instance for such orphaned messages.
|
|
"""
|
|
try:
|
|
message_id = payload["message"]["ts"]
|
|
except KeyError:
|
|
message_id = payload["original_message"]["ts"]
|
|
|
|
slack_channel = SlackChannel.objects.get(
|
|
slack_id=payload["channel"]["id"],
|
|
slack_team_identity=slack_team_identity,
|
|
)
|
|
|
|
SlackMessage.objects.create(
|
|
slack_id=message_id,
|
|
_slack_team_identity=slack_team_identity,
|
|
channel=slack_channel,
|
|
alert_group=alert_group,
|
|
)
|
|
|
|
def _get_alert_group_from_action(self, payload: EventPayload) -> AlertGroup | None:
|
|
"""
|
|
Get AlertGroup instance from action data in payload. Action data is data encoded into buttons and select
|
|
menus in apps.alerts.incident_appearance.renderers.slack_renderer.AlertGroupSlackRenderer._get_buttons_blocks.
|
|
"""
|
|
|
|
action = payload["actions"][0]
|
|
action_type = action["type"]
|
|
|
|
if action_type == "button":
|
|
value_string = action["value"]
|
|
elif action_type == "static_select":
|
|
value_string = action["selected_option"]["value"]
|
|
else:
|
|
raise ValueError(f"Unexpected action type: {action_type}")
|
|
|
|
try:
|
|
value = json.loads(value_string)
|
|
except (TypeError, json.JSONDecodeError):
|
|
return None
|
|
|
|
# New slack messages from OnCall contain alert group primary key
|
|
try:
|
|
alert_group_ppk = value["alert_group_ppk"]
|
|
return AlertGroup.objects.get(public_primary_key=alert_group_ppk)
|
|
except (KeyError, TypeError):
|
|
pass
|
|
|
|
try:
|
|
alert_group_pk = value["alert_group_pk"]
|
|
organization_pk = value["organization_id"]
|
|
except (KeyError, TypeError):
|
|
return None
|
|
|
|
try:
|
|
# check organization as well for cases when the organization was migrated from "us" cluster to "eu" and
|
|
# slack message has an outdated payload with incorrect alert group id
|
|
alert_group = AlertGroup.objects.get(pk=alert_group_pk, channel__organization_id=organization_pk)
|
|
except AlertGroup.DoesNotExist:
|
|
return None
|
|
|
|
return alert_group
|
|
|
|
def _get_alert_group_from_message(self, payload: EventPayload) -> AlertGroup | None:
|
|
"""
|
|
Get AlertGroup instance from message data in payload. It's similar to _get_alert_group_from_action,
|
|
but it tries to get alert_group_pk from ANY button in the message, not just the one that was clicked.
|
|
"""
|
|
|
|
try:
|
|
# sometimes message is in "original_message" field, not "message"
|
|
message = payload.get("message") or payload["original_message"]
|
|
elements = message["attachments"][0]["blocks"][0]["elements"]
|
|
except (KeyError, IndexError):
|
|
return None
|
|
|
|
for element in elements:
|
|
value_string = element.get("value")
|
|
if not value_string:
|
|
continue
|
|
|
|
try:
|
|
value = json.loads(value_string)
|
|
except (TypeError, json.JSONDecodeError):
|
|
continue
|
|
|
|
# New slack messages from OnCall contain alert group primary key
|
|
try:
|
|
alert_group_ppk = value["alert_group_ppk"]
|
|
return AlertGroup.objects.get(public_primary_key=alert_group_ppk)
|
|
except (KeyError, TypeError):
|
|
pass
|
|
|
|
try:
|
|
alert_group_pk = value["alert_group_pk"]
|
|
organization_pk = value["organization_id"]
|
|
except (KeyError, TypeError):
|
|
continue
|
|
|
|
try:
|
|
# check the organization as well for cases organization was migrated from "us" cluster to "eu" and
|
|
# the slack message has an outdated payload with incorrect alert group id
|
|
alert_group = AlertGroup.objects.get(pk=alert_group_pk, channel__organization_id=organization_pk)
|
|
except AlertGroup.DoesNotExist:
|
|
return None
|
|
|
|
return alert_group
|
|
return None
|
|
|
|
def _get_alert_group_from_slack_message_in_db(
|
|
self, slack_team_identity: SlackTeamIdentity, payload: EventPayload
|
|
) -> AlertGroup | None:
|
|
"""
|
|
Get AlertGroup instance from SlackMessage instance.
|
|
Old messages may not have alert_group_pk encoded into buttons, so we need to query SlackMessage to figure out
|
|
the AlertGroup.
|
|
"""
|
|
|
|
message_ts = payload.get("message_ts") or payload["container"]["message_ts"] # interactive message or block
|
|
channel_id = payload["channel"]["id"]
|
|
|
|
# All Slack messages from OnCall should have alert_group_pk encoded into buttons, so reaching this point means
|
|
# something probably went wrong.
|
|
logger.warning(f"alert_group_pk not found in payload, fetching SlackMessage from DB. message_ts: {message_ts}")
|
|
|
|
# Get SlackMessage from DB
|
|
slack_message = SlackMessage.objects.get(
|
|
slack_id=message_ts,
|
|
_slack_team_identity=slack_team_identity,
|
|
channel__slack_id=channel_id,
|
|
)
|
|
return slack_message.alert_group
|