oncall-engine/engine/apps/slack/models/slack_message.py
Michael Derynck 33a0c15b75
Truncate slack block text so it is not rejected by slack API (#5121)
# What this PR does
Truncates text for slack message to avoid this error:
```
File "/usr/local/lib/python3.12/site-packages/slack_sdk/web/slack_response.py", line 199, in validate
    raise e.SlackApiError(message=msg, response=self)
slack_sdk.errors.SlackApiError: The request to the Slack API failed. (url: https://www.slack.com/api/chat.postMessage)
The server responded with: {'ok': False, 'error': 'invalid_blocks', 'errors': ['failed to match all allowed schemas [json-pointer:/blocks/0/text]', 'must be less than 3001 characters [json-pointer:/blocks/0/text/text]'], 'response_metadata': {'messages': ['[ERROR] failed to match all allowed schemas [json-pointer:/blocks/0/text]', '[ERROR] must be less than 3001 characters [json-pointer:/blocks/0/text/text]']}}
```

## 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
- [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-10-03 18:45:23 +00:00

220 lines
8.7 KiB
Python

import logging
import time
import typing
import uuid
from django.db import models
from apps.slack.client import SlackClient
from apps.slack.constants import BLOCK_SECTION_TEXT_MAX_SIZE
from apps.slack.errors import (
SlackAPIChannelArchivedError,
SlackAPIError,
SlackAPIFetchMembersFailedError,
SlackAPIMethodNotSupportedForChannelTypeError,
SlackAPIRatelimitError,
SlackAPITokenError,
)
if typing.TYPE_CHECKING:
from apps.alerts.models import AlertGroup
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class SlackMessage(models.Model):
alert_group: typing.Optional["AlertGroup"]
id = models.CharField(primary_key=True, default=uuid.uuid4, editable=False, max_length=36)
slack_id = models.CharField(max_length=100)
channel_id = models.CharField(max_length=100, null=True, default=None)
organization = models.ForeignKey(
"user_management.Organization", on_delete=models.CASCADE, null=True, default=None, related_name="slack_message"
)
_slack_team_identity = models.ForeignKey(
"slack.SlackTeamIdentity",
on_delete=models.PROTECT,
null=True,
default=None,
related_name="slack_message",
db_column="slack_team_identity",
)
ack_reminder_message_ts = models.CharField(max_length=100, null=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)
cached_permalink = models.URLField(max_length=250, null=True, default=None)
last_updated = models.DateTimeField(null=True, default=None)
alert_group = models.ForeignKey(
"alerts.AlertGroup",
on_delete=models.CASCADE,
null=True,
default=None,
related_name="slack_messages",
)
# ID of a latest celery task to update the message
active_update_task_id = models.CharField(max_length=100, null=True, default=None)
class Meta:
# slack_id is unique within the context of a channel or conversation
constraints = [
models.UniqueConstraint(fields=["slack_id", "channel_id", "_slack_team_identity"], name="unique slack_id")
]
@property
def slack_team_identity(self):
if self._slack_team_identity is None:
if self.organization is None: # strange case when organization is None
logger.warning(
f"SlackMessage (pk: {self.pk}) fields _slack_team_identity and organization is None. "
f"It is strange!"
)
return None
self._slack_team_identity = self.organization.slack_team_identity
self.save()
return self._slack_team_identity
@property
def permalink(self) -> typing.Optional[str]:
# Don't send request for permalink if there is no slack_team_identity or slack token has been revoked
if self.cached_permalink or not self.slack_team_identity or self.slack_team_identity.detected_token_revoked:
return self.cached_permalink
try:
result = SlackClient(self.slack_team_identity).chat_getPermalink(
channel=self.channel_id, message_ts=self.slack_id
)
except SlackAPIError:
return None
self.cached_permalink = result["permalink"]
self.save(update_fields=["cached_permalink"])
return self.cached_permalink
@property
def deep_link(self) -> str:
return f"https://slack.com/app_redirect?channel={self.channel_id}&team={self.slack_team_identity.slack_id}&message={self.slack_id}"
def send_slack_notification(self, user, alert_group, notification_policy):
from apps.base.models import UserNotificationPolicyLogRecord
slack_message = alert_group.slack_message
user_verbal = user.get_username_with_slack_verbal(mention=True)
slack_user_identity = user.slack_user_identity
if slack_user_identity is None:
text = "{}\nTried to invite {} to look at the alert group. Unfortunately {} is not in slack.".format(
alert_group.long_verbose_name, user_verbal, user_verbal
)
UserNotificationPolicyLogRecord(
author=user,
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
notification_policy=notification_policy,
alert_group=alert_group,
reason="User is not in Slack",
notification_step=notification_policy.step,
notification_channel=notification_policy.notify_by,
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_IN_SLACK_USER_NOT_IN_SLACK,
).save()
else:
text = "{}\nInviting {} to look at the alert group.".format(alert_group.long_verbose_name, user_verbal)
text = text[:BLOCK_SECTION_TEXT_MAX_SIZE]
blocks = [
{
"type": "section",
"block_id": "alert",
"text": {
"type": "mrkdwn",
"text": text,
},
}
]
sc = SlackClient(self.slack_team_identity, enable_ratelimit_retry=True)
channel_id = slack_message.channel_id
try:
result = sc.chat_postMessage(
channel=channel_id,
text=text,
blocks=blocks,
thread_ts=slack_message.slack_id,
unfurl_links=True,
)
except SlackAPIRatelimitError:
UserNotificationPolicyLogRecord(
author=user,
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
notification_policy=notification_policy,
alert_group=alert_group,
reason="Slack API rate limit error",
notification_step=notification_policy.step,
notification_channel=notification_policy.notify_by,
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_IN_SLACK_RATELIMIT,
).save()
return
except SlackAPITokenError:
UserNotificationPolicyLogRecord(
author=user,
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
notification_policy=notification_policy,
alert_group=alert_group,
reason="Slack token error",
notification_step=notification_policy.step,
notification_channel=notification_policy.notify_by,
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_IN_SLACK_TOKEN_ERROR,
).save()
return
except SlackAPIChannelArchivedError:
UserNotificationPolicyLogRecord(
author=user,
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
notification_policy=notification_policy,
alert_group=alert_group,
reason="channel is archived",
notification_step=notification_policy.step,
notification_channel=notification_policy.notify_by,
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_IN_SLACK_CHANNEL_IS_ARCHIVED,
).save()
return
else:
alert_group.slack_messages.create(
slack_id=result["ts"],
organization=self.organization,
_slack_team_identity=self.slack_team_identity,
channel_id=channel_id,
)
# create success record
UserNotificationPolicyLogRecord.objects.create(
author=user,
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_SUCCESS,
notification_policy=notification_policy,
alert_group=alert_group,
notification_step=notification_policy.step,
notification_channel=notification_policy.notify_by,
)
# Check if escalated user is in channel. Otherwise send notification and request to invite him.
try:
if slack_user_identity:
channel_members = []
try:
channel_members = sc.conversations_members(channel=channel_id)["members"]
except SlackAPIFetchMembersFailedError:
pass
if slack_user_identity.slack_id not in channel_members:
time.sleep(5) # 2 messages in the same moment are ratelimited by Slack. Dirty hack.
slack_user_identity.send_link_to_slack_message(slack_message)
except (SlackAPITokenError, SlackAPIMethodNotSupportedForChannelTypeError):
pass