diff --git a/engine/apps/alerts/tasks/notify_ical_schedule_shift.py b/engine/apps/alerts/tasks/notify_ical_schedule_shift.py index 9321717c..96f3331a 100644 --- a/engine/apps/alerts/tasks/notify_ical_schedule_shift.py +++ b/engine/apps/alerts/tasks/notify_ical_schedule_shift.py @@ -6,9 +6,8 @@ from typing import TYPE_CHECKING from django.utils import timezone from apps.schedules.ical_utils import calculate_shift_diff, parse_event_uid +from apps.slack.client import SlackAPIException, SlackAPITokenException, SlackClientWithErrorHandling from apps.slack.scenarios import scenario_step -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import SlackAPIException, SlackAPITokenException from common.custom_celery_tasks import shared_dedicated_queue_retry_task from .task_logger import task_logger @@ -152,8 +151,7 @@ def notify_ical_schedule_shift(schedule_pk): report_blocks = step.get_report_blocks_ical(new_shifts, upcoming_shifts, schedule, schedule.empty_oncall) try: - slack_client.api_call( - "chat.postMessage", + slack_client.chat_postMessage( channel=schedule.channel, blocks=report_blocks, text=f"On-call shift for schedule {schedule.name} has changed", diff --git a/engine/apps/alerts/tests/test_notify_ical_schedule_shift.py b/engine/apps/alerts/tests/test_notify_ical_schedule_shift.py index 5d931181..5f7ab590 100644 --- a/engine/apps/alerts/tests/test_notify_ical_schedule_shift.py +++ b/engine/apps/alerts/tests/test_notify_ical_schedule_shift.py @@ -100,7 +100,7 @@ def test_next_shift_notification_long_shifts( with patch("apps.alerts.tasks.notify_ical_schedule_shift.datetime", Mock(wraps=datetime)) as mock_datetime: mock_datetime.datetime.now.return_value = datetime.datetime(2021, 9, 29, 12, 0, tzinfo=pytz.UTC) - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_ical_schedule_shift(ical_schedule.pk) slack_blocks = mock_slack_api_call.call_args_list[0][1]["blocks"] @@ -203,7 +203,7 @@ def test_overrides_changes_no_current_no_triggering_notification( schedule.prev_ical_file_overrides = ical_before schedule.save() - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_ical_schedule_shift(schedule.pk) assert not mock_slack_api_call.called @@ -251,7 +251,7 @@ def test_no_changes_no_triggering_notification( schedule.empty_oncall = False schedule.save() - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_ical_schedule_shift(schedule.pk) assert not mock_slack_api_call.called @@ -299,7 +299,7 @@ def test_current_shift_changes_trigger_notification( schedule.empty_oncall = False schedule.save() - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_ical_schedule_shift(schedule.pk) assert mock_slack_api_call.called @@ -363,7 +363,7 @@ def test_current_shift_changes_swap_split( schedule.empty_oncall = False schedule.save() - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_ical_schedule_shift(schedule.pk) text_block = mock_slack_api_call.call_args_list[0][1]["blocks"][0]["text"]["text"] @@ -432,7 +432,7 @@ def test_next_shift_changes_no_triggering_notification( on_call_shift_2.add_rolling_users([[user2]]) schedule.refresh_ical_file() - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_ical_schedule_shift(schedule.pk) assert not mock_slack_api_call.called @@ -499,7 +499,7 @@ def test_lower_priority_changes_no_triggering_notification( on_call_shift_2.add_rolling_users([[user2]]) schedule.refresh_ical_file() - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_ical_schedule_shift(schedule.pk) assert not mock_slack_api_call.called @@ -629,7 +629,7 @@ def test_vtimezone_changes_no_triggering_notification( schedule.cached_ical_file_primary = ical_after schedule.save() - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_ical_schedule_shift(schedule.pk) assert not mock_slack_api_call.called @@ -686,7 +686,7 @@ def test_no_changes_no_triggering_notification_from_old_to_new_task_version( schedule.empty_oncall = False schedule.save() - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_ical_schedule_shift(schedule.pk) assert not mock_slack_api_call.called @@ -748,7 +748,7 @@ def test_current_shift_changes_trigger_notification_from_old_to_new_task_version on_call_shift.add_rolling_users([[user2]]) schedule.refresh_ical_file() - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_ical_schedule_shift(schedule.pk) assert mock_slack_api_call.called @@ -813,7 +813,7 @@ def test_next_shift_notification_long_and_short_shifts( schedule.empty_oncall = False schedule.save() - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_ical_schedule_shift(schedule.pk) assert mock_slack_api_call.called diff --git a/engine/apps/api/tests/conftest.py b/engine/apps/api/tests/conftest.py index bfeedb25..3c3783b0 100644 --- a/engine/apps/api/tests/conftest.py +++ b/engine/apps/api/tests/conftest.py @@ -3,8 +3,8 @@ from datetime import timedelta import pytest from django.utils import timezone +from apps.slack.client import SlackClientWithErrorHandling from apps.slack.scenarios.distribute_alerts import AlertShootingStep -from apps.slack.slack_client import SlackClientWithErrorHandling @pytest.fixture() diff --git a/engine/apps/integrations/tasks.py b/engine/apps/integrations/tasks.py index 856818c4..1e788f55 100644 --- a/engine/apps/integrations/tasks.py +++ b/engine/apps/integrations/tasks.py @@ -8,8 +8,7 @@ from django.core.cache import cache from apps.alerts.models.alert_group_counter import ConcurrentUpdateError from apps.alerts.tasks import resolve_alert_group_by_source_if_needed -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import SlackAPIException +from apps.slack.client import SlackAPIException, SlackClientWithErrorHandling from common.custom_celery_tasks import shared_dedicated_queue_retry_task from common.custom_celery_tasks.create_alert_base_task import CreateAlertBaseTask @@ -159,8 +158,6 @@ def notify_about_integration_ratelimit_in_slack(organization_id, text, **kwargs) if slack_team_identity is not None: try: sc = SlackClientWithErrorHandling(slack_team_identity.bot_access_token) - sc.api_call( - "chat.postMessage", channel=organization.general_log_channel_id, text=text, team=slack_team_identity - ) + sc.chat_postMessage(channel=organization.general_log_channel_id, text=text, team=slack_team_identity) except SlackAPIException as e: logger.warning(f"Slack exception {e} while sending message for organization {organization_id}") diff --git a/engine/apps/public_api/helpers.py b/engine/apps/public_api/helpers.py index 587445cb..dec78ec6 100644 --- a/engine/apps/public_api/helpers.py +++ b/engine/apps/public_api/helpers.py @@ -1,15 +1,12 @@ from apps.public_api.constants import VALID_DATE_FOR_DELETE_INCIDENT -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import SlackAPITokenException +from apps.slack.client import SlackAPITokenException, SlackClientWithErrorHandling def team_has_slack_token_for_deleting(alert_group): if alert_group.slack_message and alert_group.slack_message.slack_team_identity: sc = SlackClientWithErrorHandling(alert_group.slack_message.slack_team_identity.bot_access_token) try: - sc.api_call( - "auth.test", - ) + sc.auth_test() except SlackAPITokenException: return False return True diff --git a/engine/apps/schedules/tests/tasks/shift_swaps/test_slack_followups.py b/engine/apps/schedules/tests/tasks/shift_swaps/test_slack_followups.py index 78ae1021..a39cf6f7 100644 --- a/engine/apps/schedules/tests/tasks/shift_swaps/test_slack_followups.py +++ b/engine/apps/schedules/tests/tasks/shift_swaps/test_slack_followups.py @@ -154,14 +154,13 @@ def test_followup_offsets(): assert ShiftSwapRequest.FOLLOWUP_OFFSETS[idx] > FOLLOWUP_WINDOW -@patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") +@patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") @pytest.mark.django_db -def test_send_shift_swap_request_followup(mock_slack_api_call, shift_swap_request_test_setup): +def test_send_shift_swap_request_followup(mock_slack_chat_post_message, shift_swap_request_test_setup): shift_swap_request = shift_swap_request_test_setup() send_shift_swap_request_slack_followup(shift_swap_request.pk) - mock_slack_api_call.assert_called_once_with( - "chat.postMessage", + mock_slack_chat_post_message.assert_called_once_with( channel=shift_swap_request.slack_message.channel_id, thread_ts=shift_swap_request.slack_message.slack_id, reply_broadcast=True, diff --git a/engine/apps/schedules/tests/test_notify_about_empty_shifts_in_schedule.py b/engine/apps/schedules/tests/test_notify_about_empty_shifts_in_schedule.py index c4680715..69178698 100644 --- a/engine/apps/schedules/tests/test_notify_about_empty_shifts_in_schedule.py +++ b/engine/apps/schedules/tests/test_notify_about_empty_shifts_in_schedule.py @@ -49,7 +49,7 @@ def test_no_empty_shifts_no_triggering_notification( empty_shifts_report_sent_at = schedule.empty_shifts_report_sent_at - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_about_empty_shifts_in_schedule(schedule.pk) assert not mock_slack_api_call.called @@ -97,7 +97,7 @@ def test_empty_shifts_trigger_notification( empty_shifts_report_sent_at = schedule.empty_shifts_report_sent_at - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_about_empty_shifts_in_schedule(schedule.pk) assert mock_slack_api_call.called @@ -160,7 +160,7 @@ def test_empty_non_empty_shifts_trigger_notification( empty_shifts_report_sent_at = schedule.empty_shifts_report_sent_at - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_about_empty_shifts_in_schedule(schedule.pk) assert mock_slack_api_call.called diff --git a/engine/apps/schedules/tests/test_notify_about_gaps_in_schedule.py b/engine/apps/schedules/tests/test_notify_about_gaps_in_schedule.py index 7f4946c3..432b59e0 100644 --- a/engine/apps/schedules/tests/test_notify_about_gaps_in_schedule.py +++ b/engine/apps/schedules/tests/test_notify_about_gaps_in_schedule.py @@ -48,7 +48,7 @@ def test_no_gaps_no_triggering_notification( gaps_report_sent_at = schedule.gaps_report_sent_at - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_about_gaps_in_schedule(schedule.pk) assert not mock_slack_api_call.called @@ -113,7 +113,7 @@ def test_gaps_in_the_past_no_triggering_notification( gaps_report_sent_at = schedule.gaps_report_sent_at - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_about_gaps_in_schedule(schedule.pk) assert not mock_slack_api_call.called @@ -165,7 +165,7 @@ def test_gaps_now_trigger_notification( assert schedule.has_gaps is False - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_about_gaps_in_schedule(schedule.pk) assert mock_slack_api_call.called @@ -219,7 +219,7 @@ def test_gaps_near_future_trigger_notification( assert schedule.has_gaps is False - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_about_gaps_in_schedule(schedule.pk) assert mock_slack_api_call.called @@ -271,7 +271,7 @@ def test_gaps_later_than_7_days_no_triggering_notification( gaps_report_sent_at = schedule.gaps_report_sent_at - with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call: + with patch("apps.slack.client.SlackClientWithErrorHandling.chat_postMessage") as mock_slack_api_call: notify_about_gaps_in_schedule(schedule.pk) assert not mock_slack_api_call.called diff --git a/engine/apps/slack/alert_group_slack_service.py b/engine/apps/slack/alert_group_slack_service.py index 78f79454..d779f172 100644 --- a/engine/apps/slack/alert_group_slack_service.py +++ b/engine/apps/slack/alert_group_slack_service.py @@ -1,14 +1,14 @@ import logging import typing -from apps.slack.constants import SLACK_RATE_LIMIT_DELAY -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import ( +from apps.slack.client import ( SlackAPIChannelArchivedException, SlackAPIException, SlackAPIRateLimitException, SlackAPITokenException, + SlackClientWithErrorHandling, ) +from apps.slack.constants import SLACK_RATE_LIMIT_DELAY if typing.TYPE_CHECKING: from apps.alerts.models import AlertGroup @@ -36,8 +36,7 @@ class AlertGroupSlackService: logger.info(f"Update message for alert_group {alert_group.pk}") try: - self._slack_client.api_call( - "chat.update", + self._slack_client.chat_update( channel=alert_group.slack_message.channel_id, ts=alert_group.slack_message.slack_id, attachments=alert_group.render_slack_attachments(), @@ -77,8 +76,7 @@ class AlertGroupSlackService: return try: - result = self._slack_client.api_call( - "chat.postMessage", + result = self._slack_client.chat_postMessage( channel=alert_group.slack_message.channel_id, text=text, attachments=attachments, diff --git a/engine/apps/slack/slack_client/slack_client.py b/engine/apps/slack/client.py similarity index 62% rename from engine/apps/slack/slack_client/slack_client.py rename to engine/apps/slack/client.py index a928c0d6..e3f59d26 100644 --- a/engine/apps/slack/slack_client/slack_client.py +++ b/engine/apps/slack/client.py @@ -2,44 +2,42 @@ import logging from typing import Optional, Tuple from django.utils import timezone -from slackclient import SlackClient -from slackclient.exceptions import TokenRefreshError +from slack_sdk.errors import SlackApiError +from slack_sdk.web import WebClient from apps.slack.constants import SLACK_RATE_LIMIT_DELAY -from .exceptions import ( - SlackAPIChannelArchivedException, - SlackAPIException, - SlackAPIRateLimitException, - SlackAPITokenException, -) -from .slack_client_server import SlackClientServer - logger = logging.getLogger(__name__) -class SlackClientWithErrorHandling(SlackClient): - def __init__(self, token=None, **kwargs): +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): """ - This method is rewritten because we want to use custom server SlackClientServer for SlackClient + `paginated_key` represents a key from the response which is paginated. For example "users" or "channels" """ - super().__init__(token=token, **kwargs) + api_method = getattr(self, method) - proxies = kwargs.get("proxies") - - if self.refresh_token: - if callable(self.token_update_callback): - token = None - else: - raise TokenRefreshError("Token refresh callback function is required when using refresh token.") - # Slack app configs - self.server = SlackClientServer(token=token, connect=False, proxies=proxies) - - def paginated_api_call(self, *args, **kwargs): - # It's a key from response which is paginated. For example "users" or "channels" - listed_key = kwargs["paginated_key"] - - response = self.api_call(*args, **kwargs) + response = api_method(**kwargs) cumulative_response = response while ( @@ -48,25 +46,30 @@ class SlackClientWithErrorHandling(SlackClient): and response["response_metadata"]["next_cursor"] != "" ): kwargs["cursor"] = response["response_metadata"]["next_cursor"] - response = self.api_call(*args, **kwargs) - cumulative_response[listed_key] += response[listed_key] + response = api_method(**kwargs) + cumulative_response[paginated_key] += response[paginated_key] return cumulative_response - def paginated_api_call_with_ratelimit(self, *args, **kwargs) -> Tuple[dict, Optional[str], bool]: + def paginated_api_call_with_ratelimit( + self, method: str, paginated_key: str, **kwargs + ) -> Tuple[dict, Optional[str], bool]: """ - This method do paginated api call and handle slack rate limit error in order to return collected data and have - 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 + 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" """ - # It's a key from response which is paginated. For example "users" or "channels" - listed_key = kwargs["paginated_key"] + api_method = getattr(self, method) + cumulative_response = {} - cursor = kwargs.get("cursor") + cursor = kwargs["cursor"] rate_limited = False try: - response = self.api_call(*args, **kwargs) + response = api_method(**kwargs) cumulative_response = response cursor = response["response_metadata"]["next_cursor"] @@ -77,8 +80,8 @@ class SlackClientWithErrorHandling(SlackClient): ): next_cursor = response["response_metadata"]["next_cursor"] kwargs["cursor"] = next_cursor - response = self.api_call(*args, **kwargs) - cumulative_response[listed_key] += response[listed_key] + response = api_method(**kwargs) + cumulative_response[paginated_key] += response[paginated_key] cursor = next_cursor except SlackAPIRateLimitException: @@ -87,7 +90,10 @@ class SlackClientWithErrorHandling(SlackClient): return cumulative_response, cursor, rate_limited def api_call(self, *args, **kwargs): - response = super(SlackClientWithErrorHandling, self).api_call(*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( diff --git a/engine/apps/slack/models/slack_message.py b/engine/apps/slack/models/slack_message.py index 3c4fe016..adec5272 100644 --- a/engine/apps/slack/models/slack_message.py +++ b/engine/apps/slack/models/slack_message.py @@ -5,11 +5,11 @@ import uuid from django.db import models -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import ( +from apps.slack.client import ( SlackAPIChannelArchivedException, SlackAPIException, SlackAPITokenException, + SlackClientWithErrorHandling, ) if typing.TYPE_CHECKING: @@ -83,11 +83,7 @@ class SlackMessage(models.Model): sc = SlackClientWithErrorHandling(self.slack_team_identity.bot_access_token) result = None try: - result = sc.api_call( - "chat.getPermalink", - channel=self.channel_id, - message_ts=self.slack_id, - ) + result = sc.chat_getPermalink(channel=self.channel_id, message_ts=self.slack_id) except SlackAPIException as e: if e.response["error"] == "message_not_found": return "https://slack.com/resources/using-slack/page/404" @@ -143,8 +139,7 @@ class SlackMessage(models.Model): channel_id = slack_message.channel_id try: - result = sc.api_call( - "chat.postMessage", + result = sc.chat_postMessage( channel=channel_id, text=text, blocks=blocks, @@ -190,7 +185,7 @@ class SlackMessage(models.Model): if slack_user_identity: channel_members = [] try: - channel_members = sc.api_call("conversations.members", channel=channel_id)["members"] + channel_members = sc.conversations_members(channel=channel_id)["members"] except SlackAPIException as e: if e.response["error"] == "fetch_members_failed": logger.warning( diff --git a/engine/apps/slack/models/slack_team_identity.py b/engine/apps/slack/models/slack_team_identity.py index faaf6331..c80cd456 100644 --- a/engine/apps/slack/models/slack_team_identity.py +++ b/engine/apps/slack/models/slack_team_identity.py @@ -5,9 +5,8 @@ from django.db import models from django.db.models import JSONField from apps.api.permissions import RBACPermission +from apps.slack.client import SlackAPIException, SlackAPITokenException, SlackClientWithErrorHandling from apps.slack.constants import SLACK_INVALID_AUTH_RESPONSE, SLACK_WRONG_TEAM_NAMES -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import SlackAPIException, SlackAPITokenException from apps.user_management.models.user import User from common.insight_log.chatops_insight_logs import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log @@ -87,7 +86,7 @@ class SlackTeamIdentity(models.Model): def bot_id(self): if self.cached_bot_id is None: sc = SlackClientWithErrorHandling(self.bot_access_token) - auth = sc.api_call("auth.test") + auth = sc.auth_test() self.cached_bot_id = auth.get("bot_id") self.save(update_fields=["cached_bot_id"]) return self.cached_bot_id @@ -99,7 +98,7 @@ class SlackTeamIdentity(models.Model): next_cursor = None members = [] while next_cursor != "" or next_cursor is None: - result = sc.api_call("users.list", cursor=next_cursor, team=self) + result = sc.users_list(cursor=next_cursor, team=self) next_cursor = result["response_metadata"]["next_cursor"] members += result["members"] @@ -110,7 +109,7 @@ class SlackTeamIdentity(models.Model): if self.cached_name is None or self.cached_name in SLACK_WRONG_TEAM_NAMES: try: sc = SlackClientWithErrorHandling(self.bot_access_token) - result = sc.api_call("team.info") + result = sc.team_info() self.cached_name = result["team"]["name"] self.save() except SlackAPIException as e: @@ -125,7 +124,7 @@ class SlackTeamIdentity(models.Model): def app_id(self): if not self.cached_app_id: sc = SlackClientWithErrorHandling(self.bot_access_token) - result = sc.api_call("bots.info", bot=self.bot_id) + result = sc.bots_info(bot=self.bot_id) app_id = result["bot"]["app_id"] self.cached_app_id = app_id self.save(update_fields=["cached_app_id"]) @@ -140,10 +139,10 @@ class SlackTeamIdentity(models.Model): **User.build_permissions_query(RBACPermission.Permissions.CHATOPS_WRITE, organization), ) - def get_conversation_members(self, slack_client, channel_id): + def get_conversation_members(self, slack_client: SlackClientWithErrorHandling, channel_id: str): try: members = slack_client.paginated_api_call( - "conversations.members", channel=channel_id, paginated_key="members" + "conversations_members", paginated_key="members", channel=channel_id )["members"] except SlackAPITokenException as e: logger.warning( diff --git a/engine/apps/slack/models/slack_user_identity.py b/engine/apps/slack/models/slack_user_identity.py index 85e6f447..d016c7b9 100644 --- a/engine/apps/slack/models/slack_user_identity.py +++ b/engine/apps/slack/models/slack_user_identity.py @@ -4,10 +4,9 @@ import typing import requests from django.db import models +from apps.slack.client import SlackAPIException, SlackAPITokenException, SlackClientWithErrorHandling from apps.slack.constants import SLACK_BOT_ID from apps.slack.scenarios.notified_user_not_in_channel import NotifiedUserNotInChannelStep -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import SlackAPIException, SlackAPITokenException from apps.user_management.models import Organization, User if typing.TYPE_CHECKING: @@ -135,8 +134,7 @@ class SlackUserIdentity(models.Model): ] sc = SlackClientWithErrorHandling(self.slack_team_identity.bot_access_token) - return sc.api_call( - "chat.postMessage", + return sc.chat_postMessage( channel=self.im_channel_id, text="You are invited to look at an alert group!", blocks=blocks, @@ -158,11 +156,7 @@ class SlackUserIdentity(models.Model): if self.cached_slack_login is None or self.cached_slack_login == "slack_token_revoked_unable_to_cache_login": sc = SlackClientWithErrorHandling(self.slack_team_identity.bot_access_token) try: - result = sc.api_call( - "users.info", - user=self.slack_id, - team=self.slack_team_identity, - ) + result = sc.users_info(user=self.slack_id, team=self.slack_team_identity) self.cached_slack_login = result["user"]["name"] self.save() except SlackAPITokenException as e: @@ -187,11 +181,7 @@ class SlackUserIdentity(models.Model): if self.cached_timezone is None or self.cached_timezone == "None": sc = SlackClientWithErrorHandling(self.slack_team_identity.bot_access_token) try: - result = sc.api_call( - "users.info", - user=self.slack_id, - timeout=5, - ) + result = sc.users_info(user=self.slack_id) tz_from_slack = result["user"].get("tz", "UTC") if tz_from_slack == "None" or tz_from_slack is None: tz_from_slack = "UTC" @@ -210,7 +200,7 @@ class SlackUserIdentity(models.Model): if self.cached_im_channel_id is None: sc = SlackClientWithErrorHandling(self.slack_team_identity.bot_access_token) try: - result = sc.api_call("conversations.open", users=self.slack_id, return_im=True) + result = sc.conversations_open(users=self.slack_id, return_im=True) self.cached_im_channel_id = result["channel"]["id"] self.save() except SlackAPIException as e: @@ -225,11 +215,7 @@ class SlackUserIdentity(models.Model): sc = SlackClientWithErrorHandling(self.slack_team_identity.bot_access_token) logger.info("Update user profile info") try: - result = sc.api_call( - "users.info", - user=self.slack_id, - team=self.slack_team_identity, - ) + result = sc.users_info(user=self.slack_id, team=self.slack_team_identity) except SlackAPITokenException as e: logger.warning(f"Unable to get user info due token revoked or account inactive: {e}") result = None diff --git a/engine/apps/slack/models/slack_usergroup.py b/engine/apps/slack/models/slack_usergroup.py index 013cfe29..9af48d94 100644 --- a/engine/apps/slack/models/slack_usergroup.py +++ b/engine/apps/slack/models/slack_usergroup.py @@ -9,8 +9,7 @@ from django.db.models import JSONField from django.utils import timezone from apps.api.permissions import RBACPermission -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import SlackAPIException +from apps.slack.client import SlackAPIException, SlackClientWithErrorHandling from apps.user_management.models.user import User from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length @@ -69,10 +68,10 @@ class SlackUserGroup(models.Model): @property def can_be_updated(self) -> bool: - sc = SlackClientWithErrorHandling(self.slack_team_identity.bot_access_token) + sc = SlackClientWithErrorHandling(self.slack_team_identity.bot_access_token, timeout=5) try: - sc.api_call("usergroups.update", usergroup=self.slack_id, timeout=5) + sc.usergroups_update(usergroup=self.slack_id) return True except (SlackAPIException, requests.exceptions.Timeout): return False @@ -112,11 +111,7 @@ class SlackUserGroup(models.Model): def update_members(self, slack_ids): sc = SlackClientWithErrorHandling(self.slack_team_identity.bot_access_token) - sc.api_call( - "usergroups.users.update", - usergroup=self.slack_id, - users=slack_ids, - ) + sc.usergroups_users_update(usergroup=self.slack_id, users=slack_ids) self.members = slack_ids self.save(update_fields=("members",)) @@ -132,18 +127,14 @@ class SlackUserGroup(models.Model): sc = SlackClientWithErrorHandling(slack_team_identity.bot_access_token) bot_access_token_accepted = True try: - usergroups_list = sc.api_call( - "usergroups.list", - ) + usergroups_list = sc.usergroups_list() except SlackAPIException as e: if e.response["error"] == "not_allowed_token_type": # Trying same request with access token. It is required due to migration to granular permissions # and can be removed after clients reinstall their bots try: sc_with_access_token = SlackClientWithErrorHandling(slack_team_identity.access_token) - usergroups_list = sc_with_access_token.api_call( - "usergroups.list", - ) + usergroups_list = sc_with_access_token.usergroups_list() bot_access_token_accepted = False except SlackAPIException as err: if err.response["error"] == "missing_scope": @@ -159,16 +150,10 @@ class SlackUserGroup(models.Model): if usergroup["id"] == slack_id: try: if bot_access_token_accepted: - usergroups_users = sc.api_call( - "usergroups.users.list", - usergroup=usergroup["id"], - ) + usergroups_users = sc.usergroups_users_list(usergroup=usergroup["id"]) else: sc_with_access_token = SlackClientWithErrorHandling(slack_team_identity.access_token) - usergroups_users = sc_with_access_token.api_call( - "usergroups.users.list", - usergroup=usergroup["id"], - ) + usergroups_users = sc_with_access_token.usergroups_users_list(usergroup=usergroup["id"]) except SlackAPIException as e: if e.response["error"] == "no_such_subteam": logger.info("User group does not exist") diff --git a/engine/apps/slack/scenarios/alertgroup_appearance.py b/engine/apps/slack/scenarios/alertgroup_appearance.py index af1899a1..7cad3553 100644 --- a/engine/apps/slack/scenarios/alertgroup_appearance.py +++ b/engine/apps/slack/scenarios/alertgroup_appearance.py @@ -66,11 +66,7 @@ class OpenAlertAppearanceDialogStep(AlertGroupActionsMixin, scenario_step.Scenar "private_metadata": json.dumps(private_metadata), } - self._slack_client.api_call( - "views.open", - trigger_id=payload["trigger_id"], - view=view, - ) + self._slack_client.views_open(trigger_id=payload["trigger_id"], view=view) class UpdateAppearanceStep(scenario_step.ScenarioStep): @@ -90,8 +86,7 @@ class UpdateAppearanceStep(scenario_step.ScenarioStep): attachments = alert_group.render_slack_attachments() blocks = alert_group.render_slack_blocks() - self._slack_client.api_call( - "chat.update", + self._slack_client.chat_update( channel=alert_group.slack_message.channel_id, ts=alert_group.slack_message.slack_id, attachments=attachments, diff --git a/engine/apps/slack/scenarios/distribute_alerts.py b/engine/apps/slack/scenarios/distribute_alerts.py index 40cbebe6..fd5faf2d 100644 --- a/engine/apps/slack/scenarios/distribute_alerts.py +++ b/engine/apps/slack/scenarios/distribute_alerts.py @@ -15,16 +15,16 @@ from apps.alerts.models import Alert, AlertGroup, AlertGroupLogRecord, AlertRece from apps.alerts.tasks import custom_button_result from apps.alerts.utils import render_curl_command from apps.api.permissions import RBACPermission -from apps.slack.constants import CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME, SLACK_RATE_LIMIT_DELAY -from apps.slack.scenarios import scenario_step -from apps.slack.scenarios.slack_renderer import AlertGroupLogSlackRenderer -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import ( +from apps.slack.client import ( SlackAPIChannelArchivedException, SlackAPIException, SlackAPIRateLimitException, SlackAPITokenException, + SlackClientWithErrorHandling, ) +from apps.slack.constants import CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME, SLACK_RATE_LIMIT_DELAY +from apps.slack.scenarios import scenario_step +from apps.slack.scenarios.slack_renderer import AlertGroupLogSlackRenderer from apps.slack.slack_formatter import SlackFormatter from apps.slack.tasks import ( post_or_update_log_report_message_task, @@ -133,9 +133,7 @@ class AlertShootingStep(scenario_step.ScenarioStep): return try: - result = self._slack_client.api_call( - "chat.postMessage", channel=channel_id, attachments=attachments, blocks=blocks - ) + result = self._slack_client.chat_postMessage(channel=channel_id, attachments=attachments, blocks=blocks) alert_group.slack_messages.create( slack_id=result["ts"], @@ -147,8 +145,7 @@ class AlertShootingStep(scenario_step.ScenarioStep): # If alert was made out of a message: if alert_group.channel.integration == AlertReceiveChannel.INTEGRATION_SLACK_CHANNEL: channel = json.loads(alert.integration_unique_data)["channel"] - result = self._slack_client.api_call( - "chat.postMessage", + result = self._slack_client.chat_postMessage( channel=channel, thread_ts=json.loads(alert.integration_unique_data)["ts"], text=":rocket: <{}|Incident registered!>".format(alert_group.slack_message.permalink), @@ -200,8 +197,7 @@ class AlertShootingStep(scenario_step.ScenarioStep): blocks: Block.AnyBlocks = [] text = "Escalations are silenced due to Debug mode" blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": text}}) - self._slack_client.api_call( - "chat.postMessage", + self._slack_client.chat_postMessage( channel=channel_id, text=text, attachments=[], @@ -394,11 +390,7 @@ class SelectAttachGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep): }, } ) - self._slack_client.api_call( - "views.open", - trigger_id=payload["trigger_id"], - view=view, - ) + self._slack_client.views_open(trigger_id=payload["trigger_id"], view=view) def get_select_incidents_blocks(self, alert_group: AlertGroup) -> Block.AnyBlocks: collected_options: typing.List[CompositionObjectOption] = [] @@ -482,8 +474,7 @@ class AttachGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep): slack_user_identity = log_record.author.slack_user_identity if slack_user_identity: - self._slack_client.api_call( - "chat.postEphemeral", + self._slack_client.chat_postEphemeral( user=slack_user_identity.slack_id, channel=alert_group.slack_message.channel_id, text="{}{}".format(ephemeral_text[:1].upper(), ephemeral_text[1:]), @@ -757,8 +748,7 @@ class UnAcknowledgeGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep) ) if alert_group.slack_message.ack_reminder_message_ts: try: - self._slack_client.api_call( - "chat.update", + self._slack_client.chat_update( channel=channel_id, ts=alert_group.slack_message.ack_reminder_message_ts, text=text, @@ -803,17 +793,11 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep): if self.user == alert_group.acknowledged_by_user: user_verbal = alert_group.acknowledged_by_user.get_username_with_slack_verbal() text = f"{user_verbal} confirmed that the Alert Group is still acknowledged." - self._slack_client.api_call( - "chat.update", - channel=channel, - ts=message_ts, - text=text, - ) + self._slack_client.chat_update(channel=channel, ts=message_ts, text=text) alert_group.acknowledged_by_confirmed = datetime.utcnow() alert_group.save(update_fields=["acknowledged_by_confirmed"]) else: - self._slack_client.api_call( - "chat.postEphemeral", + self._slack_client.chat_postEphemeral( channel=channel, user=slack_user_identity.slack_id, text="This Alert Group is acknowledged by another user. Acknowledge it yourself first.", @@ -821,22 +805,12 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep): elif alert_group.acknowledged_by == AlertGroup.SOURCE: user_verbal = self.user.get_username_with_slack_verbal() text = f"{user_verbal} confirmed that the Alert Group is still acknowledged." - self._slack_client.api_call( - "chat.update", - channel=channel, - ts=message_ts, - text=text, - ) + self._slack_client.chat_update(channel=channel, ts=message_ts, text=text) alert_group.acknowledged_by_confirmed = datetime.utcnow() alert_group.save(update_fields=["acknowledged_by_confirmed"]) else: - self._slack_client.api_call( - "chat.delete", - channel=channel, - ts=message_ts, - ) - self._slack_client.api_call( - "chat.postEphemeral", + self._slack_client.chat_delete(channel=channel, ts=message_ts) + self._slack_client.chat_postEphemeral( channel=channel, user=slack_user_identity.slack_id, text="This Alert Group is already unacknowledged.", @@ -877,8 +851,7 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep): } ] try: - response = self._slack_client.api_call( - "chat.postMessage", + response = self._slack_client.chat_postMessage( channel=channel_id, text=text, attachments=attachments, @@ -944,11 +917,7 @@ class DeleteGroupStep(scenario_step.ScenarioStep): for message_ts in bot_messages_ts: try: - self._slack_client.api_call( - "chat.delete", - channel=channel_id, - ts=message_ts, - ) + self._slack_client.chat_delete(channel=channel_id, ts=message_ts) except SlackAPITokenException as e: logger.error( f"Unable to delete messages in slack. Message ts: {message_ts}" @@ -981,11 +950,7 @@ class DeleteGroupStep(scenario_step.ScenarioStep): sc_with_access_token = SlackClientWithErrorHandling( self.slack_team_identity.access_token ) # used access_token instead of bot_access_token - sc_with_access_token.api_call( - "chat.delete", - channel=channel_id, - ts=message_ts, - ) + sc_with_access_token.chat_delete(channel=channel_id, ts=message_ts) else: raise e @@ -994,12 +959,7 @@ class DeleteGroupStep(scenario_step.ScenarioStep): message.added_to_resolution_note = False message.save(update_fields=["added_to_resolution_note"]) try: - self._slack_client.api_call( - "reactions.remove", - channel=message.slack_channel_id, - name="memo", - timestamp=message.ts, - ) + self._slack_client.reactions_remove(channel=message.slack_channel_id, name="memo", timestamp=message.ts) except SlackAPITokenException as e: logger.warning( f"Unable to delete resolution note reaction in slack. " @@ -1030,8 +990,8 @@ class UpdateLogReportMessageStep(scenario_step.ScenarioStep): if slack_log_message is None: logger.debug(f"Start posting new log message for alert_group {alert_group.pk}") try: - result = self._slack_client.api_call( - "chat.postMessage", channel=slack_message.channel_id, thread_ts=slack_message.slack_id, text=text + result = self._slack_client.chat_postMessage( + channel=slack_message.channel_id, thread_ts=slack_message.slack_id, text=text ) except SlackAPITokenException as e: print(e) @@ -1090,8 +1050,7 @@ class UpdateLogReportMessageStep(scenario_step.ScenarioStep): f"Update log message for alert_group {alert_group.pk}, slack_log_message {slack_log_message.pk}" ) try: - self._slack_client.api_call( - "chat.update", + self._slack_client.chat_update( channel=slack_message.channel_id, text="Alert Group log", ts=slack_log_message.slack_id, diff --git a/engine/apps/slack/scenarios/invited_to_channel.py b/engine/apps/slack/scenarios/invited_to_channel.py index e5119bb9..1ec5a7a1 100644 --- a/engine/apps/slack/scenarios/invited_to_channel.py +++ b/engine/apps/slack/scenarios/invited_to_channel.py @@ -3,8 +3,8 @@ import typing from django.utils import timezone +from apps.slack.client import SlackClientWithErrorHandling from apps.slack.scenarios import scenario_step -from apps.slack.slack_client import SlackClientWithErrorHandling from apps.slack.types import EventPayload, EventType, PayloadType, ScenarioRoute if typing.TYPE_CHECKING: @@ -24,7 +24,7 @@ class InvitedToChannelStep(scenario_step.ScenarioStep): if payload["event"]["user"] == slack_team_identity.bot_user_id: channel_id = payload["event"]["channel"] slack_client = SlackClientWithErrorHandling(slack_team_identity.bot_access_token) - channel = slack_client.api_call("conversations.info", channel=channel_id)["channel"] + channel = slack_client.conversations_info(channel=channel_id)["channel"] slack_team_identity.cached_channels.update_or_create( slack_id=channel["id"], diff --git a/engine/apps/slack/scenarios/manage_responders.py b/engine/apps/slack/scenarios/manage_responders.py index 94dee927..b6c16a69 100644 --- a/engine/apps/slack/scenarios/manage_responders.py +++ b/engine/apps/slack/scenarios/manage_responders.py @@ -46,11 +46,7 @@ class StartManageResponders(AlertGroupActionsMixin, scenario_step.ScenarioStep): return view = render_dialog(alert_group) - self._slack_client.api_call( - "views.open", - trigger_id=payload["trigger_id"], - view=view, - ) + self._slack_client.views_open(trigger_id=payload["trigger_id"], view=view) class ManageRespondersUserChange(scenario_step.ScenarioStep): @@ -77,11 +73,7 @@ class ManageRespondersUserChange(scenario_step.ScenarioStep): ManageRespondersConfirmUserChange.routing_uid(), json.dumps({USER_DATA_KEY: selected_user.id, ALERT_GROUP_DATA_KEY: alert_group.pk}), ) - self._slack_client.api_call( - "views.push", - trigger_id=payload["trigger_id"], - view=view, - ) + self._slack_client.views_push(trigger_id=payload["trigger_id"], view=view) else: try: # no warnings, proceed with paging @@ -96,8 +88,7 @@ class ManageRespondersUserChange(scenario_step.ScenarioStep): except DirectPagingAlertGroupResolvedError: view = render_dialog(alert_group, alert_group_resolved_warning=True) - self._slack_client.api_call( - "views.update", + self._slack_client.views_update( trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["id"], @@ -129,8 +120,7 @@ class ManageRespondersConfirmUserChange(scenario_step.ScenarioStep): except DirectPagingAlertGroupResolvedError: view = render_dialog(alert_group, alert_group_resolved_warning=True) - self._slack_client.api_call( - "views.update", + self._slack_client.views_update( trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["previous_view_id"], @@ -162,8 +152,7 @@ class ManageRespondersScheduleChange(scenario_step.ScenarioStep): except DirectPagingAlertGroupResolvedError: view = render_dialog(alert_group, alert_group_resolved_warning=True) - self._slack_client.api_call( - "views.update", + self._slack_client.views_update( trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["id"], @@ -185,8 +174,7 @@ class ManageRespondersRemoveUser(scenario_step.ScenarioStep): unpage_user(alert_group, selected_user, from_user) view = render_dialog(alert_group) - self._slack_client.api_call( - "views.update", + self._slack_client.views_update( trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["id"], diff --git a/engine/apps/slack/scenarios/manual_incident.py b/engine/apps/slack/scenarios/manual_incident.py index fa78b0d5..eb7af94e 100644 --- a/engine/apps/slack/scenarios/manual_incident.py +++ b/engine/apps/slack/scenarios/manual_incident.py @@ -5,9 +5,9 @@ from uuid import uuid4 from django.conf import settings from apps.alerts.models import AlertReceiveChannel, ChannelFilter +from apps.slack.client import SlackAPIException from apps.slack.constants import DIVIDER from apps.slack.scenarios import scenario_step -from apps.slack.slack_client.exceptions import SlackAPIException from apps.slack.types import ( Block, BlockActionType, @@ -68,11 +68,7 @@ class StartCreateIncidentFromSlashCommand(scenario_step.ScenarioStep): FinishCreateIncidentFromSlashCommand.routing_uid(), blocks, json.dumps(private_metadata) ) - self._slack_client.api_call( - "views.open", - trigger_id=payload["trigger_id"], - view=view, - ) + self._slack_client.views_open(trigger_id=payload["trigger_id"], view=view) class FinishCreateIncidentFromSlashCommand(scenario_step.ScenarioStep): @@ -115,16 +111,14 @@ class FinishCreateIncidentFromSlashCommand(scenario_step.ScenarioStep): author_username = slack_user_identity.slack_verbal try: - self._slack_client.api_call( - "chat.postEphemeral", + self._slack_client.chat_postEphemeral( channel=channel_id, user=slack_user_identity.slack_id, text=":white_check_mark: Alert *{}* successfully submitted".format(title), ) except SlackAPIException as e: if e.response["error"] == "channel_not_found": - self._slack_client.api_call( - "chat.postEphemeral", + self._slack_client.chat_postEphemeral( channel=slack_user_identity.im_channel_id, user=slack_user_identity.slack_id, text=":white_check_mark: Alert *{}* successfully submitted".format(title), @@ -201,8 +195,7 @@ class OnOrgChange(scenario_step.ScenarioStep): if with_title_and_message_inputs: blocks.extend([_get_title_input(payload), _get_message_input(payload)]) view = _get_manual_incident_form_view(submit_routing_uid, blocks, json.dumps(new_private_metadata)) - self._slack_client.api_call( - "views.update", + self._slack_client.views_update( trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["id"], @@ -249,8 +242,7 @@ class OnTeamChange(scenario_step.ScenarioStep): if with_title_and_message_inputs: blocks.extend([_get_title_input(payload), _get_message_input(payload)]) view = _get_manual_incident_form_view(submit_routing_uid, blocks, json.dumps(new_private_metadata)) - self._slack_client.api_call( - "views.update", + self._slack_client.views_update( trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["id"], diff --git a/engine/apps/slack/scenarios/notification_delivery.py b/engine/apps/slack/scenarios/notification_delivery.py index 141651e5..f711b3c6 100644 --- a/engine/apps/slack/scenarios/notification_delivery.py +++ b/engine/apps/slack/scenarios/notification_delivery.py @@ -1,7 +1,7 @@ import typing +from apps.slack.client import SlackAPIException, SlackAPITokenException from apps.slack.scenarios import scenario_step -from apps.slack.slack_client.exceptions import SlackAPIException, SlackAPITokenException from apps.slack.types import Block if typing.TYPE_CHECKING: @@ -72,8 +72,7 @@ class NotificationDeliveryStep(scenario_step.ScenarioStep): ] try: # TODO: slack-onprem, check exceptions - self._slack_client.api_call( - "chat.postMessage", + self._slack_client.chat_postMessage( channel=channel, text=text, blocks=blocks, diff --git a/engine/apps/slack/scenarios/paging.py b/engine/apps/slack/scenarios/paging.py index 9fcff1c6..499d0ecd 100644 --- a/engine/apps/slack/scenarios/paging.py +++ b/engine/apps/slack/scenarios/paging.py @@ -15,9 +15,9 @@ from apps.alerts.paging import ( check_user_availability, direct_paging, ) +from apps.slack.client import SlackAPIException from apps.slack.constants import DIVIDER, PRIVATE_METADATA_MAX_LENGTH from apps.slack.scenarios import scenario_step -from apps.slack.slack_client.exceptions import SlackAPIException from apps.slack.types import ( Block, BlockActionType, @@ -147,8 +147,7 @@ class StartDirectPaging(scenario_step.ScenarioStep): } initial_payload = {"view": {"private_metadata": json.dumps(private_metadata)}} view = render_dialog(slack_user_identity, slack_team_identity, initial_payload, initial=True) - self._slack_client.api_call( - "views.open", + self._slack_client.views_open( trigger_id=payload["trigger_id"], view=view, ) @@ -203,16 +202,14 @@ class FinishDirectPaging(scenario_step.ScenarioStep): text = ":white_check_mark: Alert group *{}* created: {}".format(title, alert_group.web_link) try: - self._slack_client.api_call( - "chat.postEphemeral", + self._slack_client.chat_postEphemeral( channel=channel_id, user=slack_user_identity.slack_id, text=text, ) except SlackAPIException as e: if e.response["error"] == "channel_not_found": - self._slack_client.api_call( - "chat.postEphemeral", + self._slack_client.chat_postEphemeral( channel=slack_user_identity.im_channel_id, user=slack_user_identity.slack_id, text=text, @@ -235,8 +232,7 @@ class OnPagingOrgChange(scenario_step.ScenarioStep): ) -> None: updated_payload = reset_items(payload) view = render_dialog(slack_user_identity, slack_team_identity, updated_payload) - self._slack_client.api_call( - "views.update", + self._slack_client.views_update( trigger_id=updated_payload["trigger_id"], view=view, view_id=updated_payload["view"]["id"], @@ -253,8 +249,7 @@ class OnPagingTeamChange(scenario_step.ScenarioStep): payload: EventPayload, ) -> None: view = render_dialog(slack_user_identity, slack_team_identity, payload) - self._slack_client.api_call( - "views.update", + self._slack_client.views_update( trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["id"], @@ -290,11 +285,7 @@ class OnPagingUserChange(scenario_step.ScenarioStep): if availability_warnings: # display warnings and require additional confirmation view = _display_availability_warnings(payload, availability_warnings, selected_organization, selected_user) - self._slack_client.api_call( - "views.push", - trigger_id=payload["trigger_id"], - view=view, - ) + self._slack_client.views_push(trigger_id=payload["trigger_id"], view=view) else: # user is available to be paged error_msg = None @@ -304,8 +295,7 @@ class OnPagingUserChange(scenario_step.ScenarioStep): updated_payload = payload error_msg = "Cannot add user, maximum responders exceeded" view = render_dialog(slack_user_identity, slack_team_identity, updated_payload, error_msg=error_msg) - self._slack_client.api_call( - "views.update", + self._slack_client.views_update( trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["id"], @@ -338,12 +328,7 @@ class OnPagingItemActionChange(scenario_step.ScenarioStep): error_msg = "Cannot update policy, maximum responders exceeded" view = render_dialog(slack_user_identity, slack_team_identity, updated_payload, error_msg=error_msg) - self._slack_client.api_call( - "views.update", - trigger_id=payload["trigger_id"], - view=view, - view_id=payload["view"]["id"], - ) + self._slack_client.views_update(trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["id"]) class OnPagingConfirmUserChange(scenario_step.ScenarioStep): @@ -380,8 +365,7 @@ class OnPagingConfirmUserChange(scenario_step.ScenarioStep): updated_payload = payload error_msg = "Cannot add user, maximum responders exceeded" view = render_dialog(slack_user_identity, slack_team_identity, updated_payload, error_msg=error_msg) - self._slack_client.api_call( - "views.update", + self._slack_client.views_update( trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["previous_view_id"], @@ -412,12 +396,7 @@ class OnPagingScheduleChange(scenario_step.ScenarioStep): updated_payload = payload error_msg = "Cannot add schedule, maximum responders exceeded" view = render_dialog(slack_user_identity, slack_team_identity, updated_payload, error_msg=error_msg) - self._slack_client.api_call( - "views.update", - trigger_id=payload["trigger_id"], - view=view, - view_id=payload["view"]["id"], - ) + self._slack_client.views_update(trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["id"]) # slack view/blocks rendering helpers diff --git a/engine/apps/slack/scenarios/resolution_note.py b/engine/apps/slack/scenarios/resolution_note.py index 980385a9..f50535c4 100644 --- a/engine/apps/slack/scenarios/resolution_note.py +++ b/engine/apps/slack/scenarios/resolution_note.py @@ -6,9 +6,9 @@ import typing from django.db.models import Q from apps.api.permissions import RBACPermission +from apps.slack.client import SlackAPIException from apps.slack.constants import DIVIDER from apps.slack.scenarios import scenario_step -from apps.slack.slack_client.exceptions import SlackAPIException from apps.slack.types import ( Block, BlockActionType, @@ -69,7 +69,7 @@ class AddToResolutionNoteStep(scenario_step.ScenarioStep): alert_group = slack_message.alert_group if not alert_group: self.open_warning_window(payload, warning_text) - print( + logger.exception( f"Exception: tried to add message from thread to Resolution Note: " f"Slack Team Identity pk: {self.slack_team_identity.pk}, " f"Slack Message id: {slack_message.slack_id}" @@ -80,11 +80,7 @@ class AddToResolutionNoteStep(scenario_step.ScenarioStep): message_ts = payload["message_ts"] thread_ts = payload["message"]["thread_ts"] - result = self._slack_client.api_call( - "chat.getPermalink", - channel=channel_id, - message_ts=message_ts, - ) + result = self._slack_client.chat_getPermalink(channel=channel_id, message_ts=message_ts) permalink = None if result["permalink"] is not None: permalink = result["permalink"] @@ -155,8 +151,7 @@ class AddToResolutionNoteStep(scenario_step.ScenarioStep): else: resolution_note.recreate() try: - self._slack_client.api_call( - "reactions.add", + self._slack_client.reactions_add( channel=channel_id, name="memo", timestamp=resolution_note_slack_message.ts, @@ -189,8 +184,7 @@ class UpdateResolutionNoteStep(scenario_step.ScenarioStep): resolution_note_slack_message.save(update_fields=["added_to_resolution_note"]) if resolution_note_slack_message.posted_by_bot: try: - self._slack_client.api_call( - "chat.delete", + self._slack_client.chat_delete( channel=resolution_note_slack_message.slack_channel_id, ts=resolution_note_slack_message.ts, ) @@ -240,8 +234,7 @@ class UpdateResolutionNoteStep(scenario_step.ScenarioStep): if resolution_note_slack_message is None: try: - result = self._slack_client.api_call( - "chat.postMessage", + result = self._slack_client.chat_postMessage( channel=alert_group_slack_message.channel_id, thread_ts=alert_group_slack_message.slack_id, text=resolution_note.text, @@ -270,8 +263,7 @@ class UpdateResolutionNoteStep(scenario_step.ScenarioStep): raise e else: message_ts = result["message"]["ts"] - result_permalink = self._slack_client.api_call( - "chat.getPermalink", + result_permalink = self._slack_client.chat_getPermalink( channel=alert_group_slack_message.channel_id, message_ts=message_ts, ) @@ -295,8 +287,7 @@ class UpdateResolutionNoteStep(scenario_step.ScenarioStep): resolution_note.save(update_fields=["resolution_note_slack_message"]) elif resolution_note_slack_message.posted_by_bot: try: - self._slack_client.api_call( - "chat.update", + self._slack_client.chat_update( channel=alert_group_slack_message.channel_id, ts=resolution_note_slack_message.ts, text=resolution_note_slack_message.text, @@ -345,25 +336,23 @@ class UpdateResolutionNoteStep(scenario_step.ScenarioStep): def add_resolution_note_reaction(self, slack_thread_message: "ResolutionNoteSlackMessage"): try: - self._slack_client.api_call( - "reactions.add", + self._slack_client.reactions_add( channel=slack_thread_message.slack_channel_id, name="memo", timestamp=slack_thread_message.ts, ) except SlackAPIException as e: - print(e) # TODO:770: log instead of print + logger.exception(e) def remove_resolution_note_reaction(self, slack_thread_message: "ResolutionNoteSlackMessage") -> None: try: - self._slack_client.api_call( - "reactions.remove", + self._slack_client.reactions_remove( channel=slack_thread_message.slack_channel_id, name="memo", timestamp=slack_thread_message.ts, ) except SlackAPIException as e: - print(e) + logger.exception(e) def get_resolution_note_blocks(self, resolution_note: "ResolutionNote") -> Block.AnyBlocks: blocks: Block.AnyBlocks = [] @@ -453,8 +442,7 @@ class ResolutionNoteModalStep(AlertGroupActionsMixin, scenario_step.ScenarioStep if "update" in resolution_note_window_action: try: - self._slack_client.api_call( - "views.update", + self._slack_client.views_update( trigger_id=payload["trigger_id"], view=view, view_id=payload["view"]["id"], @@ -470,11 +458,7 @@ class ResolutionNoteModalStep(AlertGroupActionsMixin, scenario_step.ScenarioStep else: raise else: - self._slack_client.api_call( - "views.open", - trigger_id=payload["trigger_id"], - view=view, - ) + self._slack_client.views_open(trigger_id=payload["trigger_id"], view=view) def get_resolution_notes_blocks( self, alert_group: "AlertGroup", resolution_note_window_action: str, action_resolve: bool diff --git a/engine/apps/slack/scenarios/scenario_step.py b/engine/apps/slack/scenarios/scenario_step.py index b474d1b2..a8a38c1a 100644 --- a/engine/apps/slack/scenarios/scenario_step.py +++ b/engine/apps/slack/scenarios/scenario_step.py @@ -3,7 +3,7 @@ import logging import typing from apps.slack.alert_group_slack_service import AlertGroupSlackService -from apps.slack.slack_client import SlackClientWithErrorHandling +from apps.slack.client import SlackClientWithErrorHandling if typing.TYPE_CHECKING: from apps.slack.models import SlackTeamIdentity, SlackUserIdentity @@ -77,8 +77,4 @@ class ScenarioStep(object): }, ], } - self._slack_client.api_call( - "views.open", - trigger_id=payload["trigger_id"], - view=view, - ) + self._slack_client.views_open(trigger_id=payload["trigger_id"], view=view) diff --git a/engine/apps/slack/scenarios/schedules.py b/engine/apps/slack/scenarios/schedules.py index b470fea2..c28e58cc 100644 --- a/engine/apps/slack/scenarios/schedules.py +++ b/engine/apps/slack/scenarios/schedules.py @@ -61,11 +61,7 @@ class EditScheduleShiftNotifyStep(scenario_step.ScenarioStep): "private_metadata": json.dumps(private_metadata), } - self._slack_client.api_call( - "views.open", - trigger_id=payload["trigger_id"], - view=view, - ) + self._slack_client.views_open(trigger_id=payload["trigger_id"], view=view) def set_selected_value(self, slack_user_identity: "SlackUserIdentity", payload: EventPayload) -> None: action = payload["actions"][0] diff --git a/engine/apps/slack/scenarios/shift_swap_requests.py b/engine/apps/slack/scenarios/shift_swap_requests.py index 18711b55..db131dc5 100644 --- a/engine/apps/slack/scenarios/shift_swap_requests.py +++ b/engine/apps/slack/scenarios/shift_swap_requests.py @@ -156,7 +156,7 @@ class BaseShiftSwapRequestStep(scenario_step.ScenarioStep): organization = self.organization blocks = self._generate_blocks(shift_swap_request) - result = self._slack_client.api_call("chat.postMessage", channel=channel_id, blocks=blocks) + result = self._slack_client.chat_postMessage(channel=channel_id, blocks=blocks) return SlackMessage.objects.create( slack_id=result["ts"], @@ -166,9 +166,7 @@ class BaseShiftSwapRequestStep(scenario_step.ScenarioStep): ) def update_message(self, shift_swap_request: "ShiftSwapRequest") -> None: - # TODO: better error handling here... - self._slack_client.api_call( - "chat.update", + self._slack_client.chat_update( channel=shift_swap_request.slack_channel_id, ts=shift_swap_request.slack_message.slack_id, blocks=self._generate_blocks(shift_swap_request), @@ -229,8 +227,7 @@ class ShiftSwapRequestFollowUp(scenario_step.ScenarioStep): ] def post_message(self, shift_swap_request: "ShiftSwapRequest") -> None: - self._slack_client.api_call( - "chat.postMessage", + self._slack_client.chat_postMessage( channel=shift_swap_request.slack_message.channel_id, thread_ts=shift_swap_request.slack_message.slack_id, reply_broadcast=True, diff --git a/engine/apps/slack/scenarios/slack_channel_integration.py b/engine/apps/slack/scenarios/slack_channel_integration.py index 2c633f62..b3e2a6d7 100644 --- a/engine/apps/slack/scenarios/slack_channel_integration.py +++ b/engine/apps/slack/scenarios/slack_channel_integration.py @@ -75,18 +75,13 @@ class SlackChannelMessageEventStep(scenario_step.ScenarioStep): # SlackMessage instances without alert_group set (e.g., SSR Slack messages) return - result = self._slack_client.api_call( - "chat.getPermalink", - channel=channel, - message_ts=message_ts, - ) + result = self._slack_client.chat_getPermalink(channel=channel, message_ts=message_ts) permalink = None if result["permalink"] is not None: permalink = result["permalink"] if len(text) > 2900: - self._slack_client.api_call( - "chat.postEphemeral", + self._slack_client.chat_postEphemeral( channel=channel, user=slack_user_identity.slack_id, text=":warning: Unable to show the <{}|message> in Resolution Note: the message is too long ({}). " diff --git a/engine/apps/slack/slack_client/__init__.py b/engine/apps/slack/slack_client/__init__.py deleted file mode 100644 index 528f1aa0..00000000 --- a/engine/apps/slack/slack_client/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .slack_client import SlackClientWithErrorHandling # noqa: F401 diff --git a/engine/apps/slack/slack_client/exceptions.py b/engine/apps/slack/slack_client/exceptions.py deleted file mode 100644 index d2c032b1..00000000 --- a/engine/apps/slack/slack_client/exceptions.py +++ /dev/null @@ -1,22 +0,0 @@ -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 SlackClientException(Exception): - pass diff --git a/engine/apps/slack/slack_client/slack_client_server.py b/engine/apps/slack/slack_client/slack_client_server.py deleted file mode 100644 index 235e084f..00000000 --- a/engine/apps/slack/slack_client/slack_client_server.py +++ /dev/null @@ -1,26 +0,0 @@ -import json - -from slackclient.server import Server - -from .exceptions import SlackClientException - - -class SlackClientServer(Server): - def api_call(self, token, request="?", timeout=None, **kwargs): - """ - This method is rewritten because we want to handle JSONDecodeError and add more information about response - """ - response = self.api_requester.do(token, request, kwargs, timeout=timeout) - response_json = {"headers": dict(response.headers)} - resp_text = response.text - try: - response_json.update(json.loads(resp_text)) - except json.JSONDecodeError: - response_json["response_text"] = resp_text - exception_text = ( - f"Slack API Call Error: unexpected response from Slack \n" - f"Status: {response.status_code}\nArgs: ('{request}',) \nKwargs: {kwargs} \n" - f"Response: {response_json}" - ) - raise SlackClientException(exception_text) - return json.dumps(response_json) diff --git a/engine/apps/slack/tasks.py b/engine/apps/slack/tasks.py index d94bd092..ca746e31 100644 --- a/engine/apps/slack/tasks.py +++ b/engine/apps/slack/tasks.py @@ -10,10 +10,9 @@ from django.utils import timezone from apps.alerts.tasks.compare_escalations import compare_escalations from apps.slack.alert_group_slack_service import AlertGroupSlackService +from apps.slack.client import SlackAPIException, SlackAPITokenException, SlackClientWithErrorHandling from apps.slack.constants import CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME, SLACK_BOT_ID from apps.slack.scenarios.scenario_step import ScenarioStep -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import SlackAPIException, SlackAPITokenException from apps.slack.utils import ( get_cache_key_update_incident_slack_message, get_populate_slack_channel_task_id_key, @@ -371,9 +370,7 @@ def populate_slack_usergroups_for_team(slack_team_identity_id): usergroups_list = None bot_access_token_accepted = True try: - usergroups_list = sc.api_call( - "usergroups.list", - ) + usergroups_list = sc.usergroups_list() except SlackAPITokenException as e: logger.info(f"token revoked\n{e}") except SlackAPIException as e: @@ -382,9 +379,7 @@ def populate_slack_usergroups_for_team(slack_team_identity_id): # Trying same request with access token. It is required due to migration to granular permissions # and can be removed after clients reinstall their bots sc_with_access_token = SlackClientWithErrorHandling(slack_team_identity.access_token) - usergroups_list = sc_with_access_token.api_call( - "usergroups.list", - ) + usergroups_list = sc_with_access_token.usergroups_list() bot_access_token_accepted = False except SlackAPIException as err: handle_usergroups_list_slack_api_exception(err) @@ -402,16 +397,10 @@ def populate_slack_usergroups_for_team(slack_team_identity_id): continue try: if bot_access_token_accepted: - usergroups_users = sc.api_call( - "usergroups.users.list", - usergroup=usergroup["id"], - ) + usergroups_users = sc.usergroups_users_list(usergroup=usergroup["id"]) else: sc_with_access_token = SlackClientWithErrorHandling(slack_team_identity.access_token) - usergroups_users = sc_with_access_token.api_call( - "usergroups.users.list", - usergroup=usergroup["id"], - ) + usergroups_users = sc_with_access_token.usergroups_users_list(usergroup=usergroup["id"]) except SlackAPIException as e: if e.response["error"] == "no_such_subteam": logger.info("User group does not exist") @@ -549,11 +538,13 @@ def populate_slack_channels_for_team(slack_team_identity_id: int, cursor: Option return start_populate_slack_channels_for_team(slack_team_identity_id, delay) try: response, cursor, rate_limited = sc.paginated_api_call_with_ratelimit( - "conversations.list", - types="public_channel,private_channel", + "conversations_list", paginated_key="channels", - limit=1000, - cursor=cursor, + json={ + "types": "public_channel,private_channel", + "limit": 1000, + "cursor": cursor, + }, ) except SlackAPITokenException as e: logger.info(f"token revoked\n{e}") diff --git a/engine/apps/slack/tests/test_populate_slack_channels.py b/engine/apps/slack/tests/test_populate_slack_channels.py index bb7a1df8..f8a2784b 100644 --- a/engine/apps/slack/tests/test_populate_slack_channels.py +++ b/engine/apps/slack/tests/test_populate_slack_channels.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest from django.utils import timezone -from apps.slack.slack_client import SlackClientWithErrorHandling +from apps.slack.client import SlackClientWithErrorHandling from apps.slack.tasks import populate_slack_channels_for_team diff --git a/engine/apps/slack/tests/test_scenario_steps/test_alert_group_actions.py b/engine/apps/slack/tests/test_scenario_steps/test_alert_group_actions.py index 8e4af96a..75517f30 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_alert_group_actions.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_alert_group_actions.py @@ -801,9 +801,14 @@ def test_step_format_alert( assert mock_slack_api_call.call_args.args == ("views.open",) +@patch("apps.slack.models.SlackTeamIdentity.get_conversation_members") @pytest.mark.django_db def test_step_resolution_note( - make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group, make_alert + mock_get_conversation_members, + make_organization_and_user_with_slack_identities, + make_alert_receive_channel, + make_alert_group, + make_alert, ): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() @@ -811,6 +816,7 @@ def test_step_resolution_note( alert_group = make_alert_group(alert_receive_channel) make_alert(alert_group, raw_request_data={}) + channel_id = "RANDOM_CHANNEL_ID" payload = { "trigger_id": "RANDOM_TRIGGER_ID", "actions": [ @@ -825,14 +831,15 @@ def test_step_resolution_note( ), } ], - "channel": {"id": "RANDOM_CHANNEL_ID"}, + "channel": {"id": channel_id}, "message": {"ts": "RANDOM_MESSAGE_TS"}, } step_class = ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep") step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_open") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.open",) + mock_slack_api_call.assert_called_once() + mock_get_conversation_members.assert_called_once_with(step._slack_client, channel_id) diff --git a/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py b/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py index 62ebe71f..91b336bd 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py @@ -3,9 +3,9 @@ from unittest.mock import patch import pytest from apps.alerts.models import AlertGroup +from apps.slack.client import SlackAPIException from apps.slack.models import SlackMessage from apps.slack.scenarios.scenario_step import ScenarioStep -from apps.slack.slack_client.exceptions import SlackAPIException @pytest.mark.django_db diff --git a/engine/apps/slack/tests/test_scenario_steps/test_manage_responders.py b/engine/apps/slack/tests/test_scenario_steps/test_manage_responders.py index 5682b55d..67bfbb4a 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_manage_responders.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_manage_responders.py @@ -94,10 +94,9 @@ def test_initial_state(manage_responders_setup): organization, user, slack_team_identity, slack_user_identity = manage_responders_setup step = StartManageResponders(slack_team_identity, organization, user) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_open") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.open",) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[ALERT_GROUP_DATA_KEY] == ALERT_GROUP_ID @@ -137,11 +136,9 @@ def test_add_user_no_warning(manage_responders_setup, make_schedule, make_on_cal payload = make_slack_payload(user=user) step = ManageRespondersUserChange(slack_team_identity, organization, user) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) - # check there's a delete button for the user assert mock_slack_api_call.call_args.kwargs["view"]["blocks"][0]["accessory"]["value"] == str(user.pk) @@ -153,10 +150,9 @@ def test_add_user_raise_warning(manage_responders_setup): payload = make_slack_payload(user=user) step = ManageRespondersUserChange(slack_team_identity, organization, user) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_push") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.push",) assert mock_slack_api_call.call_args.kwargs["view"]["callback_id"] == "ManageRespondersConfirmUserChange" text_from_blocks = "".join( b["text"]["text"] for b in mock_slack_api_call.call_args.kwargs["view"]["blocks"] if b["type"] == "section" @@ -188,10 +184,9 @@ def test_add_schedule(manage_responders_setup, make_schedule, make_on_call_shift payload = make_slack_payload(schedule=schedule) step = ManageRespondersScheduleChange(slack_team_identity, organization, user) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) assert mock_slack_api_call.call_args.kwargs["view"]["blocks"][0]["accessory"]["value"] == str(user.pk) @@ -221,10 +216,9 @@ def test_add_schedule_alert_group_resolved( payload = make_slack_payload(schedule=schedule) step = ManageRespondersScheduleChange(slack_team_identity, organization, user) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) assert ( DirectPagingAlertGroupResolvedError.DETAIL in mock_slack_api_call.call_args.kwargs["view"]["blocks"][0]["text"]["text"] @@ -237,10 +231,9 @@ def test_remove_user(manage_responders_setup): payload = make_slack_payload(actions=[{"value": user.pk}]) step = ManageRespondersRemoveUser(slack_team_identity, organization, user) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) # check there's no list of users in the view assert mock_slack_api_call.call_args.kwargs["view"]["blocks"][0]["accessory"]["type"] != "button" diff --git a/engine/apps/slack/tests/test_scenario_steps/test_paging.py b/engine/apps/slack/tests/test_scenario_steps/test_paging.py index 77835980..f4c2acfa 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_paging.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_paging.py @@ -89,14 +89,13 @@ def make_slack_payload( def test_initial_state( make_organization_and_user_with_slack_identities, ): - organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() + _, _, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() payload = {"channel_id": "123", "trigger_id": "111"} step = StartDirectPaging(slack_team_identity) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_open") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.open",) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[DataKey.USERS] == {} assert metadata[DataKey.SCHEDULES] == {} @@ -138,10 +137,9 @@ def test_add_user_no_warning( payload = make_slack_payload(organization=organization, user=user) step = OnPagingUserChange(slack_team_identity) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[DataKey.USERS] == {str(user.pk): Policy.DEFAULT} @@ -183,10 +181,9 @@ def test_add_user_maximum_exceeded( step = OnPagingUserChange(slack_team_identity) with patch("apps.slack.scenarios.paging.PRIVATE_METADATA_MAX_LENGTH", 100): - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) view_data = mock_slack_api_call.call_args.kwargs["view"] metadata = json.loads(view_data["private_metadata"]) # metadata unchanged, ignoring the prefix @@ -210,10 +207,9 @@ def test_add_user_raise_warning(make_organization_and_user_with_slack_identities payload = make_slack_payload(organization=organization, user=user) step = OnPagingUserChange(slack_team_identity) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_push") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.push",) assert mock_slack_api_call.call_args.kwargs["view"]["callback_id"] == "OnPagingConfirmUserChange" text_from_blocks = "".join( b["text"]["text"] for b in mock_slack_api_call.call_args.kwargs["view"]["blocks"] if b["type"] == "section" @@ -232,10 +228,9 @@ def test_change_user_policy(make_organization_and_user_with_slack_identities): ) step = OnPagingItemActionChange(slack_team_identity) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[DataKey.USERS] == {str(user.pk): Policy.IMPORTANT} @@ -249,10 +244,9 @@ def test_remove_user(make_organization_and_user_with_slack_identities): ) step = OnPagingItemActionChange(slack_team_identity) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[DataKey.USERS] == {} @@ -324,10 +318,9 @@ def test_add_schedule(make_organization_and_user_with_slack_identities, make_sch ) step = OnPagingScheduleChange(slack_team_identity) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[DataKey.SCHEDULES] == {str(schedule.pk): Policy.DEFAULT} assert metadata[DataKey.USERS] == {str(user.pk): Policy.IMPORTANT} @@ -345,10 +338,9 @@ def test_add_schedule_responders_exceeded(make_organization_and_user_with_slack_ step = OnPagingScheduleChange(slack_team_identity) with patch("apps.slack.scenarios.paging.PRIVATE_METADATA_MAX_LENGTH", 100): - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) view_data = mock_slack_api_call.call_args.kwargs["view"] metadata = json.loads(view_data["private_metadata"]) # metadata unchanged, ignoring the prefix @@ -376,10 +368,9 @@ def test_change_schedule_policy(make_organization_and_user_with_slack_identities ) step = OnPagingItemActionChange(slack_team_identity) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[DataKey.SCHEDULES] == {str(schedule.pk): Policy.IMPORTANT} assert metadata[DataKey.USERS] == {str(user.pk): Policy.DEFAULT} @@ -396,10 +387,9 @@ def test_remove_schedule(make_organization_and_user_with_slack_identities, make_ ) step = OnPagingItemActionChange(slack_team_identity) - with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) - assert mock_slack_api_call.call_args.args == ("views.update",) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[DataKey.SCHEDULES] == {} assert metadata[DataKey.USERS] == {str(user.pk): Policy.DEFAULT} diff --git a/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py b/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py index 285ead72..1cec2437 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py @@ -3,9 +3,8 @@ from unittest.mock import patch import pytest +from apps.slack.client import SlackAPIException, SlackClientWithErrorHandling from apps.slack.scenarios.scenario_step import ScenarioStep -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import SlackAPIException from common.api_helpers.utils import create_engine_url diff --git a/engine/apps/slack/tests/test_scenario_steps/test_shift_swap_requests.py b/engine/apps/slack/tests/test_scenario_steps/test_shift_swap_requests.py index f480b6b4..99cc0a76 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_shift_swap_requests.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_shift_swap_requests.py @@ -146,13 +146,13 @@ class TestBaseShiftSwapRequestStep: step = scenarios.BaseShiftSwapRequestStep(slack_team_identity, organization) with patch.object(step, "_slack_client") as mock_slack_client: - mock_slack_client.api_call.return_value = {"ts": ts} + mock_slack_client.chat_postMessage.return_value = {"ts": ts} slack_message = step.create_message(ssr) mock_generate_blocks.assert_called_once_with(ssr) - mock_slack_client.api_call.assert_called_once_with( - "chat.postMessage", channel=ssr.slack_channel_id, blocks=mock_generate_blocks.return_value + mock_slack_client.chat_postMessage.assert_called_once_with( + channel=ssr.slack_channel_id, blocks=mock_generate_blocks.return_value ) assert slack_message.slack_id == ts @@ -179,8 +179,8 @@ class TestBaseShiftSwapRequestStep: step.update_message(ssr) mock_generate_blocks.assert_called_once_with(ssr) - mock_slack_client.api_call.assert_called_once_with( - "chat.update", channel=ssr.slack_channel_id, ts=ts, blocks=mock_generate_blocks.return_value + mock_slack_client.chat_update.assert_called_once_with( + channel=ssr.slack_channel_id, ts=ts, blocks=mock_generate_blocks.return_value ) diff --git a/engine/apps/slack/tests/test_scenario_steps/test_slack_channel_integration.py b/engine/apps/slack/tests/test_scenario_steps/test_slack_channel_integration.py index 05bfe06d..207bd7c3 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_slack_channel_integration.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_slack_channel_integration.py @@ -1,4 +1,4 @@ -from unittest.mock import Mock, call, patch +from unittest.mock import Mock, patch import pytest @@ -237,7 +237,7 @@ class TestSlackChannelMessageEventStep: step = SlackChannelMessageEventStep(slack_team_identity, organization, user) step._slack_client = Mock() - step._slack_client.api_call.side_effect = [{"permalink": mock_permalink}, None] + step._slack_client.chat_getPermalink.return_value = {"permalink": mock_permalink} payload = { "event": { @@ -250,21 +250,15 @@ class TestSlackChannelMessageEventStep: step.save_thread_message_for_resolution_note(slack_user_identity, payload) - step._slack_client.api_call.assert_has_calls( - [ - call( - "chat.getPermalink", - channel=payload["event"]["channel"], - message_ts=payload["event"]["ts"], - ), - call( - "chat.postEphemeral", - channel=payload["event"]["channel"], - user=slack_user_identity.slack_id, - text=":warning: Unable to show the <{}|message> in Resolution Note: the message is too long ({}). " - "Max length - 2900 symbols.".format(mock_permalink, len(payload["event"]["text"])), - ), - ] + step._slack_client.chat_getPermalink.assert_called_once_with( + channel=payload["event"]["channel"], + message_ts=payload["event"]["ts"], + ) + step._slack_client.chat_postEphemeral.assert_called_once_with( + channel=payload["event"]["channel"], + user=slack_user_identity.slack_id, + text=":warning: Unable to show the <{}|message> in Resolution Note: the message is too long ({}). " + "Max length - 2900 symbols.".format(mock_permalink, len(payload["event"]["text"])), ) MockResolutionNoteSlackMessage.objects.get_or_create.assert_not_called() @@ -306,7 +300,7 @@ class TestSlackChannelMessageEventStep: step = SlackChannelMessageEventStep(slack_team_identity, organization, user) step._slack_client = Mock() - step._slack_client.api_call.side_effect = [{"permalink": mock_permalink}, None] + step._slack_client.chat_getPermalink.side_effect = [{"permalink": mock_permalink}, None] payload = { "event": { @@ -319,14 +313,9 @@ class TestSlackChannelMessageEventStep: step.save_thread_message_for_resolution_note(slack_user_identity, payload) - step._slack_client.api_call.assert_has_calls( - [ - call( - "chat.getPermalink", - channel=payload["event"]["channel"], - message_ts=payload["event"]["ts"], - ), - ] + step._slack_client.chat_getPermalink.assert_called_once_with( + channel=payload["event"]["channel"], + message_ts=payload["event"]["ts"], ) if resolution_note_slack_message_already_exists: diff --git a/engine/apps/slack/tests/test_user_group.py b/engine/apps/slack/tests/test_user_group.py index 3cecb0c3..614b6a44 100644 --- a/engine/apps/slack/tests/test_user_group.py +++ b/engine/apps/slack/tests/test_user_group.py @@ -3,8 +3,8 @@ from unittest.mock import PropertyMock, patch import pytest from apps.schedules.models.on_call_schedule import OnCallScheduleQuerySet +from apps.slack.client import SlackClientWithErrorHandling from apps.slack.models import SlackUserGroup -from apps.slack.slack_client import SlackClientWithErrorHandling @pytest.mark.django_db diff --git a/engine/apps/slack/utils.py b/engine/apps/slack/utils.py index de53843d..ab52746e 100644 --- a/engine/apps/slack/utils.py +++ b/engine/apps/slack/utils.py @@ -2,8 +2,7 @@ import enum import typing from datetime import datetime -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import SlackAPIException +from apps.slack.client import SlackAPIException, SlackClientWithErrorHandling if typing.TYPE_CHECKING: from apps.user_management.models import Organization @@ -65,7 +64,7 @@ def post_message_to_channel(organization: "Organization", channel_id: str, text: if organization.slack_team_identity: slack_client = SlackClientWithErrorHandling(organization.slack_team_identity.bot_access_token) try: - slack_client.api_call("chat.postMessage", channel=channel_id, text=text) + slack_client.chat_postMessage(channel=channel_id, text=text) except SlackAPIException as e: if e.response["error"] == "channel_not_found": pass diff --git a/engine/apps/slack/views.py b/engine/apps/slack/views.py index ec7ff8ac..fc11fedc 100644 --- a/engine/apps/slack/views.py +++ b/engine/apps/slack/views.py @@ -15,6 +15,7 @@ from rest_framework.views import APIView from apps.api.permissions import RBACPermission from apps.auth_token.auth import PluginAuthentication from apps.base.utils import live_settings +from apps.slack.client import SlackAPIException, SlackAPITokenException, SlackClientWithErrorHandling from apps.slack.scenarios.alertgroup_appearance import STEPS_ROUTING as ALERTGROUP_APPEARANCE_ROUTING # Importing routes from scenarios @@ -34,8 +35,6 @@ from apps.slack.scenarios.shift_swap_requests import STEPS_ROUTING as SHIFT_SWAP from apps.slack.scenarios.slack_channel import STEPS_ROUTING as CHANNEL_ROUTING from apps.slack.scenarios.slack_channel_integration import STEPS_ROUTING as SLACK_CHANNEL_INTEGRATION_ROUTING from apps.slack.scenarios.slack_usergroup import STEPS_ROUTING as SLACK_USERGROUP_UPDATE_ROUTING -from apps.slack.slack_client import SlackClientWithErrorHandling -from apps.slack.slack_client.exceptions import SlackAPIException, SlackAPITokenException from apps.slack.tasks import clean_slack_integration_leftovers, unpopulate_slack_user_identities from apps.slack.types import EventPayload, EventType, MessageEventSubtype, PayloadType, ScenarioRoute from apps.user_management.models import Organization @@ -195,10 +194,7 @@ class SlackEventApiEndpointView(APIView): if slack_team_identity.detected_token_revoked is not None: # check if token is still invalid try: - sc.api_call( - "auth.test", - team=slack_team_identity, - ) + sc.auth_test(team=slack_team_identity) except SlackAPITokenException: logger.info(f"Team {slack_team_identity.slack_id} has revoked token, dropping request.") return Response(status=200) @@ -223,7 +219,7 @@ class SlackEventApiEndpointView(APIView): elif ( payload_event_bot_id and slack_team_identity and payload_event_channel_type == EventType.MESSAGE_CHANNEL ): - response = sc.api_call("bots.info", bot=payload_event_bot_id) + response = sc.bots_info(bot=payload_event_bot_id) bot_user_id = response.get("bot", {}).get("user_id", "") # Don't react on own bot's messages. @@ -537,11 +533,7 @@ class SlackEventApiEndpointView(APIView): "text": "One more step!", }, } - slack_client.api_call( - "views.open", - trigger_id=payload["trigger_id"], - view=view, - ) + slack_client.views_open(trigger_id=payload["trigger_id"], view=view) class ResetSlackView(APIView): diff --git a/engine/conftest.py b/engine/conftest.py index 46d701a7..3ffaed41 100644 --- a/engine/conftest.py +++ b/engine/conftest.py @@ -69,7 +69,7 @@ from apps.schedules.tests.factories import ( OnCallScheduleICalFactory, ShiftSwapRequestFactory, ) -from apps.slack.slack_client import SlackClientWithErrorHandling +from apps.slack.client import SlackClientWithErrorHandling from apps.slack.tests.factories import ( SlackChannelFactory, SlackMessageFactory, diff --git a/engine/requirements.txt b/engine/requirements.txt index 952d1dac..50c3119b 100644 --- a/engine/requirements.txt +++ b/engine/requirements.txt @@ -1,6 +1,6 @@ django==3.2.20 djangorestframework==3.12.4 -slackclient==1.3.0 +slack_sdk==3.21.3 whitenoise==5.3.0 twilio~=6.37.0 phonenumbers==8.10.0