oncall-engine/engine/apps/slack/models/slack_user_identity.py
Joey Orlando 3977c6e9ef
chore: remove references to SlackMessage._channel_id (#5325)
# 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.
2024-12-06 09:28:26 -05:00

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