2022-06-03 08:09:47 -06:00
|
|
|
import logging
|
2023-09-12 10:49:16 +01:00
|
|
|
import typing
|
2023-07-19 09:17:21 +02:00
|
|
|
from typing import Optional, Tuple
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
from django.utils import timezone
|
2023-09-12 10:49:16 +01:00
|
|
|
from rest_framework import status
|
|
|
|
|
from slack_sdk.errors import SlackApiError as SlackSDKApiError
|
|
|
|
|
from slack_sdk.http_retry import HttpRequest, HttpResponse, RetryHandler, RetryState, default_retry_handlers
|
2023-11-21 14:32:29 -03:00
|
|
|
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
|
2023-09-07 12:25:29 +01:00
|
|
|
from slack_sdk.web import SlackResponse, WebClient
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
from apps.slack.errors import SlackAPIRatelimitError, SlackAPIServerError, SlackAPITokenError, get_error_class
|
|
|
|
|
|
|
|
|
|
if typing.TYPE_CHECKING:
|
|
|
|
|
from apps.slack.models import SlackTeamIdentity
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
class SlackServerErrorRetryHandler(RetryHandler):
|
|
|
|
|
"""Retry failed Slack API calls on Slack server errors"""
|
2023-09-05 11:31:59 +02:00
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
def _can_retry(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
state: RetryState,
|
|
|
|
|
request: HttpRequest,
|
|
|
|
|
response: Optional[HttpResponse] = None,
|
|
|
|
|
error: Optional[Exception] = None,
|
|
|
|
|
) -> bool:
|
|
|
|
|
# Retry Slack API call on 5xx errors
|
|
|
|
|
if response and response.status_code in [
|
|
|
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
|
status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
|
|
|
status.HTTP_504_GATEWAY_TIMEOUT,
|
|
|
|
|
]:
|
|
|
|
|
return True
|
2023-09-05 11:31:59 +02:00
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
# Retry Slack API call on "internal_error" and "fatal_error" errors
|
|
|
|
|
if response and response.body and response.body.get("error") in SlackAPIServerError.errors:
|
|
|
|
|
return True
|
2023-09-05 11:31:59 +02:00
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
return False
|
2023-09-05 11:31:59 +02:00
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-11-21 14:32:29 -03:00
|
|
|
# retries when HTTP status 429 is returned using the Retry-After header information
|
2023-11-30 09:35:46 -03:00
|
|
|
rate_limit_handler = RateLimitErrorRetryHandler(max_retry_count=1)
|
2023-09-12 10:49:16 +01:00
|
|
|
server_error_retry_handler = SlackServerErrorRetryHandler(max_retry_count=2)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
class SlackClient(WebClient):
|
2023-11-30 09:35:46 -03:00
|
|
|
def __init__(
|
|
|
|
|
self, slack_team_identity: "SlackTeamIdentity", enable_ratelimit_retry=False, timeout: int = 30
|
|
|
|
|
) -> None:
|
|
|
|
|
retry_handlers = default_retry_handlers() + [server_error_retry_handler]
|
|
|
|
|
if enable_ratelimit_retry:
|
|
|
|
|
retry_handlers += [rate_limit_handler]
|
|
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
super().__init__(
|
|
|
|
|
token=slack_team_identity.bot_access_token,
|
|
|
|
|
timeout=timeout,
|
2023-11-30 09:35:46 -03:00
|
|
|
retry_handlers=retry_handlers,
|
2023-09-12 10:49:16 +01:00
|
|
|
)
|
|
|
|
|
self.slack_team_identity = slack_team_identity
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-09-05 11:31:59 +02:00
|
|
|
def paginated_api_call(self, method: str, paginated_key: str, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
`paginated_key` represents a key from the response which is paginated. For example "users" or "channels"
|
|
|
|
|
"""
|
|
|
|
|
api_method = getattr(self, method)
|
|
|
|
|
|
|
|
|
|
response = api_method(**kwargs)
|
2023-09-05 14:32:28 +02:00
|
|
|
cumulative_response = response.data
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
while (
|
|
|
|
|
"response_metadata" in response
|
|
|
|
|
and "next_cursor" in response["response_metadata"]
|
|
|
|
|
and response["response_metadata"]["next_cursor"] != ""
|
|
|
|
|
):
|
|
|
|
|
kwargs["cursor"] = response["response_metadata"]["next_cursor"]
|
2023-09-05 14:32:28 +02:00
|
|
|
response = api_method(**kwargs).data
|
2023-09-05 11:31:59 +02:00
|
|
|
cumulative_response[paginated_key] += response[paginated_key]
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
return cumulative_response
|
|
|
|
|
|
2023-09-05 11:31:59 +02:00
|
|
|
def paginated_api_call_with_ratelimit(
|
|
|
|
|
self, method: str, paginated_key: str, **kwargs
|
|
|
|
|
) -> Tuple[dict, Optional[str], bool]:
|
2023-07-19 09:17:21 +02:00
|
|
|
"""
|
2023-09-05 11:31:59 +02:00
|
|
|
This method does paginated api calls and handle slack rate limit errors in order to return collected data
|
|
|
|
|
and have the ability to continue doing paginated requests from the last successful cursor.
|
|
|
|
|
|
|
|
|
|
Return last successful cursor instead of next cursor to avoid data loss during delay time.
|
|
|
|
|
|
|
|
|
|
`paginated_key` represents a key from the response which is paginated. For example "users" or "channels"
|
2023-07-19 09:17:21 +02:00
|
|
|
"""
|
2023-09-05 11:31:59 +02:00
|
|
|
api_method = getattr(self, method)
|
|
|
|
|
|
2023-07-19 09:17:21 +02:00
|
|
|
cumulative_response = {}
|
2023-09-05 11:31:59 +02:00
|
|
|
cursor = kwargs["cursor"]
|
2023-07-19 09:17:21 +02:00
|
|
|
rate_limited = False
|
|
|
|
|
|
|
|
|
|
try:
|
2023-09-05 14:32:28 +02:00
|
|
|
response = api_method(**kwargs).data
|
2023-07-19 09:17:21 +02:00
|
|
|
cumulative_response = response
|
|
|
|
|
cursor = response["response_metadata"]["next_cursor"]
|
|
|
|
|
|
|
|
|
|
while (
|
|
|
|
|
"response_metadata" in response
|
|
|
|
|
and "next_cursor" in response["response_metadata"]
|
|
|
|
|
and response["response_metadata"]["next_cursor"] != ""
|
|
|
|
|
):
|
|
|
|
|
next_cursor = response["response_metadata"]["next_cursor"]
|
|
|
|
|
kwargs["cursor"] = next_cursor
|
2023-09-05 14:32:28 +02:00
|
|
|
response = api_method(**kwargs).data
|
2023-09-05 11:31:59 +02:00
|
|
|
cumulative_response[paginated_key] += response[paginated_key]
|
2023-07-19 09:17:21 +02:00
|
|
|
cursor = next_cursor
|
|
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
except SlackAPIRatelimitError:
|
2023-07-19 09:17:21 +02:00
|
|
|
rate_limited = True
|
|
|
|
|
|
|
|
|
|
return cumulative_response, cursor, rate_limited
|
|
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
def api_call(self, *args, **kwargs) -> SlackResponse:
|
|
|
|
|
"""Wrap Slack SDK api_call with more granular error handling and logging"""
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
try:
|
|
|
|
|
response = super().api_call(*args, **kwargs)
|
|
|
|
|
self._unmark_token_revoked() # unmark token as revoked if the API call was successful
|
|
|
|
|
return response
|
|
|
|
|
except SlackSDKApiError as e:
|
|
|
|
|
logger.error(
|
|
|
|
|
"Slack API call error! slack_team_identity={} args={} kwargs={} status={} error={} response={}".format(
|
|
|
|
|
self.slack_team_identity.pk,
|
|
|
|
|
args,
|
|
|
|
|
kwargs,
|
|
|
|
|
e.response["status"] if isinstance(e.response, dict) else e.response.status_code,
|
|
|
|
|
e.response.get("error"),
|
|
|
|
|
e.response,
|
|
|
|
|
)
|
2022-06-03 08:09:47 -06:00
|
|
|
)
|
|
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
# narrow down the error
|
|
|
|
|
error_class = get_error_class(e.response)
|
|
|
|
|
|
|
|
|
|
# mark / unmark token as revoked
|
|
|
|
|
if error_class is SlackAPITokenError:
|
|
|
|
|
self._mark_token_revoked()
|
2022-06-03 08:09:47 -06:00
|
|
|
else:
|
2023-09-12 10:49:16 +01:00
|
|
|
self._unmark_token_revoked()
|
|
|
|
|
|
|
|
|
|
# raise the narrowed down error class
|
|
|
|
|
raise error_class(e.response) from e
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
def _mark_token_revoked(self) -> None:
|
|
|
|
|
if not self.slack_team_identity.detected_token_revoked:
|
|
|
|
|
self.slack_team_identity.detected_token_revoked = timezone.now()
|
|
|
|
|
self.slack_team_identity.save(update_fields=["detected_token_revoked"])
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
def _unmark_token_revoked(self) -> None:
|
|
|
|
|
if self.slack_team_identity.detected_token_revoked:
|
|
|
|
|
self.slack_team_identity.detected_token_revoked = None
|
|
|
|
|
self.slack_team_identity.save(update_fields=["detected_token_revoked"])
|