# What this PR does `OnCallSchedule` equivalent of https://github.com/grafana/oncall/pull/5191. **NOTE**: merge after https://github.com/grafana/oncall/pull/5224 (so that I can use some of the new serializer fields defined in there) ### Migration ```bash Running migrations: │ │ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Starting migration to populate slack_channel field. │ │ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Total schedules to process: 1 │ │ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Schedule 26 updated with SlackChannel 2 (slack_id: C043LL6RTS7). │ │ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Bulk updated 1 OnCallSchedules with their Slack channel. │ │ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Finished migration. Total schedules processed: 1. Schedules updated: 1. Missing SlackChannels: 0. │ │ Applying schedules.0019_auto_20241021_1735... OK ``` ### Tested Public API ```txt POST {{oncall_host}}/api/v1/schedules/ Authorization: {{oncall_api_key}} Content-Type: application/json { "name": "Demo testy testy2", "type": "web", "time_zone": "America/Los_Angeles", "slack": { "channel_id": "C05PPLYN1U1" } } HTTP/1.1 201 Created Content-Type: application/json Vary: Accept, Origin Allow: GET, POST, HEAD, OPTIONS X-Frame-Options: DENY Content-Length: 198 X-Content-Type-Options: nosniff Referrer-Policy: same-origin Cross-Origin-Opener-Policy: same-origin { "id": "SBBN73UTUTVCE", "team_id": null, "name": "Demo testy testy2", "time_zone": "America/Los_Angeles", "on_call_now": [], "shifts": [], "slack": { "channel_id": "C05PPLYN1U1", "user_group_id": null }, "type": "web" } ``` ### Tested via UI (eg; internal API) https://www.loom.com/share/e66bf3468b144dd782da5eb6e0bfd0af ## 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.
160 lines
6.2 KiB
Python
160 lines
6.2 KiB
Python
import logging
|
|
import typing
|
|
|
|
from django.conf import settings
|
|
from django.db import models
|
|
from django.db.models import JSONField
|
|
|
|
from apps.api.permissions import RBACPermission
|
|
from apps.slack.client import SlackClient
|
|
from apps.slack.constants import SLACK_INVALID_AUTH_RESPONSE, SLACK_WRONG_TEAM_NAMES
|
|
from apps.slack.errors import (
|
|
SlackAPIChannelNotFoundError,
|
|
SlackAPIFetchMembersFailedError,
|
|
SlackAPIInvalidAuthError,
|
|
SlackAPITokenError,
|
|
)
|
|
from apps.user_management.models import Organization, User
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from django.db.models.manager import RelatedManager
|
|
|
|
from apps.slack.models import SlackChannel, SlackUserGroup
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SlackTeamIdentity(models.Model):
|
|
cached_channels: "RelatedManager['SlackChannel']"
|
|
organizations: "RelatedManager[Organization]"
|
|
usergroups: "RelatedManager['SlackUserGroup']"
|
|
|
|
id = models.AutoField(primary_key=True)
|
|
slack_id = models.CharField(max_length=100)
|
|
cached_name = models.CharField(max_length=100, null=True, default=None)
|
|
cached_app_id = models.CharField(max_length=100, null=True, default=None)
|
|
access_token = models.CharField(max_length=255, null=True, default=None)
|
|
bot_user_id = models.CharField(max_length=100, null=True, default=None)
|
|
bot_access_token = models.CharField(max_length=255, null=True, default=None)
|
|
oauth_scope = models.TextField(max_length=30000, null=True, default=None)
|
|
detected_token_revoked = models.DateTimeField(null=True, default=None, verbose_name="Deleted At")
|
|
is_profile_populated = models.BooleanField(default=False)
|
|
datetime = models.DateTimeField(auto_now_add=True)
|
|
installed_via_granular_permissions = models.BooleanField(default=True)
|
|
|
|
installed_by = models.ForeignKey("SlackUserIdentity", on_delete=models.PROTECT, null=True, default=None)
|
|
|
|
last_populated = models.DateTimeField(null=True, default=None)
|
|
|
|
cached_bot_id = models.CharField(max_length=100, null=True, default=None)
|
|
|
|
# response after oauth.access. This field is used to reinstall app to another OnCall workspace
|
|
cached_reinstall_data = JSONField(null=True, default=None)
|
|
|
|
# Do not use directly, use the "needs_reinstall" property instead
|
|
_unified_slack_app_installed = models.BooleanField(null=True, default=False)
|
|
|
|
class Meta:
|
|
ordering = ("datetime",)
|
|
|
|
def __str__(self):
|
|
return f"{self.pk}: {self.name}"
|
|
|
|
def update_oauth_fields(self, user, organization, oauth_response):
|
|
logger.info(f"updated oauth_fields for sti {self.pk}")
|
|
from apps.slack.models import SlackUserIdentity
|
|
|
|
organization.slack_team_identity = self
|
|
organization.save(update_fields=["slack_team_identity"])
|
|
slack_user_identity, _ = SlackUserIdentity.objects.get_or_create(
|
|
slack_id=oauth_response["authed_user"]["id"],
|
|
slack_team_identity=self,
|
|
)
|
|
user.slack_user_identity = slack_user_identity
|
|
user.save(update_fields=["slack_user_identity"])
|
|
self.bot_access_token = oauth_response["access_token"]
|
|
self.bot_user_id = oauth_response["bot_user_id"]
|
|
self.oauth_scope = oauth_response["scope"]
|
|
self.cached_name = oauth_response["team"]["name"]
|
|
self.access_token = oauth_response["authed_user"]["access_token"]
|
|
self.installed_by = slack_user_identity
|
|
self.cached_reinstall_data = None
|
|
self.installed_via_granular_permissions = True
|
|
|
|
if settings.UNIFIED_SLACK_APP_ENABLED:
|
|
self._unified_slack_app_installed = True
|
|
|
|
self.save()
|
|
|
|
def get_cached_channels(self, search_term=None, slack_id=None):
|
|
queryset = self.cached_channels
|
|
if search_term is not None:
|
|
queryset = queryset.filter(name__startswith=search_term)
|
|
if slack_id is not None:
|
|
queryset = queryset.filter(slack_id=slack_id)
|
|
return queryset.all()
|
|
|
|
@property
|
|
def bot_id(self):
|
|
if self.cached_bot_id is None:
|
|
sc = SlackClient(self)
|
|
auth = sc.auth_test()
|
|
self.cached_bot_id = auth.get("bot_id")
|
|
self.save(update_fields=["cached_bot_id"])
|
|
return self.cached_bot_id
|
|
|
|
@property
|
|
def members(self):
|
|
sc = SlackClient(self)
|
|
|
|
next_cursor = None
|
|
members = []
|
|
while next_cursor != "" or next_cursor is None:
|
|
result = sc.users_list(cursor=next_cursor, team=self)
|
|
next_cursor = result["response_metadata"]["next_cursor"]
|
|
members += result["members"]
|
|
|
|
return members
|
|
|
|
@property
|
|
def name(self):
|
|
if self.cached_name is None or self.cached_name in SLACK_WRONG_TEAM_NAMES:
|
|
try:
|
|
sc = SlackClient(self)
|
|
result = sc.team_info()
|
|
self.cached_name = result["team"]["name"]
|
|
self.save()
|
|
except SlackAPIInvalidAuthError:
|
|
self.cached_name = SLACK_INVALID_AUTH_RESPONSE
|
|
self.save()
|
|
return self.cached_name
|
|
|
|
@property
|
|
def app_id(self):
|
|
if not self.cached_app_id:
|
|
sc = SlackClient(self)
|
|
result = sc.bots_info(bot=self.bot_id)
|
|
app_id = result["bot"]["app_id"]
|
|
self.cached_app_id = app_id
|
|
self.save(update_fields=["cached_app_id"])
|
|
return self.cached_app_id
|
|
|
|
@property
|
|
def needs_reinstall(self):
|
|
return settings.UNIFIED_SLACK_APP_ENABLED and not self._unified_slack_app_installed
|
|
|
|
def get_users_from_slack_conversation_for_organization(self, channel_id: str, organization: Organization):
|
|
sc = SlackClient(self)
|
|
return User.objects.filter_by_permission(
|
|
RBACPermission.Permissions.CHATOPS_WRITE,
|
|
organization,
|
|
slack_user_identity__slack_id__in=self.get_conversation_members(sc, channel_id),
|
|
)
|
|
|
|
def get_conversation_members(self, slack_client: SlackClient, channel_id: str):
|
|
try:
|
|
return slack_client.paginated_api_call(
|
|
"conversations_members", paginated_key="members", channel=channel_id
|
|
)["members"]
|
|
except (SlackAPITokenError, SlackAPIFetchMembersFailedError, SlackAPIChannelNotFoundError):
|
|
return []
|