Skip to content

sending to channel-id, channel resolving and listing#2165

Open
ofek1weiss wants to merge 1 commit intomasterfrom
add-slack-channel-resolution
Open

sending to channel-id, channel resolving and listing#2165
ofek1weiss wants to merge 1 commit intomasterfrom
add-slack-channel-resolution

Conversation

@ofek1weiss
Copy link
Contributor

@ofek1weiss ofek1weiss commented Mar 22, 2026

null

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced Slack channel resolution supporting both channel names (with # prefix) and channel IDs.
    • New channel listing API with pagination and timeout controls.
    • Added rate-limit awareness with retry guidance when Slack APIs are throttled.
  • Refactor

    • Improved internal channel lookup flow with caching for better performance.

@github-actions
Copy link
Contributor

👋 @ofek1weiss
Thank you for raising your pull request.
Please make sure to add tests and document all user-facing changes.
You can do this by editing the docs files in this pull request.

@coderabbitai
Copy link

coderabbitai bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

Refactored Slack channel-lookup logic by replacing _get_channel_id with resolve_channel, introducing internal channel caching, and centralizing conversation-list pagination. Added get_channels method supporting cursor-based pagination with rate-limit handling via retry_after. Introduced ResolvedChannel and ChannelsResponse dataclasses for structured responses.

Changes

Cohort / File(s) Summary
Channel Resolution & Caching
elementary/messages/messaging_integrations/slack_web.py
Replaced _get_channel_id with resolve_channel for flexible channel lookup (ID or name), added (normalized_input, only_public) caching layer, and implemented regex-based channel input normalization. New dataclasses: ResolvedChannel and ChannelsResponse for structured responses. Centralized pagination in _list_conversations, updated _iter_channels for cursor handling, and added get_channels supporting pagination with Slack rate-limit (HTTPStatus.TOO_MANY_REQUESTS) handling via retry_after.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SlackWeb as SlackWeb<br/>resolve_channel
    participant Cache
    participant SlackAPI as Slack API
    
    Client->>SlackWeb: resolve_channel(channel)
    SlackWeb->>SlackWeb: normalize_input(channel)
    SlackWeb->>Cache: lookup (normalized, only_public)
    alt Found in cache
        Cache-->>SlackWeb: ResolvedChannel
    else Not in cache
        alt Channel looks like ID
            SlackWeb->>SlackAPI: conversations_info(channel_id)
            SlackAPI-->>SlackWeb: channel_info
        else Channel is name
            SlackWeb->>SlackAPI: conversations_list()
            SlackAPI-->>SlackWeb: channels page
            SlackWeb->>SlackWeb: iterate & match name
        end
        alt Channel found
            SlackWeb->>Cache: store ResolvedChannel
            Cache-->>SlackWeb: ✓
        else Not found
            SlackWeb->>SlackWeb: raise MessagingIntegrationError
        end
    end
    SlackWeb-->>Client: ResolvedChannel
Loading
sequenceDiagram
    participant Client
    participant SlackWeb as SlackWeb<br/>get_channels
    participant SlackAPI as Slack API
    
    Client->>SlackWeb: get_channels(cursor, timeout_seconds)
    loop Until cursor exhausted or timeout
        SlackWeb->>SlackAPI: _list_conversations(cursor)
        alt Rate limited (429)
            SlackAPI-->>SlackWeb: HTTPStatus.TOO_MANY_REQUESTS
            SlackWeb->>SlackWeb: extract retry_after header
            SlackWeb-->>Client: ChannelsResponse(channels,<br/>retry_after, cursor)
            Note over Client: Client should wait retry_after
        else Success
            SlackAPI-->>SlackWeb: channels + next_cursor
            SlackWeb->>SlackWeb: accumulate ResolvedChannels
            opt More pages & time remaining
                SlackWeb->>SlackWeb: continue with next_cursor
            end
        end
    end
    SlackWeb-->>Client: ChannelsResponse(all_channels,<br/>None, None)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

Hoppy channels now resolve with grace,
A cache keeps pace in every place. 🐰
From Slack's vast list we bound and search,
With rate-limits perched on every porch.
New methods hop with structured care! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: adding channel-ID support, channel resolution functionality, and channel listing capabilities to the Slack integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-slack-channel-resolution

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can suggest fixes for GitHub Check annotations.

Configure the reviews.tools.github-checks setting to adjust the time to wait for GitHub Checks to complete.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
elementary/messages/messaging_integrations/slack_web.py (2)

83-83: Consider using modern type hint syntax for consistency.

The dataclasses use Python 3.10+ syntax (list[...], int | None), but the cache uses legacy Dict/Tuple. Consider aligning the style:

-    self._channel_cache: Dict[Tuple[str, bool], ResolvedChannel] = {}
+    self._channel_cache: dict[tuple[str, bool], ResolvedChannel] = {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/messages/messaging_integrations/slack_web.py` at line 83, Update
the cache annotation to use modern PEP 585 syntax to match the project style:
replace the legacy typing usage on the field self._channel_cache:
Dict[Tuple[str, bool], ResolvedChannel] = {} with the builtin generic form
(e.g., dict[tuple[str, bool], ResolvedChannel]) and remove any now-unnecessary
typing imports (Dict, Tuple) or keep them only if used elsewhere; ensure the
class attribute ResolvedChannel reference remains unchanged and that the file's
minimum Python version supports PEP 585.

241-245: Consider adding only_public parameter for API consistency.

For consistency with resolve_channel, get_channels could accept an only_public parameter to filter the channel types fetched. This would allow callers to list only public channels when needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/messages/messaging_integrations/slack_web.py` around lines 241 -
245, Add an optional only_public: bool = False parameter to the get_channels
method signature (alongside cursor and timeout_seconds) and thread it through
the internal logic that builds the Slack API request so only public channels are
returned when True (mirror the filtering behavior used by resolve_channel).
Update any calls to get_channels and related unit tests to pass the new
parameter where needed, and ensure ChannelsResponse semantics remain unchanged
when only_public is False.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@elementary/messages/messaging_integrations/slack_web.py`:
- Around line 176-187: The channel listing ignores the only_public flag; modify
_list_conversations to accept an only_public: bool parameter and set types to
"public_channel" when only_public is True and "public_channel,private_channel"
otherwise, then update callers _iter_channels and resolve_channel to pass the
only_public argument through (ensure _iter_channels forwards only_public to
_list_conversations and resolve_channel passes the only_public it receives when
invoking _iter_channels) so private channels are excluded when only_public=True.
- Around line 268-274: When handling SlackApiError in the block that checks
err.response.status_code == HTTPStatus.TOO_MANY_REQUESTS, defensively read the
Retry-After header using err.response.headers.get("Retry-After") instead of
indexing, and only convert to int and assign to channels_response.retry_after if
the header is present and a valid integer; otherwise set
channels_response.retry_after to a sensible default (e.g., 1 or 0) or leave it
unset. Update the SlackApiError handler surrounding SlackApiError,
HTTPStatus.TOO_MANY_REQUESTS, err.response.headers, and
channels_response.retry_after to avoid KeyError/ValueError from a missing or
malformed header.

---

Nitpick comments:
In `@elementary/messages/messaging_integrations/slack_web.py`:
- Line 83: Update the cache annotation to use modern PEP 585 syntax to match the
project style: replace the legacy typing usage on the field self._channel_cache:
Dict[Tuple[str, bool], ResolvedChannel] = {} with the builtin generic form
(e.g., dict[tuple[str, bool], ResolvedChannel]) and remove any now-unnecessary
typing imports (Dict, Tuple) or keep them only if used elsewhere; ensure the
class attribute ResolvedChannel reference remains unchanged and that the file's
minimum Python version supports PEP 585.
- Around line 241-245: Add an optional only_public: bool = False parameter to
the get_channels method signature (alongside cursor and timeout_seconds) and
thread it through the internal logic that builds the Slack API request so only
public channels are returned when True (mirror the filtering behavior used by
resolve_channel). Update any calls to get_channels and related unit tests to
pass the new parameter where needed, and ensure ChannelsResponse semantics
remain unchanged when only_public is False.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b36fe43-522b-4a03-b091-5f2ac4000f29

📥 Commits

Reviewing files that changed from the base of the PR and between a91d876 and b3ceb3a.

📒 Files selected for processing (1)
  • elementary/messages/messaging_integrations/slack_web.py

Comment on lines +176 to +187
def _list_conversations(
self, cursor: Optional[str] = None
) -> Tuple[List[dict], Optional[str]]:
response = self.client.conversations_list(
cursor=cursor,
types="public_channel,private_channel",
exclude_archived=True,
limit=1000,
)
channels = response.get("channels", [])
cursor = response.get("response_metadata", {}).get("next_cursor")
return channels, cursor
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The only_public parameter is ignored throughout the channel iteration flow.

_list_conversations hardcodes types="public_channel,private_channel", making the only_public parameter in _iter_channels (line 194) and resolve_channel (line 212) dead code. When only_public=True is passed (e.g., line 165), private channels are still searched.

🐛 Proposed fix to respect `only_public`
 def _list_conversations(
-    self, cursor: Optional[str] = None
+    self, cursor: Optional[str] = None, only_public: bool = False
 ) -> Tuple[List[dict], Optional[str]]:
+    types = "public_channel" if only_public else "public_channel,private_channel"
     response = self.client.conversations_list(
         cursor=cursor,
-        types="public_channel,private_channel",
+        types=types,
         exclude_archived=True,
         limit=1000,
     )
-    channels = response.get("channels", [])
-    cursor = response.get("response_metadata", {}).get("next_cursor")
-    return channels, cursor
+    channels = response.get("channels", [])
+    next_cursor = response.get("response_metadata", {}).get("next_cursor")
+    return channels, next_cursor

Then update callers to pass the parameter through.

Also applies to: 191-207

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/messages/messaging_integrations/slack_web.py` around lines 176 -
187, The channel listing ignores the only_public flag; modify
_list_conversations to accept an only_public: bool parameter and set types to
"public_channel" when only_public is True and "public_channel,private_channel"
otherwise, then update callers _iter_channels and resolve_channel to pass the
only_public argument through (ensure _iter_channels forwards only_public to
_list_conversations and resolve_channel passes the only_public it receives when
invoking _iter_channels) so private channels are excluded when only_public=True.

Comment on lines +268 to +274
except SlackApiError as err:
if err.response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
channels_response.retry_after = int(
err.response.headers["Retry-After"]
)
break
raise
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add defensive check for Retry-After header.

While Slack's API typically includes this header with 429 responses, a missing header would cause a KeyError. Consider using .get() with a sensible default:

             except SlackApiError as err:
                 if err.response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
-                    channels_response.retry_after = int(
-                        err.response.headers["Retry-After"]
-                    )
+                    retry_after = err.response.headers.get("Retry-After")
+                    channels_response.retry_after = int(retry_after) if retry_after else 60
                     break
                 raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except SlackApiError as err:
if err.response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
channels_response.retry_after = int(
err.response.headers["Retry-After"]
)
break
raise
except SlackApiError as err:
if err.response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
retry_after = err.response.headers.get("Retry-After")
channels_response.retry_after = int(retry_after) if retry_after else 60
break
raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/messages/messaging_integrations/slack_web.py` around lines 268 -
274, When handling SlackApiError in the block that checks
err.response.status_code == HTTPStatus.TOO_MANY_REQUESTS, defensively read the
Retry-After header using err.response.headers.get("Retry-After") instead of
indexing, and only convert to int and assign to channels_response.retry_after if
the header is present and a valid integer; otherwise set
channels_response.retry_after to a sensible default (e.g., 1 or 0) or leave it
unset. Update the SlackApiError handler surrounding SlackApiError,
HTTPStatus.TOO_MANY_REQUESTS, err.response.headers, and
channels_response.retry_after to avoid KeyError/ValueError from a missing or
malformed header.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant