# What this PR does - Rename `SlackClientWithErrorHandling` to just `SlackClient` - Add more error classes + improve the way errors are raised based on the Slack error code - Add API call retries on Slack server errors (e.g. when Slack returns `5xx` errors) - Refactor some methods working with Slack API + add tests ## Which issue(s) this PR fixes - https://github.com/grafana/oncall-private/issues/1837 - https://github.com/grafana/oncall-private/issues/1840 - https://github.com/grafana/oncall-private/issues/1842 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
110 lines
2.8 KiB
Python
110 lines
2.8 KiB
Python
import typing
|
|
|
|
from slack_sdk.web import SlackResponse
|
|
|
|
from apps.slack.constants import SLACK_RATE_LIMIT_DELAY
|
|
|
|
|
|
class UnexpectedResponse(typing.TypedDict):
|
|
status: int
|
|
headers: dict[str, typing.Any]
|
|
body: str
|
|
|
|
|
|
class SlackAPIError(Exception):
|
|
"""
|
|
Base class for Slack API errors. To add a new error class, add a new subclass of SlackAPIError in this file.
|
|
See get_error_class at the end of this file for more details on how these are raised.
|
|
"""
|
|
|
|
errors: tuple[str, ...]
|
|
|
|
def __init__(self, response: UnexpectedResponse | SlackResponse):
|
|
super().__init__(f"Slack API error! Response: {response}")
|
|
self.response = response
|
|
|
|
|
|
class SlackAPIServerError(SlackAPIError):
|
|
errors = ("internal_error", "fatal_error")
|
|
|
|
|
|
class SlackAPITokenError(SlackAPIError):
|
|
errors = ("account_inactive", "token_revoked")
|
|
|
|
|
|
class SlackAPIChannelArchivedError(SlackAPIError):
|
|
errors = ("is_archived",)
|
|
|
|
|
|
class SlackAPIRatelimitError(SlackAPIError):
|
|
errors = ("ratelimited", "rate_limited", "message_limit_exceeded")
|
|
|
|
def __init__(self, response: SlackResponse):
|
|
super().__init__(response)
|
|
self.retry_after = int(response.headers.get("Retry-After", SLACK_RATE_LIMIT_DELAY))
|
|
|
|
|
|
class SlackAPIPlanUpgradeRequiredError(SlackAPIError):
|
|
errors = ("plan_upgrade_required",)
|
|
|
|
|
|
class SlackAPIInvalidAuthError(SlackAPIError):
|
|
errors = ("invalid_auth",)
|
|
|
|
|
|
class SlackAPIUsergroupNotFoundError(SlackAPIError):
|
|
errors = ("no_such_subteam",)
|
|
|
|
|
|
class SlackAPIChannelNotFoundError(SlackAPIError):
|
|
errors = ("channel_not_found",)
|
|
|
|
|
|
class SlackAPIMessageNotFoundError(SlackAPIError):
|
|
errors = ("message_not_found",)
|
|
|
|
|
|
class SlackAPIUserNotFoundError(SlackAPIError):
|
|
errors = ("user_not_found",)
|
|
|
|
|
|
class SlackAPIChannelInactiveError(SlackAPIError):
|
|
errors = ("is_inactive",)
|
|
|
|
|
|
class SlackAPIRestrictedActionError(SlackAPIError):
|
|
errors = ("restricted_action",)
|
|
|
|
|
|
class SlackAPIPermissionDeniedError(SlackAPIError):
|
|
errors = ("permission_denied",)
|
|
|
|
|
|
class SlackAPIFetchMembersFailedError(SlackAPIError):
|
|
errors = ("fetch_members_failed",)
|
|
|
|
|
|
class SlackAPIViewNotFoundError(SlackAPIError):
|
|
errors = ("not_found",)
|
|
|
|
|
|
class SlackAPICannotDMBotError(SlackAPIError):
|
|
errors = ("cannot_dm_bot",)
|
|
|
|
|
|
class SlackAPIMethodNotSupportedForChannelTypeError(SlackAPIError):
|
|
errors = ("method_not_supported_for_channel_type",)
|
|
|
|
|
|
_error_to_error_class = {
|
|
error: error_class for error_class in SlackAPIError.__subclasses__() for error in error_class.errors
|
|
}
|
|
|
|
|
|
def get_error_class(response: UnexpectedResponse | SlackResponse) -> typing.Type[SlackAPIError]:
|
|
"""Get an appropriate error class for the response"""
|
|
|
|
if isinstance(response, dict): # UnexpectedResponse
|
|
return SlackAPIServerError
|
|
|
|
return _error_to_error_class.get(response["error"], SlackAPIError)
|