Skip to content

feat: add Discord channel adapter for channel_hub (#335)#497

Open
hognek wants to merge 4 commits into
jaylfc:masterfrom
hognek:feat/discord-channel-adapter
Open

feat: add Discord channel adapter for channel_hub (#335)#497
hognek wants to merge 4 commits into
jaylfc:masterfrom
hognek:feat/discord-channel-adapter

Conversation

@hognek
Copy link
Copy Markdown
Contributor

@hognek hognek commented May 31, 2026

Summary

Adds a Discord channel adapter that integrates with the channel_hub system, enabling agents to communicate through Discord channels.

Closes #335

Summary by CodeRabbit

  • New Features
    • Added Discord channel integration enabling agents to connect to Discord channels
    • Polls messages from Discord and routes them through the agent system
    • Supports sending messages back to Discord with text, images, and interactive buttons
    • Automatically handles Discord rate limiting and authentication
    • Skips bot-authored messages to avoid feedback loops

Add DiscordConnector in channel_hub/adapters/discord.py following the
existing adapter pattern. Uses httpx for Discord HTTP API polling with
after-id pagination, per-channel rate limiting (5/5s), and graceful
error handling. Emits channel-hub messages with source=discord,
channel_id, guild_id, and author metadata.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Review Change Stack

Warning

Review limit reached

@hognek, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 20 minutes and 55 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f75d0053-6e64-49c8-9d95-0e0921ded32e

📥 Commits

Reviewing files that changed from the base of the PR and between 474324b and cd6b6eb.

📒 Files selected for processing (1)
  • tests/channel_hub/test_discord.py
📝 Walkthrough

Walkthrough

This PR adds a complete Discord channel adapter that polls Discord channels for messages, routes them through an agent router, and sends responses back to Discord with formatted content, embeds, and interactive buttons. The implementation handles rate limiting, tracks message pagination per channel, and filters out bot-originated messages.

Changes

Discord Channel Adapter

Layer / File(s) Summary
Discord API constants and connector initialization
tinyagentos/channel_hub/adapters/discord.py
Defines Discord API base URL constant and initializes the DiscordConnector class with bot token, router, channel IDs, auth headers, running state tracking, bot user ID resolution placeholder, and per-channel message ID tracking.
Connector lifecycle management
tinyagentos/channel_hub/adapters/discord.py
start() resolves the bot user ID via the Discord /users/@me endpoint, raises RuntimeError if the ID cannot be determined, and spawns the background polling task. stop() halts polling and cancels the task.
Message polling and channel fetching
tinyagentos/channel_hub/adapters/discord.py
Main _poll() loop iterates over configured channels and periodically calls _check_channel(). The fetch method retrieves messages using after-id pagination, reacts to HTTP 429 by sleeping for retry_after, logs and skips on auth (401) and non-200 responses, updates the per-channel last-seen message ID, and filters out messages authored by the bot.
Message routing and response handling
tinyagentos/channel_hub/adapters/discord.py
_handle_message() converts Discord message payloads into IncomingMessage objects with author, channel, guild context, and raw metadata, routes through the agent router, and sends a response when one is returned. _send_response() posts passthrough or constructed payloads with optional content, HTTP-filtered image embeds, and button components (max 5 buttons per action row). Helper _build_channel_name() formats human-readable names for guild or DM messages.
Test suite for DiscordConnector
tests/test_channel_hub_new.py
Comprehensive TestDiscordAdapter covering start() error paths (network failure, missing bot ID), successful startup with bot ID resolution and _running state management, absence of a semaphore attribute, and message routing validation.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant DiscordConnector
  participant DiscordAPI as Discord API
  Client->>DiscordConnector: start()
  DiscordConnector->>DiscordAPI: GET /users/@me
  DiscordAPI-->>DiscordConnector: bot user ID
  DiscordConnector->>DiscordConnector: spawn _poll() task
  DiscordConnector-->>Client: running
  Client->>DiscordConnector: stop()
  DiscordConnector->>DiscordConnector: cancel _poll() task
  DiscordConnector-->>Client: stopped
Loading
flowchart
  Poll["_poll() loop running"]
  Channels["Iterate channels"]
  Fetch["_check_channel(channel_id)"]
  Paginate["Fetch with after message_id"]
  RateLimit["HTTP 429?"]
  Wait["Sleep retry_after"]
  Update["Update last_message_id"]
  Filter["Skip bot messages"]
  Sleep["Sleep poll_interval"]
  Poll --> Channels
  Channels --> Fetch
  Fetch --> Paginate
  Paginate --> RateLimit
  RateLimit -->|Yes| Wait
  Wait --> Paginate
  RateLimit -->|No| Update
  Update --> Filter
  Filter --> Sleep
  Sleep --> Channels
Loading
sequenceDiagram
  participant Discord as Discord API
  participant Connector as DiscordConnector
  participant Router as Agent Router
  Discord->>Connector: message event
  Connector->>Connector: _handle_message()
  Connector->>Connector: build IncomingMessage
  Connector->>Router: route_message()
  Router-->>Connector: OutgoingMessage
  Connector->>Connector: _send_response()
  Connector->>Discord: POST /channels/{id}/messages
  Discord-->>Connector: response posted
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A new Discord bridge hops into town,
Polling channels round and round,
Messages route with buttons so bright,
Agent responses send back through the night,
Bot user ID resolved at start—
A clever adapter, a fluffy work of art!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding a Discord channel adapter for the channel_hub system.
Linked Issues check ✅ Passed The PR implements Discord channel adapter with message polling and routing, partially fulfilling issue #335's Discord bridge requirements, though the internal phone-transfer router is not included.
Out of Scope Changes check ✅ Passed All changes are scoped to Discord adapter implementation (new module and tests) and align with the Discord-second phasing objective in issue #335.
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@hognek
Copy link
Copy Markdown
Contributor Author

hognek commented May 31, 2026

My apologies for the mess here. Some fixes escaped my digital friends :)

Comment thread tinyagentos/channel_hub/adapters/discord.py Outdated
Comment thread tinyagentos/channel_hub/adapters/discord.py Outdated
@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented May 31, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/channel_hub/adapters/discord.py 115 Semaphore for rate limiting may be ineffective
tinyagentos/channel_hub/adapters/discord.py 72 Failure to resolve bot user ID may cause message loop
Other Observations (not in diff)

No issues found in unchanged code.

Files Reviewed (1 files)
  • tinyagentos/channel_hub/adapters/discord.py - 2 issues

Reviewed by nemotron-3-super-120b-a12b-20230311:free · 75,710 tokens

jaylfc and others added 2 commits May 31, 2026 14:06
- Remove ineffective per-channel semaphore (_channel_sem); channels are
  polled sequentially so the Semaphore(5) never had concurrent callers.
  Rate limiting is already handled reactively via the 429/retry_after path.
- Raise RuntimeError in start() when bot user ID cannot be resolved (bad
  token or network error) instead of silently continuing, which would cause
  the connector to respond to its own messages and loop indefinitely.
- Add TestDiscordAdapter tests covering both new start() failure modes and
  the absence of _channel_sem.
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tinyagentos/channel_hub/adapters/discord.py`:
- Around line 228-235: The passthrough POST branch (checking
response.passthrough and response.passthrough_platform == "discord") is missing
the try/except around client.post; wrap the client.post call in a try block and
catch httpx.RequestError, logging the same descriptive error used for normal
sends (e.g., "Failed to send Discord response" plus the exception) and returning
after handling the error; mirror the error handling pattern used in the regular
POST branch (the try/except around client.post) so you use httpx.RequestError
and the same logger (e.g., self.logger or logger) and ensure the function still
returns on success or after logging the error.
- Around line 64-85: The code currently sets self._running = True before
validating the bot ID, which can leave the connector in a running state if
resolving the bot user ID (the HTTP call using httpx and resp.json()) raises a
RuntimeError; move the self._running = True assignment to after successful
resolution of self._bot_user_id and after creating the background task with
asyncio.create_task(self._poll_loop()), so that _running is only True when
_bot_user_id is populated and _task exists (ensure any exceptions raised during
the GET to f"{DISCORD_API_BASE}/users/@me" still propagate and do not leave
_running True).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b2d6148c-2d13-4f6b-9a9b-bd556978c3d3

📥 Commits

Reviewing files that changed from the base of the PR and between ce9a23a and 474324b.

📒 Files selected for processing (2)
  • tests/test_channel_hub_new.py
  • tinyagentos/channel_hub/adapters/discord.py

Comment on lines +64 to +85
self._running = True
# Resolve bot user ID — required to skip the bot's own messages.
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(
f"{DISCORD_API_BASE}/users/@me", headers=self.headers,
)
if resp.status_code == 200:
self._bot_user_id = resp.json().get("id")
except Exception as exc:
raise RuntimeError(
f"Discord connector for '{self.agent_name}': could not resolve "
f"bot user ID — check your token. ({exc})"
) from exc

if not self._bot_user_id:
raise RuntimeError(
f"Discord connector for '{self.agent_name}': /users/@me returned "
f"an empty user ID — token may be invalid."
)

self._task = asyncio.create_task(self._poll_loop())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

_running set before validation can leave inconsistent state on failure.

If RuntimeError is raised (lines 74 or 80), _running remains True but no task exists. Move the assignment after successful bot ID resolution.

Proposed fix
     async def start(self) -> None:
-        self._running = True
         # Resolve bot user ID — required to skip the bot's own messages.
         try:
             async with httpx.AsyncClient(timeout=10) as client:
                 resp = await client.get(
                     f"{DISCORD_API_BASE}/users/@me", headers=self.headers,
                 )
                 if resp.status_code == 200:
                     self._bot_user_id = resp.json().get("id")
         except Exception as exc:
             raise RuntimeError(
                 f"Discord connector for '{self.agent_name}': could not resolve "
                 f"bot user ID — check your token. ({exc})"
             ) from exc

         if not self._bot_user_id:
             raise RuntimeError(
                 f"Discord connector for '{self.agent_name}': /users/@me returned "
                 f"an empty user ID — token may be invalid."
             )

+        self._running = True
         self._task = asyncio.create_task(self._poll_loop())
📝 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
self._running = True
# Resolve bot user ID — required to skip the bot's own messages.
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(
f"{DISCORD_API_BASE}/users/@me", headers=self.headers,
)
if resp.status_code == 200:
self._bot_user_id = resp.json().get("id")
except Exception as exc:
raise RuntimeError(
f"Discord connector for '{self.agent_name}': could not resolve "
f"bot user ID — check your token. ({exc})"
) from exc
if not self._bot_user_id:
raise RuntimeError(
f"Discord connector for '{self.agent_name}': /users/@me returned "
f"an empty user ID — token may be invalid."
)
self._task = asyncio.create_task(self._poll_loop())
# Resolve bot user ID — required to skip the bot's own messages.
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(
f"{DISCORD_API_BASE}/users/@me", headers=self.headers,
)
if resp.status_code == 200:
self._bot_user_id = resp.json().get("id")
except Exception as exc:
raise RuntimeError(
f"Discord connector for '{self.agent_name}': could not resolve "
f"bot user ID — check your token. ({exc})"
) from exc
if not self._bot_user_id:
raise RuntimeError(
f"Discord connector for '{self.agent_name}': /users/@me returned "
f"an empty user ID — token may be invalid."
)
self._running = True
self._task = asyncio.create_task(self._poll_loop())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/channel_hub/adapters/discord.py` around lines 64 - 85, The code
currently sets self._running = True before validating the bot ID, which can
leave the connector in a running state if resolving the bot user ID (the HTTP
call using httpx and resp.json()) raises a RuntimeError; move the self._running
= True assignment to after successful resolution of self._bot_user_id and after
creating the background task with asyncio.create_task(self._poll_loop()), so
that _running is only True when _bot_user_id is populated and _task exists
(ensure any exceptions raised during the GET to f"{DISCORD_API_BASE}/users/@me"
still propagate and do not leave _running True).

Comment on lines +228 to +235
if response.passthrough and response.passthrough_platform == "discord":
payload = response.passthrough_payload
await client.post(
f"{DISCORD_API_BASE}/channels/{channel_id}/messages",
headers=self.headers,
json=payload,
)
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing error handling for passthrough POST.

The passthrough POST (lines 230-234) lacks the try/except httpx.RequestError that the regular POST has (lines 266-275). A network failure here will bubble up with a generic poll error instead of the descriptive "Failed to send Discord response" log.

Proposed fix
         if response.passthrough and response.passthrough_platform == "discord":
             payload = response.passthrough_payload
-            await client.post(
-                f"{DISCORD_API_BASE}/channels/{channel_id}/messages",
-                headers=self.headers,
-                json=payload,
-            )
+            try:
+                await client.post(
+                    f"{DISCORD_API_BASE}/channels/{channel_id}/messages",
+                    headers=self.headers,
+                    json=payload,
+                )
+            except httpx.RequestError as exc:
+                logger.error(
+                    "Failed to send Discord passthrough response to %s: %s",
+                    channel_id, exc,
+                )
             return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/channel_hub/adapters/discord.py` around lines 228 - 235, The
passthrough POST branch (checking response.passthrough and
response.passthrough_platform == "discord") is missing the try/except around
client.post; wrap the client.post call in a try block and catch
httpx.RequestError, logging the same descriptive error used for normal sends
(e.g., "Failed to send Discord response" plus the exception) and returning after
handling the error; mirror the error handling pattern used in the regular POST
branch (the try/except around client.post) so you use httpx.RequestError and the
same logger (e.g., self.logger or logger) and ensure the function still returns
on success or after logging the error.

Two test-implementation mismatches from the auto-generated test suite:

1. test_stores_per_channel_semaphores → test_initial_bot_user_id_none
   Adapter has no _channel_sem (rate limiting is reactive, not
   semaphore-based). Replaced with test for actual init state attribute.

2. test_start_resolves_bot_user_id_failure
   Adapter raises RuntimeError on empty _bot_user_id (fail-fast design
   is correct — can't poll without filtering own messages). Test now
   expects the RuntimeError instead of asserting graceful degradation.
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.

External comms channels (WhatsApp/Telegram/Discord) with internal phone-transfer router

2 participants