# What this PR does - As a follow-up to https://github.com/grafana/oncall/pull/5292, and now that `SlackMessage.channel` has been migrated via [`engine/apps/slack/migrations/0007_migrate_slackmessage_channel_id.py`](https://github.com/grafana/oncall/pull/5292/files#diff-8aebe133401715a4262baad9b2c5c9fc59367c18d6bd6ac2b3c462fcdabafd66), this PR removes reads/writes from `SlackMessage._channel_id` to `SlackMessage.channel`. In a separate PR I will focus on dropping that column from the model/db. - Drops `SlackMessage.active_update_task_id`. There're zero references to this column in the codebase. - Removes two Django `manage.py` commands that're no longer needed: - `engine/engine/management/commands/alertmanager_v2_migrate.py` (and it's associated tests) - `engine/engine/management/commands/batch_migrate_slack_message_channel.py` ## 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.
256 lines
10 KiB
Python
256 lines
10 KiB
Python
import logging
|
|
import typing
|
|
|
|
import requests
|
|
from django.db import models
|
|
|
|
from apps.slack.client import SlackClient
|
|
from apps.slack.constants import SLACK_BOT_ID
|
|
from apps.slack.errors import (
|
|
SlackAPICannotDMBotError,
|
|
SlackAPIInvalidAuthError,
|
|
SlackAPITokenError,
|
|
SlackAPIUserNotFoundError,
|
|
)
|
|
from apps.slack.scenarios.notified_user_not_in_channel import NotifiedUserNotInChannelStep
|
|
from apps.user_management.models import Organization, User
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from django.db.models.manager import RelatedManager
|
|
|
|
from apps.slack.models import SlackMessage
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AllSlackUserIdentityManager(models.Manager):
|
|
use_in_migrations = False
|
|
|
|
def get_queryset(self):
|
|
return super().get_queryset()
|
|
|
|
|
|
class SlackUserIdentityManager(models.Manager):
|
|
use_in_migrations = False
|
|
|
|
def get_queryset(self):
|
|
return super().get_queryset().filter(counter=1)
|
|
|
|
def get(self, **kwargs):
|
|
try:
|
|
instance = super().get(**kwargs, is_restricted=False, is_ultra_restricted=False)
|
|
except SlackUserIdentity.DoesNotExist:
|
|
instance = self.filter(**kwargs).first()
|
|
if instance is None:
|
|
raise SlackUserIdentity.DoesNotExist
|
|
return instance
|
|
|
|
|
|
class SlackUserIdentity(models.Model):
|
|
users: "RelatedManager['User']"
|
|
|
|
objects: models.Manager["SlackUserIdentity"] = SlackUserIdentityManager()
|
|
all_objects: models.Manager["SlackUserIdentity"] = AllSlackUserIdentityManager()
|
|
|
|
id = models.AutoField(primary_key=True)
|
|
|
|
slack_id = models.CharField(max_length=100)
|
|
|
|
slack_team_identity = models.ForeignKey(
|
|
"SlackTeamIdentity", on_delete=models.PROTECT, related_name="slack_user_identities"
|
|
)
|
|
|
|
cached_slack_email = models.EmailField(blank=True, default="")
|
|
|
|
cached_im_channel_id = models.CharField(max_length=100, null=True, default=None)
|
|
cached_phone_number = models.CharField(max_length=20, null=True, default=None)
|
|
cached_country_code = models.CharField(max_length=3, null=True, default=None)
|
|
cached_timezone = models.CharField(max_length=100, null=True, default=None)
|
|
cached_slack_login = models.CharField(max_length=100, null=True, default=None)
|
|
cached_avatar = models.URLField(max_length=200, null=True, default=None)
|
|
cached_name = models.CharField(max_length=200, null=True, default=None)
|
|
|
|
phone_from_onboarding = models.BooleanField(default=False)
|
|
|
|
cached_is_bot = models.BooleanField(null=True, default=None)
|
|
|
|
# Fields from user profile
|
|
profile_real_name_normalized = models.CharField(max_length=200, null=True, default=None)
|
|
profile_display_name = models.CharField(max_length=200, null=True, default=None)
|
|
profile_display_name_normalized = models.CharField(max_length=200, null=True, default=None)
|
|
profile_real_name = models.CharField(max_length=200, null=True, default=None)
|
|
|
|
deleted = models.BooleanField(null=True, default=None)
|
|
is_admin = models.BooleanField(null=True, default=None)
|
|
is_owner = models.BooleanField(null=True, default=None)
|
|
is_primary_owner = models.BooleanField(null=True, default=None)
|
|
is_restricted = models.BooleanField(null=True, default=None)
|
|
is_ultra_restricted = models.BooleanField(null=True, default=None)
|
|
is_app_user = models.BooleanField(null=True, default=None)
|
|
has_2fa = models.BooleanField(null=True, default=None)
|
|
|
|
main_menu_last_opened_datetime = models.DateTimeField(null=True, default=None)
|
|
counter = models.PositiveSmallIntegerField(default=1)
|
|
|
|
is_stranger = models.BooleanField(default=False)
|
|
is_not_found = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["slack_id", "slack_team_identity", "counter"], name="unique_slack_identity_per_team"
|
|
)
|
|
]
|
|
|
|
def __str__(self):
|
|
return self.slack_login
|
|
|
|
def send_link_to_slack_message(self, slack_message: "SlackMessage"):
|
|
blocks = [
|
|
{
|
|
"type": "section",
|
|
"text": {
|
|
"type": "plain_text",
|
|
"text": "You are invited to look at an alert group!",
|
|
"emoji": True,
|
|
},
|
|
},
|
|
{
|
|
"type": "actions",
|
|
"elements": [
|
|
{
|
|
"type": "button",
|
|
"action_id": f"{NotifiedUserNotInChannelStep.routing_uid()}",
|
|
"text": {"type": "plain_text", "text": "➡️ Go to the alert group"},
|
|
"url": slack_message.permalink,
|
|
"style": "primary",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"type": "context",
|
|
"elements": [
|
|
{
|
|
"type": "mrkdwn",
|
|
"text": (
|
|
f"You received this message because you're not a member of "
|
|
f"<#{slack_message.channel.slack_id}>.\n"
|
|
"Please join the channel to get notified right in the alert group thread."
|
|
),
|
|
}
|
|
],
|
|
},
|
|
]
|
|
|
|
sc = SlackClient(self.slack_team_identity)
|
|
return sc.chat_postMessage(
|
|
channel=self.im_channel_id,
|
|
text="You are invited to look at an alert group!",
|
|
blocks=blocks,
|
|
)
|
|
|
|
@property
|
|
def slack_verbal(self) -> str | None:
|
|
return (
|
|
self.profile_real_name_normalized
|
|
or self.profile_real_name
|
|
or self.profile_display_name_normalized
|
|
or self.profile_display_name
|
|
or self.cached_name
|
|
or self.cached_slack_login
|
|
)
|
|
|
|
@property
|
|
def slack_login(self):
|
|
if self.cached_slack_login is None or self.cached_slack_login == "slack_token_revoked_unable_to_cache_login":
|
|
sc = SlackClient(self.slack_team_identity)
|
|
try:
|
|
result = sc.users_info(user=self.slack_id, team=self.slack_team_identity)
|
|
self.cached_slack_login = result["user"]["name"]
|
|
self.save()
|
|
except SlackAPITokenError:
|
|
self.cached_slack_login = "slack_token_revoked_unable_to_cache_login"
|
|
self.save()
|
|
return "slack_token_revoked_unable_to_cache_login"
|
|
except SlackAPIUserNotFoundError:
|
|
self.cached_slack_login = "user_not_found"
|
|
self.save()
|
|
except SlackAPIInvalidAuthError:
|
|
return "no_enough_permissions_to_retrieve"
|
|
|
|
return str(self.cached_slack_login)
|
|
|
|
@property
|
|
def timezone(self):
|
|
if self.cached_timezone is None or self.cached_timezone == "None":
|
|
sc = SlackClient(self.slack_team_identity)
|
|
try:
|
|
result = sc.users_info(user=self.slack_id)
|
|
tz_from_slack = result["user"].get("tz", "UTC")
|
|
if tz_from_slack == "None" or tz_from_slack is None:
|
|
tz_from_slack = "UTC"
|
|
self.cached_timezone = tz_from_slack
|
|
self.save(update_fields=["cached_timezone"])
|
|
except SlackAPITokenError:
|
|
pass
|
|
except requests.exceptions.Timeout:
|
|
# Do not save tz in case of timeout to try to load it later again
|
|
return "UTC"
|
|
|
|
return str(self.cached_timezone)
|
|
|
|
@property
|
|
def im_channel_id(self):
|
|
if self.cached_im_channel_id is None:
|
|
sc = SlackClient(self.slack_team_identity)
|
|
try:
|
|
result = sc.conversations_open(users=self.slack_id, return_im=True)
|
|
self.cached_im_channel_id = result["channel"]["id"]
|
|
self.save()
|
|
except SlackAPICannotDMBotError:
|
|
pass
|
|
|
|
return self.cached_im_channel_id
|
|
|
|
def update_profile_info(self):
|
|
sc = SlackClient(self.slack_team_identity)
|
|
logger.info("Update user profile info")
|
|
try:
|
|
result = sc.users_info(user=self.slack_id, team=self.slack_team_identity)
|
|
except SlackAPITokenError as e:
|
|
logger.warning(f"Unable to get user info due token revoked or account inactive: {e}")
|
|
result = None
|
|
else:
|
|
if not self.cached_slack_email and "email" in result["user"]["profile"]:
|
|
self.cached_slack_email = result["user"]["profile"]["email"]
|
|
if "real_name" in result["user"]["profile"]:
|
|
self.profile_real_name = result["user"]["profile"]["real_name"]
|
|
if "real_name_normalized" in result["user"]["profile"]:
|
|
self.profile_real_name_normalized = result["user"]["profile"]["real_name_normalized"]
|
|
if "display_name" in result["user"]["profile"]:
|
|
self.profile_display_name = result["user"]["profile"]["display_name"]
|
|
if "display_name_normalized" in result["user"]["profile"]:
|
|
self.profile_display_name_normalized = result["user"]["profile"]["display_name_normalized"]
|
|
self.cached_avatar = result["user"]["profile"].get("image_512")
|
|
if result["user"].get("is_bot") is True or result["user"].get("id") == SLACK_BOT_ID:
|
|
self.cached_is_bot = True
|
|
self.cached_name = result["user"].get("real_name", result["user"]["name"])
|
|
self.cached_slack_login = result["user"].get("name")
|
|
self.save()
|
|
return result
|
|
|
|
def get_slack_username(self):
|
|
if not self.slack_verbal:
|
|
logger.info("Trying to get username from slack")
|
|
result = self.update_profile_info()
|
|
if result is None:
|
|
logger.info("Unable to populate username")
|
|
return None
|
|
return self.slack_verbal or self.cached_slack_email.split("@")[0] or None
|
|
|
|
def get_user(self, organization: Organization) -> User | None:
|
|
try:
|
|
user = organization.users.get(slack_user_identity=self)
|
|
except User.DoesNotExist:
|
|
user = None
|
|
return user
|