Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/sentry/api/endpoints/external_team_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,5 @@ def delete(self, request: Request, team: Team, external_team: ExternalActor) ->
self.assert_has_feature(request, team.organization)

external_team.delete()

return Response(status=status.HTTP_204_NO_CONTENT)
10 changes: 10 additions & 0 deletions src/sentry/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
from sentry.exceptions import InvalidIdentity
from sentry.models import (
AuditLogEntryEvent,
ExternalActor,
Identity,
Integration,
Organization,
OrganizationIntegration,
Team,
)
from sentry.pipeline import PipelineProvider
from sentry.shared_integrations.constants import (
Expand Down Expand Up @@ -387,3 +389,11 @@ def uninstall(self) -> None:
task.
"""
pass

# NotifyBasicMixin noops

def notify_remove_external_team(self, external_team: ExternalActor, team: Team) -> None:
pass

def remove_notification_settings(self, actor_id: int, provider: str) -> None:
pass
35 changes: 35 additions & 0 deletions src/sentry/integrations/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import logging

from sentry.models import ExternalActor, NotificationSetting, Team
from sentry.types.integrations import ExternalProviders

logger = logging.getLogger("sentry.integrations.notifications")

SUCCESS_UNLINKED_TEAM_TITLE = "Team unlinked"
SUCCESS_UNLINKED_TEAM_MESSAGE = (
"This channel will no longer receive issue alert notifications for the {team} team."
)


class NotifyBasicMixin:
def send_message(self, channel_id: str, message: str) -> None:
"""
Send a message through the integration.
"""
raise NotImplementedError

def notify_remove_external_team(self, external_team: ExternalActor, team: Team) -> None:
"""
Notify through the integration that an external team has been removed.
"""
self.send_message(
channel_id=external_team.external_id,
message=SUCCESS_UNLINKED_TEAM_MESSAGE.format(team=team.slug),
)

def remove_notification_settings(self, actor_id: int, provider: ExternalProviders) -> None:
"""
Delete notification settings based on an actor_id
There is no foreign key relationship so we have to manually cascade.
"""
NotificationSetting.objects._filter(target_ids=[actor_id], provider=provider).delete()
3 changes: 2 additions & 1 deletion src/sentry/integrations/slack/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from sentry.utils.json import JSONData

from .client import SlackClient
from .notifications import SlackNotifyBasicMixin
from .utils import logger

Channel = namedtuple("Channel", ["name", "id"])
Expand Down Expand Up @@ -65,7 +66,7 @@
)


class SlackIntegration(IntegrationInstallation): # type: ignore
class SlackIntegration(SlackNotifyBasicMixin, IntegrationInstallation): # type: ignore
def get_config_data(self) -> Mapping[str, str]:
metadata_ = self.model.metadata
# Classic bots had a user_access_token in the metadata.
Expand Down
20 changes: 20 additions & 0 deletions src/sentry/integrations/slack/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections import defaultdict
from typing import Any, Iterable, Mapping, MutableMapping, Optional, Union

from sentry.integrations.notifications import NotifyBasicMixin
from sentry.integrations.slack.client import SlackClient # NOQA
from sentry.integrations.slack.message_builder.notifications import build_notification_attachment
from sentry.models import ExternalActor, Identity, Integration, Organization, Team, User
Expand All @@ -15,6 +16,25 @@
SLACK_TIMEOUT = 5


class SlackNotifyBasicMixin(NotifyBasicMixin): # type: ignore
def send_message(self, channel_id: str, message: str) -> None:
client = SlackClient()
token = self.metadata.get("user_access_token") or self.metadata["access_token"]
headers = {"Authorization": f"Bearer {token}"}
payload = {
"token": token,
"channel": channel_id,
"text": message,
}
try:
client.post("/chat.postMessage", headers=headers, data=payload, json=True)
except ApiError as e:
message = str(e)
if message != "Expired url":
logger.error("slack.slash-notify.response-error", extra={"error": message})
return


def get_context(
notification: BaseNotification,
recipient: Union["Team", "User"],
Expand Down
2 changes: 0 additions & 2 deletions src/sentry/integrations/slack/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"get_channel_id_with_timeout",
"is_valid_role",
"logger",
"send_confirmation",
"send_incident_alert_notification",
"send_slack_response",
"strip_channel_name",
Expand All @@ -30,7 +29,6 @@
)
from .notifications import (
build_notification_footer,
send_confirmation,
send_incident_alert_notification,
send_slack_response,
)
Expand Down
40 changes: 0 additions & 40 deletions src/sentry/integrations/slack/utils/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
from typing import Mapping, Union
from urllib.parse import urljoin

from django.http import HttpResponse
from rest_framework.request import Request

from sentry.constants import ObjectStatus
from sentry.incidents.models import AlertRuleTriggerAction, Incident
from sentry.integrations.slack.client import SlackClient
Expand All @@ -15,7 +12,6 @@
from sentry.shared_integrations.exceptions import ApiError
from sentry.utils import json
from sentry.utils.http import absolute_uri
from sentry.web.helpers import render_to_response

from . import logger

Expand Down Expand Up @@ -52,42 +48,6 @@ def send_incident_alert_notification(
logger.info("rule.fail.slack_post", extra={"error": str(e)})


def send_confirmation(
integration: Integration,
channel_id: str,
heading: str,
text: str,
template: str,
request: Request,
) -> HttpResponse:
client = SlackClient()
token = integration.metadata.get("user_access_token") or integration.metadata["access_token"]
payload = {
"token": token,
"channel": channel_id,
"text": text,
}

headers = {"Authorization": f"Bearer {token}"}
try:
client.post("/chat.postMessage", headers=headers, data=payload, json=True)
except ApiError as e:
message = str(e)
if message != "Expired url":
logger.error("slack.slash-notify.response-error", extra={"error": message})
else:
return render_to_response(
template,
request=request,
context={
"heading_text": heading,
"body_text": text,
"channel_id": channel_id,
"team_id": integration.external_id,
},
)


def get_referrer_qstring(notification: BaseNotification, recipient: Union["Team", "User"]) -> str:
# TODO: make a generic version that works for other notification types
return (
Expand Down
35 changes: 22 additions & 13 deletions src/sentry/integrations/slack/views/link_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from sentry.web.frontend.base import BaseView
from sentry.web.helpers import render_to_response

from ..utils import is_valid_role, logger, send_confirmation
from ..utils import is_valid_role, logger
from . import build_linking_url as base_build_linking_url
from . import never_cache, render_error_page

Expand Down Expand Up @@ -124,6 +124,7 @@ def handle(self, request: Request, signed_params: str) -> HttpResponse:
if not Identity.objects.filter(idp=idp, external_id=params["slack_id"]).exists():
return render_error_page(request, body_text="HTTP 403: User identity does not exist")

install = integration.get_installation(team.organization.id)
external_team, created = ExternalActor.objects.get_or_create(
actor_id=team.actor_id,
organization=team.organization,
Expand All @@ -136,13 +137,17 @@ def handle(self, request: Request, signed_params: str) -> HttpResponse:
)

if not created:
return send_confirmation(
integration,
channel_id,
ALREADY_LINKED_TITLE,
ALREADY_LINKED_MESSAGE.format(slug=team.slug),
message = ALREADY_LINKED_MESSAGE.format(slug=team.slug)
install.send_message(channel_id=channel_id, message=message)
return render_to_response(
"sentry/integrations/slack/post-linked-team.html",
request,
request=request,
context={
"heading_text": ALREADY_LINKED_TITLE,
"body_text": message,
"channel_id": channel_id,
"team_id": integration.external_id,
},
)

# Turn on notifications for all of a team's projects.
Expand All @@ -152,11 +157,15 @@ def handle(self, request: Request, signed_params: str) -> HttpResponse:
NotificationSettingOptionValues.ALWAYS,
team=team,
)
return send_confirmation(
integration,
channel_id,
SUCCESS_LINKED_TITLE,
SUCCESS_LINKED_MESSAGE.format(slug=team.slug, channel_name=channel_name),
message = SUCCESS_LINKED_MESSAGE.format(slug=team.slug, channel_name=channel_name)
install.send_message(channel_id=channel_id, message=message)
return render_to_response(
"sentry/integrations/slack/post-linked-team.html",
request,
request=request,
context={
"heading_text": SUCCESS_LINKED_TITLE,
"body_text": message,
"channel_id": channel_id,
"team_id": integration.external_id,
},
)
24 changes: 12 additions & 12 deletions src/sentry/integrations/slack/views/unlink_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
from rest_framework.request import Request
from rest_framework.response import Response

from sentry.integrations.notifications import (
SUCCESS_UNLINKED_TEAM_MESSAGE,
SUCCESS_UNLINKED_TEAM_TITLE,
)
from sentry.integrations.utils import get_identity_or_404
from sentry.models import ExternalActor, Identity, Integration
from sentry.types.integrations import ExternalProviders
Expand All @@ -10,15 +14,9 @@
from sentry.web.frontend.base import BaseView
from sentry.web.helpers import render_to_response

from ..utils import send_confirmation
from . import build_linking_url as base_build_linking_url
from . import never_cache, render_error_page

SUCCESS_UNLINKED_TITLE = "Team unlinked"
SUCCESS_UNLINKED_MESSAGE = (
"This channel will no longer receive issue alert notifications for the {team} team."
)


def build_team_unlinking_url(
integration: Integration,
Expand Down Expand Up @@ -90,11 +88,13 @@ def handle(self, request: Request, signed_params: str) -> Response:
for external_team in external_teams:
external_team.delete()

return send_confirmation(
integration,
channel_id,
SUCCESS_UNLINKED_TITLE,
SUCCESS_UNLINKED_MESSAGE.format(team=team.slug),
return render_to_response(
"sentry/integrations/slack/unlinked-team.html",
request,
request=request,
context={
"heading_text": SUCCESS_UNLINKED_TEAM_TITLE,
"body_text": SUCCESS_UNLINKED_TEAM_MESSAGE.format(team=team.slug),
"channel_id": channel_id,
"team_id": integration.external_id,
},
)
10 changes: 5 additions & 5 deletions src/sentry/models/externalactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ class Meta:
unique_together = (("organization", "provider", "external_name", "actor"),)

def delete(self, **kwargs):
from sentry.models import NotificationSetting
install = self.integration.get_installation(self.organization_id)

# There is no foreign key relationship so we have to manually cascade.
NotificationSetting.objects._filter(
target_ids=[self.actor_id], provider=ExternalProviders(self.provider)
).delete()
install.notify_remove_external_team(external_team=self, team=self.actor.resolve())
install.remove_notification_settings(
actor_id=self.actor_id, provider=ExternalProviders(self.provider)
)

return super().delete(**kwargs)

Expand Down
Loading