oncall-engine/engine/apps/slack/client.py
Joey Orlando a9155130df
update slack_sdk dependency to latest version (#2947)
# What this PR does

- update `slackclient` dependency to latest version. The version we were
using was 5 years old 😲
- first followed the v2 migration guide
[here](https://github.com/slackapi/python-slack-sdk/wiki/Migrating-to-2.x)
followed by the v3 migration guide
[here](https://slack.dev/python-slack-sdk/v3-migration/). The main
changes were:
    - The PyPI project was renamed from `slackclient` to `slack_sdk`
- it is discouraged/harder to call `api_call` and encouraged to call the
helper methods (ex. `chat_postMessage`;
[note](https://github.com/slackapi/python-slack-sdk/wiki/Migrating-to-2.x#web-client-api-changes)
in migration guide docs)
- In 1.x, a failed api call would return the error payload to you and
have you handle the error. In 2.x, a failed api call will throw an
exception. To handle this in your code, you will have to wrap api calls
with a try except block. Since we overload `WebClient.api_call` this was
an easy change and only required a one line change
- remove `apps.slack.slack_client.slack_server.SlackClientServer` class.
The new version of `slack_sdk` handles the case that we needed to
overload for in the first place.
- merged `apps/slack/slack_client/slack_client.py` and
`apps/slack/slack_client/exceptions.py` into `apps/slack/client.py`

## 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)
2023-09-05 11:31:59 +02:00

140 lines
5.3 KiB
Python

import logging
from typing import Optional, Tuple
from django.utils import timezone
from slack_sdk.errors import SlackApiError
from slack_sdk.web import WebClient
from apps.slack.constants import SLACK_RATE_LIMIT_DELAY
logger = logging.getLogger(__name__)
class SlackAPIException(Exception):
def __init__(self, *args, **kwargs):
self.response = {}
if "response" in kwargs:
self.response = kwargs["response"]
super().__init__(*args)
class SlackAPITokenException(SlackAPIException):
pass
class SlackAPIChannelArchivedException(SlackAPIException):
pass
class SlackAPIRateLimitException(SlackAPIException):
pass
class SlackClientWithErrorHandling(WebClient):
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)
cumulative_response = response
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"]
response = api_method(**kwargs)
cumulative_response[paginated_key] += response[paginated_key]
return cumulative_response
def paginated_api_call_with_ratelimit(
self, method: str, paginated_key: str, **kwargs
) -> Tuple[dict, Optional[str], bool]:
"""
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"
"""
api_method = getattr(self, method)
cumulative_response = {}
cursor = kwargs["cursor"]
rate_limited = False
try:
response = api_method(**kwargs)
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
response = api_method(**kwargs)
cumulative_response[paginated_key] += response[paginated_key]
cursor = next_cursor
except SlackAPIRateLimitException:
rate_limited = True
return cumulative_response, cursor, rate_limited
def api_call(self, *args, **kwargs):
try:
response = super(SlackClientWithErrorHandling, self).api_call(*args, **kwargs)
except SlackApiError as err:
response = err.response
if not response["ok"]:
exception_text = "Slack API Call Error: {} \nArgs: {} \nKwargs: {} \nResponse: {}".format(
response["error"], args, kwargs, response
)
if response["error"] == "is_archived":
raise SlackAPIChannelArchivedException(exception_text, response=response)
if (
response["error"] == "rate_limited"
or response["error"] == "ratelimited"
or response["error"] == "message_limit_exceeded"
# "message_limit_exceeded" is related to the limit on post messages for free Slack workspace
):
if "headers" in response and response["headers"].get("Retry-After") is not None:
delay = int(response["headers"]["Retry-After"])
else:
delay = SLACK_RATE_LIMIT_DELAY
response["rate_limit_delay"] = delay
raise SlackAPIRateLimitException(exception_text, response=response)
if response["error"] == "code_already_used":
return response
# Optionally detect account_inactive
if response["error"] == "account_inactive" or response["error"] == "token_revoked":
if "team" in kwargs:
team_identity = kwargs["team"]
del kwargs["team"]
team_identity.detected_token_revoked = timezone.now()
team_identity.is_profile_populated = False
team_identity.save(update_fields=["detected_token_revoked", "is_profile_populated"])
raise SlackAPITokenException(exception_text, response=response)
else:
if "team" in kwargs:
slack_team_identity = kwargs["team"]
if slack_team_identity.detected_token_revoked:
slack_team_identity.detected_token_revoked = None
slack_team_identity.save(update_fields=["detected_token_revoked"])
raise SlackAPIException(exception_text, response=response)
return response