예제 #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()
예제 #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)})
예제 #3
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)
예제 #4
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,
            },
        )
예제 #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)})
예제 #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,
    )
예제 #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)
예제 #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,
    )
예제 #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()
예제 #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,
    )
예제 #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,
    )
예제 #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
예제 #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)
예제 #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()
예제 #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})
예제 #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()
예제 #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,
        )
예제 #18
0
def validate_channel_id(name: str, integration_id: Optional[int],
                        input_channel_id: str) -> None:
    """
    In the case that the user is creating an alert via the API and providing the channel ID and name
    themselves, we want to make sure both values are correct.
    """
    try:
        integration = Integration.objects.get(id=integration_id)
    except Integration.DoesNotExist:
        raise Http404

    token = integration.metadata["access_token"]
    headers = {"Authorization": f"Bearer {token}"}
    payload = {"channel": input_channel_id}
    client = SlackClient()

    try:
        results = client.get("/conversations.info",
                             headers=headers,
                             params=payload)
    except ApiError as e:
        if e.text == "channel_not_found":
            raise ValidationError("Channel not found. Invalid ID provided.")
        logger.info("rule.slack.conversation_info_failed",
                    extra={"error": str(e)})
        raise IntegrationError("Could not retrieve Slack channel information.")

    if not isinstance(results, dict):
        raise IntegrationError("Bad slack channel list response.")

    stripped_channel_name = strip_channel_name(name)
    if not stripped_channel_name == results["channel"]["name"]:
        channel_name = results["channel"]["name"]
        raise ValidationError(
            f"Received channel name {channel_name} does not match inputted channel name {stripped_channel_name}."
        )
예제 #19
0
파일: event.py 프로젝트: baoxin/sentry
    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()
예제 #20
0
def get_users(integration: Integration,
              organization: Organization) -> Sequence[Mapping[str, Any]]:
    access_token = (integration.metadata.get("user_access_token")
                    or integration.metadata["access_token"])
    headers = {"Authorization": f"Bearer {access_token}"}
    client = SlackClient()

    user_list = []
    next_cursor = None
    for i in range(SLACK_GET_USERS_PAGE_LIMIT):
        try:
            next_users = client.get(
                "/users.list",
                headers=headers,
                params={
                    "limit": SLACK_GET_USERS_PAGE_SIZE,
                    "cursor": next_cursor
                },
            )
        except ApiError as e:
            logger.info(
                "post_install.fail.slack_users.list",
                extra={
                    "error": str(e),
                    "organization": organization.slug,
                    "integration_id": integration.id,
                },
            )
            break
        user_list += next_users["members"]

        next_cursor = next_users["response_metadata"]["next_cursor"]
        if not next_cursor:
            break

    return user_list
예제 #21
0
파일: action.py 프로젝트: KingDEV95/sentry
    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)
예제 #22
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()
예제 #23
0
def get_channel_id_with_timeout(
    integration: "Integration",
    name: Optional[str],
    timeout: int,
) -> Tuple[str, Optional[str], bool]:
    """
    Fetches the internal slack id of a channel.
    :param integration: The slack integration
    :param name: The name of the channel
    :param timeout: Our self-imposed time limit.
    :return: a tuple of three values
        1. prefix: string (`"#"` or `"@"`)
        2. channel_id: string or `None`
        3. timed_out: boolean (whether we hit our self-imposed time limit)
    """

    headers = {
        "Authorization": f"Bearer {integration.metadata['access_token']}"
    }

    payload = {
        "exclude_archived": False,
        "exclude_members": True,
        "types": "public_channel,private_channel",
    }

    list_types = LIST_TYPES

    time_to_quit = time.time() + timeout

    client = SlackClient()
    id_data: Optional[Tuple[str, Optional[str], bool]] = None
    found_duplicate = False
    prefix = ""
    for list_type, result_name, prefix in list_types:
        cursor = ""
        while True:
            endpoint = f"/{list_type}.list"
            try:
                # Slack limits the response of `<list_type>.list` to 1000 channels
                items = client.get(endpoint,
                                   headers=headers,
                                   params=dict(payload,
                                               cursor=cursor,
                                               limit=1000))
            except ApiRateLimitedError as e:
                logger.info(f"rule.slack.{list_type}_list_rate_limited",
                            extra={"error": str(e)})
                raise e
            except ApiError as e:
                logger.info(f"rule.slack.{list_type}_list_rate_limited",
                            extra={"error": str(e)})
                return prefix, None, False

            if not isinstance(items, dict):
                continue

            for c in items[result_name]:
                # The "name" field is unique (this is the username for users)
                # so we return immediately if we find a match.
                # convert to lower case since all names in Slack are lowercase
                if name and c["name"].lower() == name.lower():
                    return prefix, c["id"], False
                # If we don't get a match on a unique identifier, we look through
                # the users' display names, and error if there is a repeat.
                if list_type == "users":
                    profile = c.get("profile")
                    if profile and profile.get("display_name") == name:
                        if id_data:
                            found_duplicate = True
                        else:
                            id_data = (prefix, c["id"], False)

            cursor = items.get("response_metadata",
                               {}).get("next_cursor", None)
            if time.time() > time_to_quit:
                return prefix, None, True

            if not cursor:
                break
        if found_duplicate:
            raise DuplicateDisplayNameError(name)
        elif id_data:
            return id_data

    return prefix, None, False
예제 #24
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)
예제 #25
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)
예제 #26
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,
    )
예제 #27
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()