def on_message(self, request: Request, slack_request: SlackDMRequest) -> Response: command = request.data.get("event", {}).get("text", "").lower() if slack_request.is_bot() or not command: return self.respond() headers = { "Authorization": f"Bearer {self._get_access_token(slack_request.integration)}" } payload = { "channel": slack_request.channel_name, **SlackHelpMessageBuilder(command).as_payload(), } client = SlackClient() try: client.post("/chat.postMessage", headers=headers, data=payload, json=True) except ApiError as e: logger.error("slack.event.on-message-error", extra={"error": str(e)}) return self.respond()
def open_resolve_dialog(self, slack_request: SlackActionRequest, group: Group) -> None: # XXX(epurkhiser): In order to update the original message we have to # keep track of the response_url in the callback_id. Definitely hacky, # but seems like there's no other solutions [1]: # # [1]: https://stackoverflow.com/questions/46629852/update-a-bot-message-after-responding-to-a-slack-dialog#comment80795670_46629852 callback_id = json.dumps({ "issue": group.id, "orig_response_url": slack_request.data["response_url"], "is_message": _is_message(slack_request.data), }) dialog = { "callback_id": callback_id, "title": "Resolve Issue", "submit_label": "Resolve", "elements": [RESOLVE_SELECTOR], } payload = { "dialog": json.dumps(dialog), "trigger_id": slack_request.data["trigger_id"], "token": slack_request.integration.metadata["access_token"], } slack_client = SlackClient() try: slack_client.post("/dialog.open", data=payload) except ApiError as e: logger.error("slack.action.response-error", 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 prompt_link(self, slack_request: SlackDMRequest) -> None: associate_url = build_linking_url( integration=slack_request.integration, slack_id=slack_request.user_id, channel_id=slack_request.channel_id, response_url=slack_request.response_url, ) if not slack_request.channel_name: return payload = { "token": self._get_access_token(slack_request.integration), "channel": slack_request.channel_name, "user": slack_request.user_id, "text": "Link your Slack identity to Sentry to unfurl Discover charts.", **SlackPromptLinkMessageBuilder(associate_url).as_payload(), } client = SlackClient() try: client.post("/chat.postEphemeral", data=payload) except ApiError as e: logger.error("slack.event.unfurl-error", extra={"error": str(e)}, exc_info=True)
def send_incident_alert_notification( action: AlertRuleTriggerAction, incident: Incident, metric_value: int, method: str, ) -> None: # Make sure organization integration is still active: try: integration = Integration.objects.get( id=action.integration_id, organizations=incident.organization, status=ObjectStatus.VISIBLE, ) except Integration.DoesNotExist: # Integration removed, but rule is still active. return channel = action.target_identifier attachment = SlackIncidentsMessageBuilder(incident, action, metric_value, method).build() payload = { "token": integration.metadata["access_token"], "channel": channel, "attachments": json.dumps([attachment]), } client = SlackClient() try: client.post("/chat.postMessage", data=payload, timeout=5) except ApiError as e: logger.info("rule.fail.slack_post", extra={"error": str(e)})
def send_issue_notification_as_slack( notification: Any, user_ids: int, context: Mapping[str, Any], ) -> None: """ Send an "issue notification" to a Slack user which are project level issue alerts """ users = User.objects.filter(id__in=list(user_ids)) external_actors_by_user = get_integrations_by_user_id(context["project"].organization, users) client = SlackClient() for user in users: try: channel, token = get_channel_and_token(external_actors_by_user, user) except AttributeError as e: logger.info( "notification.fail.invalid_slack", extra={ "error": str(e), "notification": "issue_alert", "user": user.id, }, ) continue attachment = [ build_issue_notification_attachment( context["group"], event=context["event"], tags=context["tags"], rules=context["rules"], ) ] payload = { "token": token, "channel": channel, "link_names": 1, "attachments": json.dumps(attachment), } try: client.post("/chat.postMessage", data=payload, timeout=5) except ApiError as e: logger.info( "notification.fail.slack_post", extra={ "error": str(e), "notification": "issue_alert", "user": user.id, "channel_id": channel, }, ) continue metrics.incr( "issue_alert.notifications.sent", instance="slack.issue_alert.notification", skip_internal=False, )
def post_message(payload: Mapping[str, Any], log_error_message: str, log_params: Mapping[str, Any]) -> None: client = SlackClient() try: client.post("/chat.postMessage", data=payload, timeout=5) except ApiError as e: extra = {"error": str(e), **log_params} logger.info(log_error_message, extra=extra)
def send_notification_as_slack( notification: BaseNotification, users: Set[User], shared_context: Mapping[str, Any], extra_context_by_user_id: Mapping[str, Any], ) -> None: """ Send an "activity notification" to a Slack user which are workflow and deploy notification types """ external_actors_by_user = get_integrations_by_user_id( notification.organization, users) client = SlackClient() for user in users: extra_context = (extra_context_by_user_id or {}).get(user.id, {}) try: channel, token = get_channel_and_token(external_actors_by_user, user) except AttributeError as e: logger.info( "notification.fail.invalid_slack", extra={ "error": str(e), "notification": notification, "user": user.id, }, ) continue context = get_context(notification, user, shared_context, extra_context) attachment = [build_notification_attachment(notification, context)] payload = { "token": token, "channel": channel, "link_names": 1, "attachments": json.dumps(attachment), } try: client.post("/chat.postMessage", data=payload, timeout=5) except ApiError as e: logger.info( "notification.fail.slack_post", extra={ "error": str(e), "notification": notification, "user": user.id, "channel_id": channel, }, ) continue metrics.incr( "activity.notifications.sent", instance="slack.activity.notification", skip_internal=False, )
def reply(self, slack_request: SlackDMRequest, message: str) -> Response: headers = {"Authorization": f"Bearer {self._get_access_token(slack_request.integration)}"} payload = {"channel": slack_request.channel_name, "text": message} client = SlackClient() try: client.post("/chat.postMessage", headers=headers, data=payload, json=True) except ApiError as e: logger.error("slack.event.on-message-error", extra={"error": str(e)}) return self.respond()
def send_notification_as_slack( notification: BaseNotification, recipients: Union[Set[User], Set[Team]], shared_context: Mapping[str, Any], extra_context_by_user_id: Mapping[str, Any], ) -> None: """ Send an "activity" or "alert rule" notification to a Slack user or team. """ external_actors_by_recipient = get_integrations_by_recipient_id( notification.organization, recipients) client = SlackClient() for recipient in recipients: extra_context = (extra_context_by_user_id or {}).get(recipient.id, {}) try: channel, token = get_channel_and_token( external_actors_by_recipient, recipient) except AttributeError as e: logger.info( "notification.fail.invalid_slack", extra={ "error": str(e), "notification": notification, "recipient": recipient.id, }, ) continue context = get_context(notification, recipient, shared_context, extra_context) attachment = [build_notification_attachment(notification, context)] payload = { "token": token, "channel": channel, "link_names": 1, "attachments": json.dumps(attachment), } try: client.post("/chat.postMessage", data=payload, timeout=5) except ApiError as e: logger.info( "notification.fail.slack_post", extra={ "error": str(e), "notification": notification, "recipient": recipient.id, "channel_id": channel, }, ) continue key = get_key(notification) metrics.incr( f"{key}.notifications.sent", instance=f"slack.{key}.notification", skip_internal=False, )
def send_notification_as_slack( notification: ActivityNotification, users: Mapping[User, int], shared_context: Mapping[str, Any], ) -> None: external_actors_by_user = get_integrations_by_user_id( notification.organization, users.keys()) client = SlackClient() for user, reason in users.items(): try: channel, token = get_channel_and_token(external_actors_by_user, user) except AttributeError as e: logger.info( "notification.fail.invalid_slack", extra={ "error": str(e), "notification": notification, "user": user.id, }, ) continue context = get_context(notification, user, reason, shared_context) attachment = [build_notification_attachment(notification, context)] payload = { "token": token, "channel": channel, "link_names": 1, "attachments": json.dumps(attachment), } try: client.post("/chat.postMessage", data=payload, timeout=5) except ApiError as e: logger.info( "notification.fail.slack_post", extra={ "error": str(e), "notification": notification, "user": user.id, "channel_id": channel, }, ) continue metrics.incr( "activity.notifications.sent", instance="slack.activity.notification", skip_internal=False, )
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 prompt_link( self, data: Mapping[str, Any], slack_request: SlackRequest, integration: Integration, ): # This will break if multiple Sentry orgs are added # to a single Slack workspace and a user is a part of one # org and not the other. Since we pick the first org # in the integration organizations set, we might be picking # the org the user is not a part of. organization = integration.organizations.all()[0] associate_url = build_linking_url( integration=integration, organization=organization, slack_id=slack_request.user_id, channel_id=slack_request.channel_id, response_url=slack_request.response_url, ) builder = BlockSlackMessageBuilder() blocks = [ builder.get_markdown_block( "Link your Slack identity to Sentry to unfurl Discover charts." ), builder.get_action_block([("Link", associate_url, "link"), ("Cancel", None, "ignore")]), ] payload = { "token": self._get_access_token(integration), "channel": data["channel"], "user": data["user"], "text": "Link your Slack identity to Sentry to unfurl Discover charts.", "blocks": json.dumps(blocks), } client = SlackClient() try: client.post("/chat.postEphemeral", data=payload) except ApiError as e: logger.error("slack.event.unfurl-error", extra={"error": str(e)}, exc_info=True)
def reply(self, slack_request: SlackRequest, message: str) -> Response: client = SlackClient() access_token = self._get_access_token(slack_request.integration) headers = {"Authorization": f"Bearer {access_token}"} data = slack_request.data.get("event") channel = data["channel"] payload = {"channel": channel, "text": message} try: client.post("/chat.postMessage", headers=headers, data=payload, json=True) except ApiError as e: logger.error("slack.event.on-message-error", extra={"error": str(e)}) return self.respond()
def send_slack_response( integration: Integration, text: str, params: Mapping[str, str], command: str ) -> None: payload = { "replace_original": False, "response_type": "ephemeral", "text": text, } client = SlackClient() if params["response_url"]: path = params["response_url"] headers = {} else: # Command has been invoked in a DM, not as a slash command # we do not have a response URL in this case token = ( integration.metadata.get("user_access_token") or integration.metadata["access_token"] ) headers = {"Authorization": f"Bearer {token}"} payload["token"] = token payload["channel"] = params["slack_id"] path = "/chat.postMessage" try: client.post(path, headers=headers, data=payload, json=True) except ApiError as e: message = str(e) # If the user took their time to link their slack account, we may no # longer be able to respond, and we're not guaranteed able to post into # the channel. Ignore Expired url errors. # # XXX(epurkhiser): Yes the error string has a space in it. if message != "Expired url": log_message = ( "slack.link-notify.response-error" if command == "link" else "slack.unlink-notify.response-error" ) logger.error(log_message, extra={"error": message})
def on_message(self, request: Request, integration: Integration, token: str, data: Mapping[str, Any]) -> Response: channel = data["channel"] if self.is_bot(data): return self.respond() access_token = self._get_access_token(integration) headers = {"Authorization": "Bearer %s" % access_token} payload = { "channel": channel, **SlackEventMessageBuilder(integration).build() } client = SlackClient() try: client.post("/chat.postMessage", headers=headers, data=payload, json=True) except ApiError as e: logger.error("slack.event.on-message-error", extra={"error": str(e)}) return self.respond()
def send_notification_as_slack(notification: ActivityNotification, user: User, context: Mapping[str, Any]) -> None: external_actors = ExternalActor.objects.filter( provider=ExternalProviders.SLACK.value, actor=user.actor, organization=notification.organization, ).select_related("integration") client = SlackClient() for external_actor in external_actors: attachment = [build_notification_attachment(notification, context)] integration = external_actor.integration if integration: token = integration.metadata["access_token"] payload = { "token": token, "channel": external_actor.external_id, "link_names": 1, "attachments": json.dumps(attachment), } try: client.post("/chat.postMessage", data=payload, timeout=5) except ApiError as e: logger.info( "notification.fail.slack_post", extra={ "error": str(e), "notification": notification, "user": user.id, "channel_id": external_actor.external_id, }, ) metrics.incr( "activity.notifications.sent", instance="slack.activity.notification", skip_internal=False, )
def on_message(self, request: Request, integration: Integration, token: str, data: Mapping[str, Any]) -> Response: channel = data["channel"] # if it's a message posted by our bot, we don't want to respond since # that will cause an infinite loop of messages if data.get("bot_id"): return self.respond() access_token = self._get_access_token(integration) headers = {"Authorization": "Bearer %s" % access_token} payload = { "channel": channel, **SlackEventMessageBuilder(integration).build() } client = SlackClient() try: client.post("/chat.postMessage", headers=headers, data=payload, json=True) except ApiError as e: logger.error("slack.event.on-message-error", extra={"error": str(e)}) return self.respond()
def send_notification_as_slack( notification: BaseNotification, recipients: Union[Set[User], Set[Team]], shared_context: Mapping[str, Any], extra_context_by_user_id: Mapping[str, Any], ) -> None: """Send an "activity" or "alert rule" notification to a Slack user or team.""" client = SlackClient() data = get_channel_and_token_by_recipient(notification.organization, recipients) for recipient, tokens_by_channel in data.items(): is_multiple = True if len([token for token in tokens_by_channel ]) > 1 else False if is_multiple: logger.info( "notification.multiple.slack_post", extra={ "notification": notification, "recipient": recipient.id, }, ) extra_context = (extra_context_by_user_id or {}).get(recipient.id, {}) context = get_context(notification, recipient, shared_context, extra_context) attachment = [ build_notification_attachment(notification, context, recipient) ] for channel, token in tokens_by_channel.items(): # unfurl_links and unfurl_media are needed to preserve the intended message format # and prevent the app from replying with help text to the unfurl payload = { "token": token, "channel": channel, "link_names": 1, "unfurl_links": False, "unfurl_media": False, "text": notification.get_notification_title(), "attachments": json.dumps(attachment), } try: client.post("/chat.postMessage", data=payload, timeout=5) except ApiError as e: logger.info( "notification.fail.slack_post", extra={ "error": str(e), "notification": notification, "recipient": recipient.id, "channel_id": channel, "is_multiple": is_multiple, }, ) continue key = get_key(notification) metrics.incr( f"{key}.notifications.sent", instance=f"slack.{key}.notification", skip_internal=False, )
def on_link_shared( self, request: Request, slack_request: SlackRequest, ) -> Optional[Response]: matches: Dict[LinkType, List[UnfurlableUrl]] = defaultdict(list) links_seen = set() integration = slack_request.integration data = slack_request.data.get("event") # An unfurl may have multiple links to unfurl for item in data["links"]: try: url = item["url"] slack_shared_link = parse_link(url) except Exception as e: logger.error("slack.parse-link-error", extra={"error": str(e)}) continue # We would like to track what types of links users are sharing, but # it's a little difficult to do in Sentry because we filter requests # from Slack bots. Instead we just log to Kibana. logger.info("slack.link-shared", extra={"slack_shared_link": slack_shared_link}) link_type, args = match_link(url) # Link can't be unfurled if link_type is None or args is None: continue if (link_type == LinkType.DISCOVER and not slack_request.has_identity and features.has( "organizations:chart-unfurls", slack_request.integration.organizations.all()[0], actor=request.user, )): analytics.record( "integrations.slack.chart_unfurl", organization_id=integration.organizations.all()[0].id, unfurls_count=0, ) self.prompt_link(data, slack_request, integration) return self.respond() # Don't unfurl the same thing multiple times seen_marker = hash(json.dumps((link_type, args), sort_keys=True)) if seen_marker in links_seen: continue links_seen.add(seen_marker) matches[link_type].append(UnfurlableUrl(url=url, args=args)) if not matches: return None # Unfurl each link type results: Dict[str, Any] = {} for link_type, unfurl_data in matches.items(): results.update(link_handlers[link_type].fn(request, integration, unfurl_data, slack_request.user)) if not results: return None access_token = self._get_access_token(integration) payload = { "token": access_token, "channel": data["channel"], "ts": data["message_ts"], "unfurls": json.dumps(results), } client = SlackClient() try: client.post("/chat.unfurl", data=payload) except ApiError as e: logger.error("slack.event.unfurl-error", extra={"error": str(e)}, exc_info=True) return self.respond()
def post(self, request: Request) -> Response: try: slack_request = SlackActionRequest(request) slack_request.validate() except SlackRequestError as e: return self.respond(status=e.status) action_option = slack_request.action_option if action_option in ["approve_member", "reject_member"]: return self.handle_member_approval(slack_request) # 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": return self.respond() # Actions list may be empty when receiving a dialog response data = slack_request.data action_list_raw = data.get("actions", []) action_list = [ MessageAction(**action_data) for action_data in action_list_raw ] organizations = slack_request.integration.organizations.all() if action_option in ["link", "ignore"]: analytics.record( "integrations.slack.chart_unfurl_action", organization_id=organizations[0].id, action=action_option, ) payload = {"delete_original": "true"} try: requests_.post(slack_request.response_url, json=payload) except ApiError as e: logger.error("slack.action.response-error", extra={"error": str(e)}) return self.respond(status=403) return self.respond() # Determine the issue group action is being taken on group_id = slack_request.callback_data["issue"] logging_data = {**slack_request.logging_data, "group_id": group_id} try: group = Group.objects.select_related("project__organization").get( project__in=Project.objects.filter( organization__in=organizations), id=group_id, ) except Group.DoesNotExist: logger.info("slack.action.invalid-issue", extra={**logging_data}) return self.respond(status=403) logging_data["organization_id"] = group.organization.id # Determine the acting user by slack identity try: identity = slack_request.get_identity() except IdentityProvider.DoesNotExist: return self.respond(status=403) if not identity: associate_url = build_linking_url( integration=slack_request.integration, slack_id=slack_request.user_id, channel_id=slack_request.channel_id, response_url=slack_request.response_url, ) return self.respond_ephemeral( LINK_IDENTITY_MESSAGE.format(associate_url=associate_url)) # Handle status dialog submission if slack_request.type == "dialog_submission" and "resolve_type" in data[ "submission"]: # Masquerade a status action action = MessageAction( name="status", value=data["submission"]["resolve_type"], ) try: self.on_status(request, identity, group, action, data, slack_request.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 = SlackIssuesMessageBuilder(group, identity=identity, actions=[action]).build() body = self.construct_reply( attachment, is_message=slack_request.callback_data["is_message"]) # use the original response_url to update the link attachment slack_client = SlackClient() try: slack_client.post( slack_request.callback_data["orig_response_url"], data=body, json=True) except ApiError as e: logger.error("slack.action.response-error", extra={"error": str(e)}) return self.respond() # Usually we'll want to respond with the updated attachment including # the list of actions taken. However, when opening a dialog we do not # have anything to update the message with and will use the # response_url later to update it. defer_attachment_update = False # Handle interaction actions for action in action_list: action_type = action.name try: if action_type == "status": self.on_status(request, identity, group, action, data, slack_request.integration) elif action_type == "assign": self.on_assign(request, identity, group, action) elif action_type == "resolve_dialog": self.open_resolve_dialog(data, group, slack_request.integration) defer_attachment_update = True except client.ApiError as error: return self.api_error(slack_request, group, identity, error, action_type) if defer_attachment_update: return self.respond() # Reload group as it may have been mutated by the action group = Group.objects.get(id=group.id) attachment = SlackIssuesMessageBuilder(group, identity=identity, actions=action_list).build() body = self.construct_reply(attachment, is_message=self.is_message(data)) return self.respond(body)
def on_link_shared(self, request: Request, integration: Integration, token: str, data: Mapping[str, Any]) -> Optional[Response]: matches: Dict[LinkType, List[UnfurlableUrl]] = defaultdict(list) links_seen = set() # An unfurl may have multiple links to unfurl for item in data["links"]: try: # We would like to track what types of links users are sharing, # but it's a little difficult to do in sentry since we filter # requests from Slack bots. Instead we just log to Kibana logger.info( "slack.link-shared", extra={"slack_shared_link": parse_link(item["url"])}) except Exception as e: logger.error("slack.parse-link-error", extra={"error": str(e)}) link_type, args = match_link(item["url"]) # Link can't be unfurled if link_type is None or args is None: continue # Don't unfurl the same thing multiple times seen_marker = hash(json.dumps((link_type, args), sort_keys=True)) if seen_marker in links_seen: continue links_seen.add(seen_marker) matches[link_type].append(UnfurlableUrl(url=item["url"], args=args)) if not matches: return None # Unfurl each link type results: Dict[str, Any] = {} for link_type, unfurl_data in matches.items(): results.update(link_handlers[link_type].fn(request, integration, unfurl_data)) if not results: return None access_token = self._get_access_token(integration) payload = { "token": access_token, "channel": data["channel"], "ts": data["message_ts"], "unfurls": json.dumps(results), } client = SlackClient() try: client.post("/chat.unfurl", data=payload) except ApiError as e: logger.error("slack.event.unfurl-error", extra={"error": str(e)}, exc_info=True) return self.respond()
def _handle_group_actions( self, slack_request: SlackActionRequest, request: Request, action_list: Sequence[MessageAction], ) -> Response: group = get_group(slack_request) if not group: return self.respond(status=403) # Determine the acting user by Slack identity. try: identity = slack_request.get_identity() except IdentityProvider.DoesNotExist: return self.respond(status=403) if not identity: associate_url = build_linking_url( integration=slack_request.integration, slack_id=slack_request.user_id, channel_id=slack_request.channel_id, response_url=slack_request.response_url, ) return self.respond_ephemeral( LINK_IDENTITY_MESSAGE.format(associate_url=associate_url)) # Handle status dialog submission if (slack_request.type == "dialog_submission" and "resolve_type" in slack_request.data["submission"]): # Masquerade a status action action = MessageAction( name="status", value=slack_request.data["submission"]["resolve_type"], ) try: self.on_status(request, identity, group, action) except client.ApiError as error: return self.api_error(slack_request, group, identity, error, "status_dialog") attachment = SlackIssuesMessageBuilder(group, identity=identity, actions=[action]).build() body = self.construct_reply( attachment, is_message=slack_request.callback_data["is_message"]) # use the original response_url to update the link attachment slack_client = SlackClient() try: slack_client.post( slack_request.callback_data["orig_response_url"], data=body, json=True) except ApiError as e: logger.error("slack.action.response-error", extra={"error": str(e)}) return self.respond() # Usually we'll want to respond with the updated attachment including # the list of actions taken. However, when opening a dialog we do not # have anything to update the message with and will use the # response_url later to update it. defer_attachment_update = False # Handle interaction actions for action in action_list: try: if action.name == "status": self.on_status(request, identity, group, action) elif action.name == "assign": self.on_assign(request, identity, group, action) elif action.name == "resolve_dialog": self.open_resolve_dialog(slack_request, group) defer_attachment_update = True except client.ApiError as error: return self.api_error(slack_request, group, identity, error, action.name) if defer_attachment_update: return self.respond() # Reload group as it may have been mutated by the action group = Group.objects.get(id=group.id) attachment = SlackIssuesMessageBuilder(group, identity=identity, actions=action_list).build() body = self.construct_reply(attachment, is_message=_is_message(slack_request.data)) return self.respond(body)
def post(self, request: Request) -> Response: logging_data: Dict[str, str] = {} try: slack_request = SlackActionRequest(request) slack_request.validate() except SlackRequestError as e: return self.respond(status=e.status) 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", "") # 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": return self.respond() channel_id = slack_request.channel_id user_id = slack_request.user_id integration = slack_request.integration response_url = data.get("response_url") if action_option in ["link", "ignore"]: analytics.record( "integrations.slack.chart_unfurl_action", organization_id=integration.organizations.all()[0].id, action=action_option, ) payload = {"delete_original": "true"} try: post(response_url, json=payload) except ApiError as e: logger.error("slack.action.response-error", extra={"error": str(e)}) return self.respond(status=403) return self.respond() logging_data["channel_id"] = channel_id logging_data["slack_user_id"] = user_id logging_data["response_url"] = response_url logging_data["integration_id"] = integration.id # Determine the issue group action is being taken on group_id = slack_request.callback_data["issue"] logging_data["group_id"] = group_id try: group = Group.objects.select_related("project__organization").get( project__in=Project.objects.filter( organization__in=integration.organizations.all()), id=group_id, ) except Group.DoesNotExist: logger.info("slack.action.invalid-issue", extra=logging_data) return self.respond(status=403) logging_data["organization_id"] = group.organization.id # Determine the acting user by slack identity try: idp = IdentityProvider.objects.get( type="slack", external_id=slack_request.team_id) except IdentityProvider.DoesNotExist: logger.error("slack.action.invalid-team-id", extra=logging_data) return self.respond(status=403) try: identity = Identity.objects.select_related("user").get( idp=idp, external_id=user_id) except Identity.DoesNotExist: associate_url = build_linking_url(integration, group.organization, user_id, channel_id, response_url) return self.respond({ "response_type": "ephemeral", "replace_original": False, "text": LINK_IDENTITY_MESSAGE.format(associate_url=associate_url), }) # 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"] } try: self.on_status(request, identity, group, action, data, integration) except client.ApiError as e: if e.status_code == 403: text = UNLINK_IDENTITY_MESSAGE.format( associate_url=build_unlinking_url( integration.id, user_id, channel_id, response_url), user_email=identity.user, org_name=group.organization.name, ) else: text = DEFAULT_ERROR_MESSAGE return self.api_error(e, "status_dialog", logging_data, text) group = Group.objects.get(id=group.id) attachment = build_group_attachment(group, identity=identity, actions=[action]) body = self.construct_reply( attachment, is_message=slack_request.callback_data["is_message"]) # use the original response_url to update the link attachment slack_client = SlackClient() try: slack_client.post( slack_request.callback_data["orig_response_url"], data=body, json=True) except ApiError as e: logger.error("slack.action.response-error", extra={"error": str(e)}) return self.respond() # Usually we'll want to respond with the updated attachment including # the list of actions taken. However, when opening a dialog we do not # have anything to update the message with and will use the # response_url later to update it. defer_attachment_update = False # Handle interaction actions action_type = None try: for action in action_list: action_type = action["name"] if action_type == "status": self.on_status(request, identity, group, action, data, integration) elif action_type == "assign": self.on_assign(request, identity, group, action) elif action_type == "resolve_dialog": self.open_resolve_dialog(data, group, integration) defer_attachment_update = True except client.ApiError as e: if e.status_code == 403: text = UNLINK_IDENTITY_MESSAGE.format( associate_url=build_unlinking_url(integration.id, user_id, channel_id, response_url), user_email=identity.user, org_name=group.organization.name, ) else: text = DEFAULT_ERROR_MESSAGE return self.api_error(e, action_type, logging_data, text) if defer_attachment_update: return self.respond() # 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) body = self.construct_reply(attachment, is_message=self.is_message(data)) return self.respond(body)