oncall-engine/engine/apps/slack/scenarios/slack_channel_integration.py
Joey Orlando a29e35c25a
refactor SlackMessage.channel_id (CHAR field) to SlackMessage.channel (foreign key relationship) (#5292)
# What this PR does

Related to https://github.com/grafana/oncall-private/issues/2947

**NOTE**

This PR introduces steps 1 and 2 of the 3 part migration proposed
[here](https://raintank-corp.slack.com/archives/C06K1MQ07GS/p1732555465144099).
Step 3, swapping reads to be from the new-column and dropping
dual-writes, will be done in a future PR/release.

---

I’m tackling this work now because _ultimately_ I want to move
`AlertReceiveChannel.rate_limited_in_slack_at` to
`SlackChannel.rate_limited_at` , but first I sorta need to refactor
`SlackMessage.channel_id` from a `CHAR` field to a foreign key
relationship (because in the spots where we touch Slack rate limiting,
like
[here](https://github.com/grafana/oncall/blob/dev/engine/apps/slack/alert_group_slack_service.py#L42-L50)
for example, we only have `slack_message.channel_id`, which means I need
to do extra queries to fetch the appropriate `SlackChannel` to then be
able to get/set `SlackChannel.rate_limited_at`

Other minor stuffs:
- it also prepares us to drop `SlackMessage._slack_team_identity`. We
already have a `@property` of `SlackMessage.slack_team_identity` (which
[previously had some hacky
logic](https://github.com/grafana/oncall/blob/dev/engine/apps/slack/models/slack_message.py#L74-L84)).
I've refactored `SlackMessage.slack_team_identity` to simply point to
`self.organization.slack_team_identity` + updated our code to _stop_
setting `SlackMessage._slack_team_identity` (will drop this column in
future release)

## 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.
2024-11-26 11:03:38 +00:00

163 lines
6.3 KiB
Python

import logging
import typing
from apps.slack.scenarios import scenario_step
from apps.slack.scenarios.resolution_note import RESOLUTION_NOTE_EXCEPTIONS
from apps.slack.types import EventPayload, EventType, MessageEventSubtype, PayloadType, ScenarioRoute
if typing.TYPE_CHECKING:
from apps.slack.models import SlackTeamIdentity, SlackUserIdentity
from apps.user_management.models import Organization
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class SlackChannelMessageEventStep(scenario_step.ScenarioStep):
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
"""
Triggered by action: Any new message in channel.
Dangerous because it's often triggered by internal client's company systems.
May cause flood, should be ready for useless updates.
"""
# If it is a message from thread - save it for resolution note
if ("thread_ts" in payload["event"] and "subtype" not in payload["event"]) or (
payload["event"].get("subtype") == MessageEventSubtype.MESSAGE_CHANGED
and "subtype" not in payload["event"]["message"]
and "thread_ts" in payload["event"]["message"]
):
self.save_thread_message_for_resolution_note(slack_user_identity, payload)
elif (
payload["event"].get("subtype") == MessageEventSubtype.MESSAGE_DELETED
and "thread_ts" in payload["event"]["previous_message"]
):
self.delete_thread_message_from_resolution_note(slack_user_identity, payload)
def save_thread_message_for_resolution_note(
self, slack_user_identity: "SlackUserIdentity", payload: EventPayload
) -> None:
from apps.alerts.models import ResolutionNoteSlackMessage
from apps.slack.models import SlackChannel, SlackMessage
if slack_user_identity is None:
logger.warning(
f"Empty slack_user_identity in PublicMainMenu step:\n"
f"{self.slack_team_identity} {self.slack_team_identity.pk}"
)
return
channel_id = payload["event"]["channel"]
thread_ts = payload["event"].get("thread_ts") or payload["event"]["message"]["thread_ts"]
# sometimes we get messages with empty text, probably because it's an image or attachment
event_text = payload["event"].get("text")
event_text = "empty message" if event_text == "" else event_text
text = event_text or payload["event"]["message"]["text"]
if "message" in payload["event"]:
message_ts = payload["event"]["message"]["ts"]
else:
message_ts = payload["event"]["ts"]
try:
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p1732555465144099
slack_message = SlackMessage.objects.get(
slack_id=thread_ts,
organization__slack_team_identity=self.slack_team_identity,
_channel_id=channel_id,
# channel__slack_id=channel_id,
)
except SlackMessage.DoesNotExist:
return
try:
slack_channel = SlackChannel.objects.get(slack_id=channel_id, slack_team_identity=self.slack_team_identity)
except SlackChannel.DoesNotExist:
return
if not slack_message.alert_group:
# SlackMessage instances without alert_group set (e.g., SSR Slack messages)
return
try:
result = self._slack_client.chat_getPermalink(channel=channel_id, message_ts=message_ts)
except RESOLUTION_NOTE_EXCEPTIONS:
return
permalink = None
if result["permalink"] is not None:
permalink = result["permalink"]
if len(text) > 2900:
self._slack_client.chat_postEphemeral(
channel=channel_id,
user=slack_user_identity.slack_id,
text=":warning: Unable to show the <{}|message> in Resolution Note: the message is too long ({}). "
"Max length - 2900 symbols.".format(permalink, len(text)),
)
return
slack_thread_message, created = ResolutionNoteSlackMessage.objects.get_or_create(
ts=message_ts,
thread_ts=thread_ts,
alert_group=slack_message.alert_group,
defaults={
"user": self.user,
"added_by_user": self.user,
"text": text,
"slack_channel": slack_channel,
"permalink": permalink,
},
)
if not created:
slack_thread_message.text = text
slack_thread_message.save()
def delete_thread_message_from_resolution_note(
self, slack_user_identity: "SlackUserIdentity", payload: EventPayload
) -> None:
from apps.alerts.models import ResolutionNoteSlackMessage
if slack_user_identity is None:
logger.warning(
f"Empty slack_user_identity in PublicMainMenu step:\n"
f"{self.slack_team_identity} {self.slack_team_identity.pk}"
)
return
channel_id = payload["event"]["channel"]
message_ts = payload["event"]["previous_message"]["ts"]
thread_ts = payload["event"]["previous_message"]["thread_ts"]
try:
slack_thread_message = ResolutionNoteSlackMessage.objects.get(
ts=message_ts,
thread_ts=thread_ts,
slack_channel__slack_id=channel_id,
)
except ResolutionNoteSlackMessage.DoesNotExist:
pass
else:
alert_group = slack_thread_message.alert_group
slack_thread_message.delete()
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
STEPS_ROUTING: ScenarioRoute.RoutingSteps = [
typing.cast(
ScenarioRoute.EventCallbackScenarioRoute,
{
"payload_type": PayloadType.EVENT_CALLBACK,
"event_type": EventType.MESSAGE,
"message_channel_type": EventType.MESSAGE_CHANNEL,
"step": SlackChannelMessageEventStep,
},
),
]