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
32 changes: 16 additions & 16 deletions src/sentry/api/helpers/group_index/update.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

from collections import defaultdict
from datetime import timedelta
from typing import Any, Mapping, MutableMapping, Optional, Sequence
from typing import Any, Mapping, MutableMapping, Sequence
from uuid import uuid4

from django.db import IntegrityError, transaction
Expand Down Expand Up @@ -60,9 +62,9 @@

def handle_discard(
request: Request,
group_list: Sequence["Group"],
projects: Sequence["Project"],
user: "User",
group_list: Sequence[Group],
projects: Sequence[Project],
user: User,
) -> Response:
for project in projects:
if not features.has("projects:discard-groups", project, actor=user):
Expand Down Expand Up @@ -98,9 +100,7 @@ def handle_discard(
return Response(status=204)


def self_subscribe_and_assign_issue(
acting_user: Optional["User"], group: "Group"
) -> Optional["ActorTuple"]:
def self_subscribe_and_assign_issue(acting_user: User | None, group: Group) -> ActorTuple | None:
# Used during issue resolution to assign to acting user
# returns None if the user didn't elect to self assign on resolution
# or the group is assigned already, otherwise returns Actor
Expand All @@ -118,8 +118,8 @@ def self_subscribe_and_assign_issue(


def get_current_release_version_of_group(
group: "Group", follows_semver: bool = False
) -> Optional[Release]:
group: Group, follows_semver: bool = False
) -> Release | None:
"""
Function that returns the latest release version associated with a Group, and by latest we
mean either most recent (date) or latest in semver versioning scheme
Expand Down Expand Up @@ -161,12 +161,12 @@ def get_current_release_version_of_group(

def update_groups(
request: Request,
group_ids: Sequence["Group"],
projects: Sequence["Project"],
group_ids: Sequence[Group],
projects: Sequence[Project],
organization_id: int,
search_fn: SearchFunction,
user: Optional["User"] = None,
data: Optional[Mapping[str, Any]] = None,
search_fn: SearchFunction | None,
user: User | None = None,
data: Mapping[str, Any] | None = None,
) -> Response:
# If `user` and `data` are passed as parameters then they should override
# the values in `request`.
Expand Down Expand Up @@ -202,7 +202,7 @@ def update_groups(

acting_user = user if user.is_authenticated else None

if not group_ids:
if search_fn and not group_ids:
try:
cursor_result, _ = search_fn(
{
Expand Down Expand Up @@ -250,7 +250,7 @@ def update_groups(
.order_by("-sort")[0]
)
activity_type = Activity.SET_RESOLVED_IN_RELEASE
activity_data: MutableMapping[str, Optional[Any]] = {
activity_data: MutableMapping[str, Any | None] = {
# no version yet
"version": ""
}
Expand Down
40 changes: 24 additions & 16 deletions src/sentry/integrations/slack/endpoints/action.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Any, Dict, Mapping
from __future__ import annotations

from typing import Any, Mapping, MutableMapping

from requests import post
from rest_framework.request import Request
Expand All @@ -9,17 +11,18 @@
from sentry.api.base import Endpoint
from sentry.api.helpers.group_index import update_groups
from sentry.integrations.slack.client import SlackClient
from sentry.integrations.slack.message_builder.issues import build_group_attachment
from sentry.integrations.slack.requests.action import SlackActionRequest
from sentry.integrations.slack.requests.base import SlackRequestError
from sentry.integrations.slack.views.link_identity import build_linking_url
from sentry.integrations.slack.views.unlink_identity import build_unlinking_url
from sentry.models import Group, Identity, IdentityProvider, Integration, Project
from sentry.notifications.utils.actions import MessageAction
from sentry.shared_integrations.exceptions import ApiError
from sentry.utils import json
from sentry.web.decorators import transaction_start

from ..message_builder import SlackBody
from ..message_builder.issues import SlackIssuesMessageBuilder
from ..utils import logger

LINK_IDENTITY_MESSAGE = (
Expand Down Expand Up @@ -110,10 +113,9 @@ def api_error(
return self.respond_ephemeral(text)

def on_assign(
self, request: Request, identity: Identity, group: Group, action: Mapping[str, Any]
self, request: Request, identity: Identity, group: Group, action: MessageAction
) -> None:
assignee = action["selected_options"][0]["value"]

assignee = action.selected_options[0]["value"]
if assignee == "none":
assignee = None

Expand All @@ -125,13 +127,11 @@ def on_status(
request: Request,
identity: Identity,
group: Group,
action: Mapping[str, Any],
action: MessageAction,
data: Mapping[str, Any],
integration: Integration,
) -> None:
status = action["value"]

status_data = status.split(":", 1)
status_data = (action.value or "").split(":", 1)
status = {"status": status_data[0]}

resolve_type = status_data[-1]
Expand Down Expand Up @@ -208,7 +208,7 @@ def is_message(self, data: Mapping[str, Any]) -> bool:

@transaction_start("SlackActionEndpoint")
def post(self, request: Request) -> Response:
logging_data: Dict[str, str] = {}
logging_data: MutableMapping[str, str] = {}

try:
slack_request = SlackActionRequest(request)
Expand All @@ -219,8 +219,9 @@ def post(self, request: Request) -> Response:
data = slack_request.data

# Actions list may be empty when receiving a dialog response
action_list = data.get("actions", [])
action_option = action_list and action_list[0].get("value", "")
action_list_raw = data.get("actions", [])
action_list = [MessageAction(**action_data) for action_data in action_list_raw]
action_option = (action_list[0].value if len(action_list) else None) or ""

# if a user is just clicking our auto response in the messages tab we just return a 200
if action_option == "sentry_docs_link_clicked":
Expand Down Expand Up @@ -281,15 +282,20 @@ def post(self, request: Request) -> Response:
# Handle status dialog submission
if slack_request.type == "dialog_submission" and "resolve_type" in data["submission"]:
# Masquerade a status action
action = {"name": "status", "value": data["submission"]["resolve_type"]}
action = MessageAction(
name="status",
value=data["submission"]["resolve_type"],
)

try:
self.on_status(request, identity, group, action, data, integration)
except client.ApiError as error:
return self.api_error(slack_request, group, identity, error, "status_dialog")

group = Group.objects.get(id=group.id)
attachment = build_group_attachment(group, identity=identity, actions=[action])
attachment = SlackIssuesMessageBuilder(
group, identity=identity, actions=[action]
).build()

body = self.construct_reply(
attachment, is_message=slack_request.callback_data["is_message"]
Expand All @@ -316,7 +322,7 @@ def post(self, request: Request) -> Response:
action_type = None
try:
for action in action_list:
action_type = action["name"]
action_type = action.name

if action_type == "status":
self.on_status(request, identity, group, action, data, integration)
Expand All @@ -334,7 +340,9 @@ def post(self, request: Request) -> Response:
# Reload group as it may have been mutated by the action
group = Group.objects.get(id=group.id)

attachment = build_group_attachment(group, identity=identity, actions=action_list)
attachment = SlackIssuesMessageBuilder(
group, identity=identity, actions=action_list
).build()
body = self.construct_reply(attachment, is_message=self.is_message(data))

return self.respond(body)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# TODO(mgaeta): Continue fleshing out these types.
SlackAttachment = Mapping[str, Any]
SlackBlock = Mapping[str, Any]
SlackBody = Union[SlackAttachment, Mapping[str, Sequence[SlackBlock]]]
SlackBody = Union[SlackAttachment, Sequence[SlackAttachment], Mapping[str, Sequence[SlackBlock]]]

# Attachment colors used for issues with no actions take.
LEVEL_TO_COLOR = {
Expand Down
42 changes: 36 additions & 6 deletions src/sentry/integrations/slack/message_builder/base/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
from __future__ import annotations

from abc import ABC
from typing import Any, Optional
from typing import Any, Mapping, MutableMapping, Sequence

from sentry.integrations.slack.message_builder import LEVEL_TO_COLOR, SlackAttachment, SlackBody
from sentry.integrations.slack.message_builder import LEVEL_TO_COLOR, SlackBody
from sentry.integrations.slack.message_builder.base import AbstractMessageBuilder
from sentry.notifications.utils.actions import MessageAction
from sentry.utils.assets import get_asset_url
from sentry.utils.http import absolute_uri


def get_slack_button(action: MessageAction) -> Mapping[str, Any]:
kwargs: MutableMapping[str, Any] = {
"text": action.label or action.name,
"name": action.name,
"type": action.type,
}
for field in ("style", "url", "value"):
value = getattr(action, field, None)
if value:
kwargs[field] = value

if action.type == "select":
kwargs["selected_options"] = action.selected_options or []
kwargs["option_groups"] = action.option_groups or []

return kwargs


class SlackMessageBuilder(AbstractMessageBuilder, ABC):
def build(self) -> SlackBody:
"""Abstract `build` method that all inheritors must implement."""
Expand All @@ -15,18 +36,22 @@ def build(self) -> SlackBody:
@staticmethod
def _build(
text: str,
title: Optional[str] = None,
footer: Optional[str] = None,
color: Optional[str] = None,
title: str | None = None,
title_link: str | None = None,
footer: str | None = None,
color: str | None = None,
actions: Sequence[MessageAction] | None = None,
**kwargs: Any,
) -> SlackAttachment:
) -> SlackBody:
"""
Helper to DRY up Slack specific fields.

:param string text: Body text.
:param [string] title: Title text.
:param [string] title_link: Optional URL attached to the title.
:param [string] footer: Footer text.
:param [string] color: The key in the Slack palate table, NOT hex. Default: "info".
:param [list[MessageAction]] actions: List of actions displayed alongside the message.
:param kwargs: Everything else.
"""
# If `footer` string is passed, automatically attach a `footer_icon`.
Expand All @@ -38,6 +63,11 @@ def _build(

if title:
kwargs["title"] = title
if title_link:
kwargs["title_link"] = title_link

if actions is not None:
kwargs["actions"] = [get_slack_button(action) for action in actions]

return {
"text": text,
Expand Down
Loading