|
@@ -1,14 +1,16 @@
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
from collections import defaultdict
|
|
|
-from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple
|
|
|
+from typing import Any, Mapping, MutableMapping
|
|
|
|
|
|
from rest_framework.request import Request
|
|
|
from rest_framework.response import Response
|
|
|
|
|
|
from sentry import analytics, features
|
|
|
from sentry.integrations.slack.client import SlackClient
|
|
|
-from sentry.integrations.slack.message_builder.base.block import BlockSlackMessageBuilder
|
|
|
-from sentry.integrations.slack.message_builder.event import SlackEventMessageBuilder
|
|
|
-from sentry.integrations.slack.requests.base import SlackRequest, SlackRequestError
|
|
|
+from sentry.integrations.slack.message_builder.help import SlackHelpMessageBuilder
|
|
|
+from sentry.integrations.slack.message_builder.prompt import SlackPromptLinkMessageBuilder
|
|
|
+from sentry.integrations.slack.requests.base import SlackDMRequest, SlackRequestError
|
|
|
from sentry.integrations.slack.requests.event import COMMANDS, SlackEventRequest
|
|
|
from sentry.integrations.slack.unfurl import LinkType, UnfurlableUrl, link_handlers, match_link
|
|
|
from sentry.integrations.slack.views.link_identity import build_linking_url
|
|
@@ -23,7 +25,7 @@ from .base import SlackDMEndpoint
|
|
|
from .command import LINK_FROM_CHANNEL_MESSAGE
|
|
|
|
|
|
|
|
|
-class SlackEventEndpoint(SlackDMEndpoint): # type: ignore
|
|
|
+class SlackEventEndpoint(SlackDMEndpoint):
|
|
|
"""
|
|
|
XXX(dcramer): a lot of this is copied from sentry-plugins right now, and will need refactoring
|
|
|
"""
|
|
@@ -31,26 +33,10 @@ class SlackEventEndpoint(SlackDMEndpoint): # type: ignore
|
|
|
authentication_classes = ()
|
|
|
permission_classes = ()
|
|
|
|
|
|
- def is_bot(self, data: Mapping[str, Any]) -> bool:
|
|
|
- """
|
|
|
- If it's a message posted by our bot, we don't want to respond since that
|
|
|
- will cause an infinite loop of messages.
|
|
|
- """
|
|
|
- return bool(data.get("bot_id"))
|
|
|
-
|
|
|
- def get_command_and_args(self, slack_request: SlackRequest) -> Tuple[str, Sequence[str]]:
|
|
|
- data = slack_request.data.get("event")
|
|
|
- command = data["text"].lower().split()
|
|
|
- return command[0], command[1:]
|
|
|
-
|
|
|
- def reply(self, slack_request: SlackRequest, message: str) -> Response:
|
|
|
+ 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()
|
|
|
- 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:
|
|
@@ -58,49 +44,40 @@ class SlackEventEndpoint(SlackDMEndpoint): # type: ignore
|
|
|
|
|
|
return self.respond()
|
|
|
|
|
|
- def link_team(self, slack_request: SlackRequest) -> Any:
|
|
|
+ def link_team(self, slack_request: SlackDMRequest) -> Response:
|
|
|
return self.reply(slack_request, LINK_FROM_CHANNEL_MESSAGE)
|
|
|
|
|
|
- def unlink_team(self, slack_request: SlackRequest) -> Any:
|
|
|
+ def unlink_team(self, slack_request: SlackDMRequest) -> Response:
|
|
|
return self.reply(slack_request, LINK_FROM_CHANNEL_MESSAGE)
|
|
|
|
|
|
- def _get_access_token(self, integration: Integration) -> Any:
|
|
|
- # the classic bot tokens must use the user auth token for URL unfurling
|
|
|
- # we stored the user_access_token there
|
|
|
- # but for workspace apps and new slack bot tokens, we can just use access_token
|
|
|
- return integration.metadata.get("user_access_token") or integration.metadata["access_token"]
|
|
|
+ def _get_access_token(self, integration: Integration) -> str:
|
|
|
+ """
|
|
|
+ The classic bot tokens must use the user auth token for URL unfurling we
|
|
|
+ stored the user_access_token there but for workspace apps and new Slack
|
|
|
+ bot tokens, we can just use access_token.
|
|
|
+ """
|
|
|
+ access_token: str = (
|
|
|
+ integration.metadata.get("user_access_token") or integration.metadata["access_token"]
|
|
|
+ )
|
|
|
+ return access_token
|
|
|
|
|
|
def on_url_verification(self, request: Request, data: Mapping[str, str]) -> Response:
|
|
|
return self.respond({"challenge": data["challenge"]})
|
|
|
|
|
|
- def prompt_link(
|
|
|
- self,
|
|
|
- data: Mapping[str, Any],
|
|
|
- slack_request: SlackRequest,
|
|
|
- integration: Integration,
|
|
|
- ):
|
|
|
+ def prompt_link(self, slack_request: SlackDMRequest) -> None:
|
|
|
associate_url = build_linking_url(
|
|
|
- integration=integration,
|
|
|
+ integration=slack_request.integration,
|
|
|
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"],
|
|
|
+ "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.",
|
|
|
- "blocks": json.dumps(blocks),
|
|
|
+ **SlackPromptLinkMessageBuilder(associate_url).as_payload(),
|
|
|
}
|
|
|
|
|
|
client = SlackClient()
|
|
@@ -109,16 +86,16 @@ class SlackEventEndpoint(SlackDMEndpoint): # type: ignore
|
|
|
except ApiError as e:
|
|
|
logger.error("slack.event.unfurl-error", extra={"error": str(e)}, exc_info=True)
|
|
|
|
|
|
- def on_message(
|
|
|
- self, request: Request, integration: Integration, token: str, data: Mapping[str, Any]
|
|
|
- ) -> Response:
|
|
|
- channel = data["channel"]
|
|
|
+ def on_message(self, request: Request, slack_request: SlackDMRequest) -> Response:
|
|
|
command = request.data.get("event", {}).get("text", "").lower()
|
|
|
- if self.is_bot(data) or not command:
|
|
|
+ if slack_request.is_bot() or not command:
|
|
|
return self.respond()
|
|
|
- access_token = self._get_access_token(integration)
|
|
|
- headers = {"Authorization": f"Bearer {access_token}"}
|
|
|
- payload = {"channel": channel, **SlackEventMessageBuilder(integration, command).build()}
|
|
|
+
|
|
|
+ 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)
|
|
@@ -127,19 +104,15 @@ class SlackEventEndpoint(SlackDMEndpoint): # type: ignore
|
|
|
|
|
|
return self.respond()
|
|
|
|
|
|
- def on_link_shared(
|
|
|
- self,
|
|
|
- request: Request,
|
|
|
- slack_request: SlackRequest,
|
|
|
- ) -> Optional[Response]:
|
|
|
- matches: Dict[LinkType, List[UnfurlableUrl]] = defaultdict(list)
|
|
|
+ def on_link_shared(self, request: Request, slack_request: SlackDMRequest) -> bool:
|
|
|
+ """Returns true on success"""
|
|
|
+ matches: MutableMapping[LinkType, list[UnfurlableUrl]] = defaultdict(list)
|
|
|
links_seen = set()
|
|
|
|
|
|
- integration = slack_request.integration
|
|
|
- data = slack_request.data.get("event")
|
|
|
+ data = slack_request.data.get("event", {})
|
|
|
|
|
|
# An unfurl may have multiple links to unfurl
|
|
|
- for item in data["links"]:
|
|
|
+ for item in data.get("links", []):
|
|
|
try:
|
|
|
url = item["url"]
|
|
|
slack_shared_link = parse_link(url)
|
|
@@ -157,22 +130,19 @@ class SlackEventEndpoint(SlackDMEndpoint): # type: ignore
|
|
|
if link_type is None or args is None:
|
|
|
continue
|
|
|
|
|
|
+ organization = slack_request.integration.organizations.all()[0]
|
|
|
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,
|
|
|
- )
|
|
|
+ and features.has("organizations:chart-unfurls", organization, actor=request.user)
|
|
|
):
|
|
|
analytics.record(
|
|
|
"integrations.slack.chart_unfurl",
|
|
|
- organization_id=integration.organizations.all()[0].id,
|
|
|
+ organization_id=organization.id,
|
|
|
unfurls_count=0,
|
|
|
)
|
|
|
- self.prompt_link(data, slack_request, integration)
|
|
|
- return self.respond()
|
|
|
+ self.prompt_link(slack_request)
|
|
|
+ return True
|
|
|
|
|
|
# Don't unfurl the same thing multiple times
|
|
|
seen_marker = hash(json.dumps((link_type, args), sort_keys=True))
|
|
@@ -183,19 +153,24 @@ class SlackEventEndpoint(SlackDMEndpoint): # type: ignore
|
|
|
matches[link_type].append(UnfurlableUrl(url=url, args=args))
|
|
|
|
|
|
if not matches:
|
|
|
- return None
|
|
|
+ return False
|
|
|
|
|
|
# Unfurl each link type
|
|
|
- results: Dict[str, Any] = {}
|
|
|
+ results: MutableMapping[str, Any] = {}
|
|
|
for link_type, unfurl_data in matches.items():
|
|
|
results.update(
|
|
|
- link_handlers[link_type].fn(request, integration, unfurl_data, slack_request.user)
|
|
|
+ link_handlers[link_type].fn(
|
|
|
+ request,
|
|
|
+ slack_request.integration,
|
|
|
+ unfurl_data,
|
|
|
+ slack_request.user,
|
|
|
+ )
|
|
|
)
|
|
|
|
|
|
if not results:
|
|
|
- return None
|
|
|
+ return False
|
|
|
|
|
|
- access_token = self._get_access_token(integration)
|
|
|
+ access_token = self._get_access_token(slack_request.integration)
|
|
|
|
|
|
payload = {
|
|
|
"token": access_token,
|
|
@@ -210,7 +185,7 @@ class SlackEventEndpoint(SlackDMEndpoint): # type: ignore
|
|
|
except ApiError as e:
|
|
|
logger.error("slack.event.unfurl-error", extra={"error": str(e)}, exc_info=True)
|
|
|
|
|
|
- return self.respond()
|
|
|
+ return True
|
|
|
|
|
|
# TODO(dcramer): implement app_uninstalled and tokens_revoked
|
|
|
@transaction_start("SlackEventEndpoint")
|
|
@@ -225,30 +200,19 @@ class SlackEventEndpoint(SlackDMEndpoint): # type: ignore
|
|
|
return self.on_url_verification(request, slack_request.data)
|
|
|
|
|
|
if slack_request.type == "link_shared":
|
|
|
- resp = self.on_link_shared(
|
|
|
- request,
|
|
|
- slack_request,
|
|
|
- )
|
|
|
-
|
|
|
- if resp:
|
|
|
- return resp
|
|
|
+ if self.on_link_shared(request, slack_request):
|
|
|
+ return self.respond()
|
|
|
|
|
|
if slack_request.type == "message":
|
|
|
- data = slack_request.data.get("event")
|
|
|
- if self.is_bot(data):
|
|
|
+ if slack_request.is_bot():
|
|
|
return self.respond()
|
|
|
|
|
|
- command = data.get("text")
|
|
|
+ command, _ = slack_request.get_command_and_args()
|
|
|
if command in COMMANDS:
|
|
|
resp = super().post_dispatcher(slack_request)
|
|
|
|
|
|
else:
|
|
|
- resp = self.on_message(
|
|
|
- request,
|
|
|
- slack_request.integration,
|
|
|
- slack_request.data.get("token"),
|
|
|
- slack_request.data.get("event"),
|
|
|
- )
|
|
|
+ resp = self.on_message(request, slack_request)
|
|
|
|
|
|
if resp:
|
|
|
return resp
|