Esempio n. 1
0
    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()
Esempio n. 2
0
    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)})
Esempio n. 3
0
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,
            },
        )
Esempio n. 4
0
    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)
Esempio n. 5
0
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)})
Esempio n. 6
0
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,
    )
Esempio n. 7
0
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)
Esempio n. 8
0
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,
    )
Esempio n. 9
0
    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()
Esempio n. 10
0
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,
    )
Esempio n. 11
0
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,
    )
Esempio n. 12
0
 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
Esempio n. 13
0
    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)
Esempio n. 14
0
    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()
Esempio n. 15
0
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})
Esempio n. 16
0
    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()
Esempio n. 17
0
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,
        )
Esempio n. 18
0
    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()
Esempio n. 19
0
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,
    )
Esempio n. 20
0
    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()
Esempio n. 21
0
    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)
Esempio n. 22
0
    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()
Esempio n. 23
0
    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)
Esempio n. 24
0
    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)