-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
♻️ ref(slack): fix slack webhook actions typing #77322
Changes from all commits
4dd80be
97d6bd8
f6ca079
094a247
f2e49e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -208,22 +208,40 @@ def api_error( | |
channel_id = data.get("channel_id") | ||
response_url = data.get("orig_response_url") | ||
|
||
if error.status_code == 403: | ||
user_id = slack_request.user_id | ||
channel = channel_id or slack_request.channel_id | ||
resp_url = response_url or slack_request.response_url | ||
|
||
if user_id is None or channel is None or resp_url is None: | ||
text = DEFAULT_ERROR_MESSAGE | ||
# keeping this separate from above since its a different condition | ||
elif error.status_code != 403: | ||
text = DEFAULT_ERROR_MESSAGE | ||
else: | ||
text = UNLINK_IDENTITY_MESSAGE.format( | ||
associate_url=build_unlinking_url( | ||
slack_request.integration.id, | ||
slack_request.user_id, | ||
channel_id or slack_request.channel_id, | ||
response_url or slack_request.response_url, | ||
slack_id=user_id, | ||
channel_id=channel, | ||
response_url=resp_url, | ||
), | ||
user_email=user.email, | ||
org_name=group.organization.name, | ||
) | ||
else: | ||
text = DEFAULT_ERROR_MESSAGE | ||
|
||
return self.respond_ephemeral(text) | ||
|
||
@staticmethod | ||
def _unpack_error_text(validation_error: serializers.ValidationError) -> str: | ||
detail = validation_error.detail | ||
while True: | ||
if isinstance(detail, dict): | ||
detail = list(detail.values()) | ||
element = detail[0] | ||
if isinstance(element, str): | ||
return element | ||
detail = element | ||
|
||
def validation_error( | ||
self, | ||
slack_request: SlackActionRequest, | ||
|
@@ -240,11 +258,15 @@ def validation_error( | |
}, | ||
) | ||
|
||
text: str = list(*error.detail.values())[0] | ||
text: str = self._unpack_error_text(error) | ||
return self.respond_ephemeral(text) | ||
|
||
def on_assign( | ||
self, request: Request, user: RpcUser, group: Group, action: MessageAction | ||
self, | ||
request: Request, | ||
user: RpcUser, | ||
group: Group, | ||
action: MessageAction | BlockKitMessageAction, | ||
) -> None: | ||
if not (action.selected_options and len(action.selected_options)): | ||
# Short-circuit if action is invalid | ||
|
@@ -269,7 +291,7 @@ def on_status( | |
request: Request, | ||
user: RpcUser, | ||
group: Group, | ||
action: MessageAction, | ||
action: MessageAction | BlockKitMessageAction, | ||
) -> None: | ||
status_data = (action.value or "").split(":", 2) | ||
if not len(status_data): | ||
|
@@ -408,11 +430,11 @@ def _update_modal( | |
}, | ||
) | ||
# The modal was not found, so we need to open a new one | ||
self._open_view(slack_client, modal_payload, slack_request) | ||
self._open_modal(slack_client, modal_payload, slack_request) | ||
else: | ||
raise | ||
|
||
def _open_view( | ||
def _open_modal( | ||
self, slack_client: SlackSdkClient, modal_payload: View, slack_request: SlackActionRequest | ||
) -> None: | ||
# Error handling is done in the calling function | ||
|
@@ -451,10 +473,11 @@ def open_resolve_dialog(self, slack_request: SlackActionRequest, group: Group) - | |
# We passed this in control when we sent the loading modal to beat the 3 second timeout | ||
external_id = slack_request.get_action_ts() | ||
|
||
if options.get("send-slack-response-from-control-silo"): | ||
self._update_modal(slack_client, external_id, modal_payload, slack_request) | ||
if not external_id or not options.get("send-slack-response-from-control-silo"): | ||
# If we don't have an external_id or option is disabled we need to open a new modal | ||
self._open_modal(slack_client, modal_payload, slack_request) | ||
else: | ||
self._open_view(slack_client, modal_payload, slack_request) | ||
self._update_modal(slack_client, external_id, modal_payload, slack_request) | ||
|
||
metrics.incr( | ||
SLACK_WEBHOOK_GROUP_ACTIONS_SUCCESS_DATADOG_METRIC, | ||
|
@@ -503,10 +526,11 @@ def open_archive_dialog(self, slack_request: SlackActionRequest, group: Group) - | |
# We passed this in control when we sent the loading modal to beat the 3 second timeout | ||
external_id = slack_request.get_action_ts() | ||
|
||
if options.get("send-slack-response-from-control-silo"): | ||
self._update_modal(slack_client, external_id, modal_payload, slack_request) | ||
if not external_id or not options.get("send-slack-response-from-control-silo"): | ||
# If we don't have an external_id or option is disabled we need to open a new modal | ||
self._open_modal(slack_client, modal_payload, slack_request) | ||
else: | ||
self._open_view(slack_client, modal_payload, slack_request) | ||
self._update_modal(slack_client, external_id, modal_payload, slack_request) | ||
|
||
metrics.incr( | ||
SLACK_WEBHOOK_GROUP_ACTIONS_SUCCESS_DATADOG_METRIC, | ||
|
@@ -549,7 +573,7 @@ def _handle_group_actions( | |
self, | ||
slack_request: SlackActionRequest, | ||
request: Request, | ||
action_list: Sequence[MessageAction], | ||
action_list: Sequence[BlockKitMessageAction], | ||
) -> Response: | ||
from sentry.integrations.slack.views.link_identity import build_linking_url | ||
|
||
|
@@ -564,6 +588,10 @@ def _handle_group_actions( | |
identity_user = slack_request.get_identity_user() | ||
|
||
if not identity or not identity_user: | ||
# if we don't have user_id or channel_id, we can't link the identity | ||
if not slack_request.user_id or not slack_request.channel_id: | ||
return self.respond_ephemeral(NO_IDENTITY_MESSAGE) | ||
|
||
associate_url = build_linking_url( | ||
integration=slack_request.integration, | ||
slack_id=slack_request.user_id, | ||
|
@@ -594,10 +622,10 @@ def _handle_group_actions( | |
if not selection: | ||
return self.respond() | ||
|
||
action = MessageAction(name="status", value=selection) | ||
status_action = MessageAction(name="status", value=selection) | ||
|
||
try: | ||
self.on_status(request, identity_user, group, action) | ||
self.on_status(request, identity_user, group, status_action) | ||
except client.ApiError as error: | ||
return self.api_error(slack_request, group, identity_user, error, "status_dialog") | ||
|
||
|
@@ -608,19 +636,18 @@ def _handle_group_actions( | |
blocks = SlackIssuesMessageBuilder( | ||
group, | ||
identity=identity, | ||
actions=[action], | ||
actions=[status_action], | ||
tags=original_tags_from_request, | ||
rules=[rule] if rule else None, | ||
issue_details=True, | ||
skip_fallback=True, | ||
).build() | ||
|
||
# use the original response_url to update the link attachment | ||
json_blocks = orjson.dumps(blocks.get("blocks")).decode() | ||
try: | ||
webhook_client = WebhookClient(private_metadata["orig_response_url"]) | ||
webhook_client.send( | ||
blocks=json_blocks, delete_original=False, replace_original=True | ||
blocks=blocks.get("blocks"), delete_original=False, replace_original=True | ||
) | ||
metrics.incr( | ||
SLACK_WEBHOOK_GROUP_ACTIONS_SUCCESS_DATADOG_METRIC, | ||
|
@@ -700,18 +727,20 @@ def _handle_group_actions( | |
return self.respond() | ||
|
||
response_url = slack_request.data["response_url"] | ||
json_blocks = orjson.dumps(response.get("blocks")).decode() | ||
webhook_client = WebhookClient(response_url) | ||
try: | ||
webhook_client.send( | ||
blocks=json_blocks, | ||
blocks=response.get("blocks"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't need to do this, originally that orjson line only converted from single quotes to double quotes (i am assume this was because in pre-slack_sdk world, we needed to. the slack_sdk handles this for us, i verified this with my local setup |
||
text=response.get("text"), | ||
delete_original=False, | ||
replace_original=True, | ||
) | ||
_logger.info( | ||
"slack.webhook.update_status.success", | ||
extra={"integration_id": slack_request.integration.id, "blocks": json_blocks}, | ||
extra={ | ||
"integration_id": slack_request.integration.id, | ||
"blocks": response.get("blocks"), | ||
}, | ||
) | ||
metrics.incr( | ||
SLACK_WEBHOOK_GROUP_ACTIONS_SUCCESS_DATADOG_METRIC, | ||
|
@@ -758,44 +787,42 @@ def get_action_option(cls, slack_request: SlackActionRequest) -> str | None: | |
return action_option | ||
|
||
@classmethod | ||
def get_action_list(cls, slack_request: SlackActionRequest) -> list[MessageAction]: | ||
def get_action_list(cls, slack_request: SlackActionRequest) -> list[BlockKitMessageAction]: | ||
action_data = slack_request.data.get("actions") | ||
if action_data: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't need this since we have ga'ed blockkit for months now and won't be dealing with non-blockkit interactions. |
||
# XXX(CEO): this is here for backwards compatibility - if a user performs an action with an "older" | ||
# style issue alert but the block kit flag is enabled, we don't want to fall into this code path | ||
if action_data[0].get("action_id"): | ||
action_list = [] | ||
for action_data in action_data: | ||
if action_data.get("type") in ("static_select", "external_select"): | ||
action = BlockKitMessageAction( | ||
name=action_data["action_id"], | ||
label=action_data["selected_option"]["text"]["text"], | ||
type=action_data["type"], | ||
value=action_data["selected_option"]["value"], | ||
action_id=action_data["action_id"], | ||
block_id=action_data["block_id"], | ||
selected_options=[ | ||
{"value": action_data.get("selected_option", {}).get("value")} | ||
], | ||
) | ||
# TODO: selected_options is kinda ridiculous, I think this is built to handle multi-select? | ||
else: | ||
action = BlockKitMessageAction( | ||
name=action_data["action_id"], | ||
label=action_data["text"]["text"], | ||
type=action_data["type"], | ||
value=action_data["value"], | ||
action_id=action_data["action_id"], | ||
block_id=action_data["block_id"], | ||
) | ||
action_list.append(action) | ||
|
||
return action_list | ||
return [ | ||
MessageAction(**action_data) | ||
for action_data in action_data or [] | ||
if "name" in action_data | ||
] | ||
if ( | ||
not action_data | ||
or not isinstance(action_data, list) | ||
or not action_data[0].get("action_id") | ||
): | ||
return [] | ||
|
||
action_list = [] | ||
for action_data in action_data: | ||
if action_data.get("type") in ("static_select", "external_select"): | ||
action = BlockKitMessageAction( | ||
name=action_data["action_id"], | ||
label=action_data["selected_option"]["text"]["text"], | ||
type=action_data["type"], | ||
value=action_data["selected_option"]["value"], | ||
action_id=action_data["action_id"], | ||
block_id=action_data["block_id"], | ||
selected_options=[ | ||
{"value": action_data.get("selected_option", {}).get("value")} | ||
], | ||
) | ||
# TODO: selected_options is kinda ridiculous, I think this is built to handle multi-select? | ||
else: | ||
action = BlockKitMessageAction( | ||
name=action_data["action_id"], | ||
label=action_data["text"]["text"], | ||
type=action_data["type"], | ||
value=action_data["value"], | ||
action_id=action_data["action_id"], | ||
block_id=action_data["block_id"], | ||
) | ||
action_list.append(action) | ||
|
||
return action_list | ||
|
||
def post(self, request: Request) -> Response: | ||
try: | ||
|
@@ -903,6 +930,7 @@ def handle_member_approval(self, slack_request: SlackActionRequest, action: str) | |
except Exception: | ||
# shouldn't error but if it does, respond to the user | ||
_logger.exception( | ||
"slack.action.member-invitation-error", | ||
extra={ | ||
"organization_id": organization.id, | ||
"member_id": member.id, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the difference between
MessageAction
andBlockKitMessageAction
? should we always be using one over the other if we don't support non-block kit anymore?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ideally we should delete
MessageAction
, but i saw some places in our logic where we create aMessageAction
payload to send to slack, so i want to slowly remove it so make sure we don't break changes since ff this is hardThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, as I mentioned here it seems like
BlockKitMessageAction
ought to extendMessageAction
(assuming we need both).The "TODO" comment on
MessageAction
suggests that its bottom four attributes (includingblock_id
, which is confusing) ought to belong to some other subclass. But I agree with delaying that sort of refactoring for later.I'm okay with using
MessageAction | BlockKitMessageAction
for type hints temporarily, but would consider changingBlockKitMessageAction
to extendMessageAction
instead.