Skip to content

CG-17442: Fix DoubleReactException by handling already_reacted Slack API errors #1061

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/codegen/extensions/slack/utils/README.md
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 46 additions & 0 deletions src/codegen/extensions/slack/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -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(

Check failure on line 33 in src/codegen/extensions/slack/utils/__init__.py

View workflow job for this annotation

GitHub Actions / mypy

error: Incompatible return value type (got "SlackResponse", expected "dict[str, Any]") [return-value]
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
Empty file.
51 changes: 51 additions & 0 deletions tests/unit/codegen/extensions/slack/utils/test_utils.py
Original file line number Diff line number Diff line change
@@ -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")
Loading