diff --git a/src/codegen/extensions/slack/utils/README.md b/src/codegen/extensions/slack/utils/README.md new file mode 100644 index 000000000..f404809cf --- /dev/null +++ b/src/codegen/extensions/slack/utils/README.md @@ -0,0 +1,32 @@ +# Slack Utilities + +This module provides utility functions for working with the Slack API in a safe and reliable way. + +## Features + +### `safe_add_reaction` + +A utility function that safely adds a reaction to a Slack message, handling the case where the reaction already exists. + +This function is particularly useful for preventing the `SlackApiError` with the error message `already_reacted` that can occur when trying to add a reaction that already exists on a message. + +### Usage + +```python +from slack_sdk import WebClient +from codegen.extensions.slack.utils import safe_add_reaction + +client = WebClient(token="your-token") + +# Safely add a reaction +response = safe_add_reaction(client=client, channel="C12345", timestamp="1234567890.123456", name="thumbsup") + +# The function will not raise an exception if the reaction already exists +``` + +## Error Handling + +The `safe_add_reaction` function handles the following error cases: + +- If the reaction already exists (`already_reacted` error), it logs the information and returns a success response. +- For all other errors, it logs the error and re-raises the exception for proper handling upstream. diff --git a/src/codegen/extensions/slack/utils/__init__.py b/src/codegen/extensions/slack/utils/__init__.py new file mode 100644 index 000000000..f6d3db86c --- /dev/null +++ b/src/codegen/extensions/slack/utils/__init__.py @@ -0,0 +1,46 @@ +"""Utility functions for Slack integration.""" + +import logging +from typing import Any, Dict, Optional + +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError + +from codegen.shared.logging.get_logger import get_logger + +logger = get_logger(__name__) +logger.setLevel(logging.INFO) + + +def safe_add_reaction( + client: WebClient, + channel: str, + timestamp: str, + name: str, +) -> dict[str, Any]: + """Safely add a reaction to a Slack message, handling the case where the reaction already exists. + + Args: + client: The Slack WebClient instance + channel: The channel ID where the message is located + timestamp: The timestamp of the message + name: The name of the reaction emoji to add + + Returns: + The response from the Slack API or an error response + """ + try: + return client.reactions_add( + channel=channel, + timestamp=timestamp, + name=name, + ) + except SlackApiError as e: + # If the error is "already_reacted", just log it and continue + if e.response["error"] == "already_reacted": + logger.info(f"Reaction '{name}' already exists on message in channel {channel} at {timestamp}. Ignoring.") + # Return a success response to prevent further error handling + return {"ok": True, "already_exists": True} + # For other errors, re-raise + logger.exception(f"Error adding reaction: {e}") + raise diff --git a/tests/unit/codegen/extensions/slack/utils/__init__.py b/tests/unit/codegen/extensions/slack/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/codegen/extensions/slack/utils/test_utils.py b/tests/unit/codegen/extensions/slack/utils/test_utils.py new file mode 100644 index 000000000..8aa877904 --- /dev/null +++ b/tests/unit/codegen/extensions/slack/utils/test_utils.py @@ -0,0 +1,51 @@ +"""Tests for Slack utility functions.""" + +from unittest.mock import MagicMock + +import pytest +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError + +from codegen.extensions.slack.utils import safe_add_reaction + + +def test_safe_add_reaction_success(): + """Test that safe_add_reaction works correctly when the API call succeeds.""" + mock_client = MagicMock(spec=WebClient) + mock_client.reactions_add.return_value = {"ok": True} + + result = safe_add_reaction(client=mock_client, channel="C12345", timestamp="1234567890.123456", name="thumbsup") + + assert result == {"ok": True} + mock_client.reactions_add.assert_called_once_with(channel="C12345", timestamp="1234567890.123456", name="thumbsup") + + +def test_safe_add_reaction_already_reacted(): + """Test that safe_add_reaction handles the 'already_reacted' error correctly.""" + mock_client = MagicMock(spec=WebClient) + + # Create a mock SlackApiError with the 'already_reacted' error + mock_response = {"ok": False, "error": "already_reacted"} + mock_error = SlackApiError(message="already_reacted", response=mock_response) + mock_client.reactions_add.side_effect = mock_error + + result = safe_add_reaction(client=mock_client, channel="C12345", timestamp="1234567890.123456", name="thumbsup") + + # Should return a success response with already_exists=True + assert result == {"ok": True, "already_exists": True} + mock_client.reactions_add.assert_called_once_with(channel="C12345", timestamp="1234567890.123456", name="thumbsup") + + +def test_safe_add_reaction_other_error(): + """Test that safe_add_reaction re-raises other errors.""" + mock_client = MagicMock(spec=WebClient) + + # Create a mock SlackApiError with a different error + mock_response = {"ok": False, "error": "invalid_auth"} + mock_error = SlackApiError(message="invalid_auth", response=mock_response) + mock_client.reactions_add.side_effect = mock_error + + with pytest.raises(SlackApiError): + safe_add_reaction(client=mock_client, channel="C12345", timestamp="1234567890.123456", name="thumbsup") + + mock_client.reactions_add.assert_called_once_with(channel="C12345", timestamp="1234567890.123456", name="thumbsup")