oncall-engine/engine/apps/slack/errors.py
Joey Orlando 93c92a7a4c
Update Slack user group for a schedule - handle paid_team_only Slack API error (#4793)
# What this PR does

Semi-related to https://github.com/grafana/oncall-private/issues/2131

Addresses occasional task failures for
`apps.slack.tasks.update_slack_user_group_for_schedules` when trying to
update a Slack user group for a non-paid Slack account. [Slack's
documentation](https://slack.com/help/articles/212906697-Create-a-user-group)
mentions this is a paid only feature, hence the error
([logs](https://ops.grafana-ops.net/goto/-AWfsrrIR?orgId=1) from an
actual task):
```
2024-08-08 16:20:36,613 source=engine:celery worker=ForkPoolWorker-16 task_id=6bdaae94-1552-4b6d-93e2-e2fa0bae57b1 task_name=apps.slack.tasks.update_slack_user_group_for_schedules name=apps.slack.models.slack_usergroup level=WARNING Slack usergroup S06LW5GJ88Z update failed: Slack API error! Response: {'ok': False, 'error': 'paid_teams_only'}
```

Updated our docs on our Slack integration to emphasize that this feature
_only_ works for paid Slack accounts

## 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] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
2024-08-09 14:51:01 +00:00

127 lines
3.3 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", "subteam_not_found")
class SlackAPIUsergroupPaidTeamOnlyError(SlackAPIError):
"""
https://api.slack.com/methods/usergroups.create#:~:text=Name%20too%20long.-,paid_teams_only,-Usergroups%20can%20only
https://slack.com/help/articles/212906697-Create-a-user-group
"""
errors = ("paid_teams_only",)
class SlackAPIInvalidUsersError(SlackAPIError):
errors = ("invalid_users",)
class SlackAPIChannelNotFoundError(SlackAPIError):
errors = ("channel_not_found",)
class SlackAPIMessageNotFoundError(SlackAPIError):
errors = ("message_not_found",)
class SlackAPICantUpdateMessageError(SlackAPIError):
errors = ("cant_update_message",)
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)