Skip to content

feat(trogon-source-discord): add Discord source with Gateway and Interactions support#98

Merged
yordis merged 1 commit intomainfrom
yordis/trogon-source-discord
Apr 6, 2026
Merged

feat(trogon-source-discord): add Discord source with Gateway and Interactions support#98
yordis merged 1 commit intomainfrom
yordis/trogon-source-discord

Conversation

@yordis
Copy link
Copy Markdown
Member

@yordis yordis commented Apr 5, 2026

  • Normalize Discord integration to follow the trogon-source-* pattern as a dumb inbound pipe into NATS JetStream
  • Two mutually exclusive modes via DISCORD_MODE: gateway (WebSocket, all events) or webhook (HTTP Interactions Endpoint, interactions only)
  • Gateway mode uses Serenity to connect to Discord and publishes every event (messages, reactions, members, voice, etc.) to NATS without filtering
  • Webhook mode uses Ed25519 signature verification for the HTTP Interactions Endpoint (slash commands, buttons, modals, autocomplete)

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 5, 2026

PR Summary

Medium Risk
Adds a new inbound Discord integration that opens network-facing HTTP and WebSocket paths and publishes into JetStream, increasing operational/security surface area (signature verification, request/reply for autocomplete). Changes are largely additive but touch shared workspace deps and local compose/dev tooling.

Overview
Adds a new trogon-source-discord service that can ingest Discord events in two modes: a Gateway WebSocket client that republishes dispatch events to JetStream, and an HTTP Interactions webhook that verifies Ed25519 signatures and publishes (or request/replies for autocomplete) over NATS.

Wires the source into local Docker Compose (new service, env vars, optional ngrok tunnel), extends shared workspace deps with twilight-* and related lockfile updates, and registers the binary in acp-telemetry::ServiceName for consistent logging/OTel naming.

Reviewed by Cursor Bugbot for commit fa6ca2f. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new trogon-source-discord service: Docker Compose entries and Dockerfile, a README, and a new Rust crate implementing Discord Gateway and Webhook ingestion with configuration, constants, signature verification, gateway bridge, webhook server, JetStream provisioning/publishing, a binary entrypoint, and tests.

Changes

Cohort / File(s) Summary
Docker Compose & env
devops/docker/compose/.env.example, devops/docker/compose/compose.yml
Added commented Discord env templates and a trogon-source-discord service; updated ngrok to route Discord webhooks and depend on the new service.
Docker service files
devops/docker/compose/services/trogon-source-discord/Dockerfile, devops/docker/compose/services/trogon-source-discord/README.md
Added multi-stage Dockerfile for the binary and README documenting gateway vs webhook modes, setup, and env vars.
Workspace deps & manifest
rsworkspace/Cargo.toml, rsworkspace/crates/trogon-source-discord/Cargo.toml
Added twilight-gateway/twilight-model workspace deps and a new crate manifest declaring the trogon-source-discord binary and dependencies.
Telemetry enum
rsworkspace/crates/acp-telemetry/src/service_name.rs
Added ServiceName::TrogonSourceDiscord variant and updated tests.
Config & constants
rsworkspace/crates/trogon-source-discord/src/config.rs, .../src/constants.rs
New DiscordConfig and SourceMode env parsing (mode-specific validation, intents CSV parsing, numeric defaults) and exported constants for defaults, headers, and NATS keys.
Signature module
rsworkspace/crates/trogon-source-discord/src/signature.rs
Ed25519 public-key parsing and signature verification with SignatureError and unit tests.
Gateway bridge
rsworkspace/crates/trogon-source-discord/src/gateway.rs
Added GatewayBridge that maps Twilight gateway Events to event names, serializes payloads, extracts dedup/guild ids, and publishes to JetStream with headers and ack-timeout handling; includes tests.
Webhook server & routing
rsworkspace/crates/trogon-source-discord/src/server.rs
Axum router with /webhook and /health, request size limits, signature verification, PING handling, unroutable publishing, JetStream stream provisioning, publish/ack handling, and tests.
Crate entrypoints & lib
rsworkspace/crates/trogon-source-discord/src/lib.rs, .../src/main.rs
Library module re-exports and async main that loads config, connects to NATS/JetStream, provisions streams, and runs Gateway or Webhook mode with graceful shutdown and logging.

Sequence Diagram(s)

sequenceDiagram
    participant DiscordGateway as Discord Gateway
    participant Shard as Twilight Shard
    participant Bridge as GatewayBridge
    participant JetStream as JetStream (NATS)

    DiscordGateway->>Shard: Emit Gateway Event
    Shard->>Bridge: next_event() -> Event
    Bridge->>Bridge: map event -> name, serialize JSON
    Bridge->>JetStream: publish("discord.<event>", payload, headers, ack_timeout)
    JetStream-->>Bridge: Ack / Error
Loading
sequenceDiagram
    participant HTTP as Discord (Webhook)
    participant Axum as Axum Server
    participant Sig as Signature Verifier
    participant JetStream as JetStream (NATS)

    HTTP->>Axum: POST /webhook (x-signature-ed25519, x-signature-timestamp, body)
    Axum->>Sig: verify(public_key, timestamp, body, signature)
    alt Signature Invalid
        Sig-->>Axum: SignatureError
        Axum-->>HTTP: 401 Unauthorized
    else Signature Valid
        Axum->>Axum: parse JSON -> type_id
        alt type_id == 1 (PING)
            Axum-->>HTTP: 200 {"type":1}
        else Known Interaction
            Axum->>JetStream: publish("discord.<type>", body, headers)
            JetStream-->>Axum: Ack / Error
            alt Publish Success
                Axum-->>HTTP: 200 {"type":5 or 8}
            else Publish Failure
                Axum-->>HTTP: 500 Internal Server Error
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

rust:coverage-baseline-reset

Poem

🐰 I hopped in with keys and code,
Signed each ping down the rabbit road,
Gateway hum and webhook cheer,
Events to NATS — safe and clear,
I twitch my nose, the stream's in mode 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding Discord source support with two operation modes (Gateway and Interactions).
Description check ✅ Passed The description provides meaningful context about the PR objectives, explaining the normalization pattern, dual modes, and key implementation details for both gateway and webhook approaches.
Docstring Coverage ✅ Passed Docstring coverage is 84.80% 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
  • Commit unit tests in branch yordis/trogon-source-discord

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.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 5, 2026

badge

Code Coverage Summary

Details
Filename                                                                      Stmts    Miss  Cover    Missing
--------------------------------------------------------------------------  -------  ------  -------  ---------------------------------------------------------------------------------------------
crates/trogon-std/src/env/in_memory.rs                                           81       0  100.00%
crates/trogon-std/src/env/system.rs                                              17       0  100.00%
crates/trogon-std/src/fs/mem.rs                                                 220      10  95.45%   61-63, 77-79, 133-135, 158
crates/trogon-std/src/fs/system.rs                                               29      12  58.62%   17-19, 31-45
crates/trogon-nats/src/mocks.rs                                                 304       0  100.00%
crates/trogon-nats/src/connect.rs                                               105      11  89.52%   22-24, 37, 49, 68-73
crates/trogon-nats/src/messaging.rs                                             552       2  99.64%   132, 142
crates/trogon-nats/src/client.rs                                                 25      25  0.00%    50-89
crates/trogon-nats/src/auth.rs                                                  119       0  100.00%
crates/trogon-nats/src/nats_token.rs                                            161       0  100.00%
crates/trogon-nats/src/token.rs                                                   8       0  100.00%
crates/trogon-std/src/json.rs                                                    30       0  100.00%
crates/trogon-std/src/args.rs                                                    10       0  100.00%
crates/trogon-source-discord/src/server.rs                                      632       0  100.00%
crates/trogon-source-discord/src/config.rs                                      305       2  99.34%   234, 393
crates/trogon-source-discord/src/signature.rs                                   103       0  100.00%
crates/trogon-source-discord/src/main.rs                                          4       0  100.00%
crates/trogon-source-discord/src/gateway.rs                                     440       1  99.77%   135
crates/trogon-std/src/time/mock.rs                                              129       0  100.00%
crates/trogon-std/src/time/system.rs                                             27       3  88.89%   27-29
crates/acp-nats/src/nats/subjects/client_ops/session_request_permission.rs       15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_create.rs                  15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_read_text_file.rs                15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_update.rs                   15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_kill.rs                    15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_output.rs                  15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_write_text_file.rs               15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_wait_for_exit.rs           15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_release.rs                 15       0  100.00%
crates/trogon-source-telegram/src/main.rs                                         4       0  100.00%
crates/trogon-source-telegram/src/signature.rs                                   38       0  100.00%
crates/trogon-source-telegram/src/server.rs                                     375       0  100.00%
crates/trogon-source-telegram/src/config.rs                                     104       0  100.00%
crates/trogon-nats/src/jetstream/publish.rs                                      57       0  100.00%
crates/trogon-nats/src/jetstream/mocks.rs                                       474       0  100.00%
crates/acp-nats/src/jetstream/consumers.rs                                       99       0  100.00%
crates/acp-nats/src/jetstream/provision.rs                                       61       0  100.00%
crates/acp-nats/src/jetstream/ext_policy.rs                                      26       0  100.00%
crates/acp-nats/src/jetstream/streams.rs                                        194       4  97.94%   254-256, 266
crates/acp-nats/src/nats/subjects/responses/update.rs                            27       0  100.00%
crates/acp-nats/src/nats/subjects/responses/prompt_response.rs                   27       0  100.00%
crates/acp-nats/src/nats/subjects/responses/ext_ready.rs                         15       0  100.00%
crates/acp-nats/src/nats/subjects/responses/cancelled.rs                         18       0  100.00%
crates/acp-nats/src/nats/subjects/responses/response.rs                          20       0  100.00%
crates/trogon-source-gitlab/src/config.rs                                       115       0  100.00%
crates/trogon-source-gitlab/src/server.rs                                       439       0  100.00%
crates/trogon-source-gitlab/src/signature.rs                                     30       0  100.00%
crates/trogon-source-gitlab/src/webhook_secret.rs                                36       0  100.00%
crates/trogon-source-gitlab/src/main.rs                                           4       0  100.00%
crates/acp-nats/src/client/fs_read_text_file.rs                                 384       0  100.00%
crates/acp-nats/src/client/mod.rs                                              2987       0  100.00%
crates/acp-nats/src/client/rpc_reply.rs                                          71       0  100.00%
crates/acp-nats/src/client/ext.rs                                               365       8  97.81%   193-204, 229-240
crates/acp-nats/src/client/terminal_create.rs                                   294       0  100.00%
crates/acp-nats/src/client/fs_write_text_file.rs                                451       0  100.00%
crates/acp-nats/src/client/session_update.rs                                     55       0  100.00%
crates/acp-nats/src/client/request_permission.rs                                338       0  100.00%
crates/acp-nats/src/client/ext_session_prompt_response.rs                       157       0  100.00%
crates/acp-nats/src/client/terminal_kill.rs                                     309       0  100.00%
crates/acp-nats/src/client/terminal_release.rs                                  357       0  100.00%
crates/acp-nats/src/client/terminal_wait_for_exit.rs                            396       0  100.00%
crates/acp-nats/src/client/terminal_output.rs                                   223       0  100.00%
crates/acp-nats-agent/src/connection.rs                                        1434       1  99.93%   686
crates/acp-nats/src/nats/parsing.rs                                             285       1  99.65%   153
crates/acp-nats/src/nats/extensions.rs                                            3       0  100.00%
crates/acp-nats/src/nats/mod.rs                                                  23       0  100.00%
crates/acp-nats/src/agent/ext_notification.rs                                    88       0  100.00%
crates/acp-nats/src/agent/logout.rs                                              49       0  100.00%
crates/acp-nats/src/agent/bridge.rs                                             123       4  96.75%   109-112
crates/acp-nats/src/agent/new_session.rs                                         91       0  100.00%
crates/acp-nats/src/agent/test_support.rs                                       299       0  100.00%
crates/acp-nats/src/agent/resume_session.rs                                     102       0  100.00%
crates/acp-nats/src/agent/cancel.rs                                             105       0  100.00%
crates/acp-nats/src/agent/close_session.rs                                       67       0  100.00%
crates/acp-nats/src/agent/list_sessions.rs                                       50       0  100.00%
crates/acp-nats/src/agent/set_session_model.rs                                   71       0  100.00%
crates/acp-nats/src/agent/fork_session.rs                                       106       0  100.00%
crates/acp-nats/src/agent/ext_method.rs                                          92       0  100.00%
crates/acp-nats/src/agent/prompt.rs                                             633       0  100.00%
crates/acp-nats/src/agent/mod.rs                                                 65       0  100.00%
crates/acp-nats/src/agent/authenticate.rs                                        52       0  100.00%
crates/acp-nats/src/agent/initialize.rs                                          83       0  100.00%
crates/acp-nats/src/agent/set_session_config_option.rs                           71       0  100.00%
crates/acp-nats/src/agent/js_request.rs                                         304       0  100.00%
crates/acp-nats/src/agent/set_session_mode.rs                                    71       0  100.00%
crates/acp-nats/src/agent/load_session.rs                                       101       0  100.00%
crates/trogon-std/src/dirs/fixed.rs                                              84       0  100.00%
crates/trogon-std/src/dirs/system.rs                                             98      11  88.78%   57, 65, 67, 75, 77, 85, 87, 96, 98, 109, 154
crates/acp-nats-ws/src/config.rs                                                 83       0  100.00%
crates/acp-nats-ws/src/upgrade.rs                                                57       2  96.49%   59, 90
crates/acp-nats-ws/src/main.rs                                                  187      18  90.37%   87, 204-225, 303
crates/acp-nats-ws/src/connection.rs                                            166      35  78.92%   75-82, 87-98, 114, 116-117, 122, 133-135, 142, 146, 150, 153-161, 172, 176, 179, 182-186, 220
crates/acp-nats/src/nats/subjects/stream.rs                                      58       0  100.00%
crates/acp-nats/src/nats/subjects/mod.rs                                        380       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_agent.rs                     18       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent.rs                     11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_client.rs                    11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_session.rs                   11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent_ext.rs                 11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/global_all.rs                    11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_client.rs                    18       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/prompt_wildcard.rs               11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_session.rs                   18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/fork.rs                               18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/prompt.rs                             18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/cancel.rs                             18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/load.rs                               18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_model.rs                          18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/close.rs                              18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_config_option.rs                  18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/resume.rs                             18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_mode.rs                           18       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_list.rs                          8       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_new.rs                           8       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext.rs                                  12       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext_notify.rs                           12       0  100.00%
crates/acp-nats/src/nats/subjects/global/authenticate.rs                          8       0  100.00%
crates/acp-nats/src/nats/subjects/global/initialize.rs                            8       0  100.00%
crates/acp-nats/src/nats/subjects/global/logout.rs                                8       0  100.00%
crates/trogon-source-linear/src/config.rs                                       208       0  100.00%
crates/trogon-source-linear/src/main.rs                                           4       0  100.00%
crates/trogon-source-linear/src/server.rs                                       351       3  99.15%   175-177
crates/trogon-source-linear/src/signature.rs                                     54       1  98.15%   16
crates/acp-nats/src/req_id.rs                                                    39       0  100.00%
crates/acp-nats/src/session_id.rs                                                72       0  100.00%
crates/acp-nats/src/ext_method_name.rs                                           70       0  100.00%
crates/acp-nats/src/pending_prompt_waiters.rs                                   141       0  100.00%
crates/acp-nats/src/in_flight_slot_guard.rs                                      32       0  100.00%
crates/acp-nats/src/config.rs                                                   204       0  100.00%
crates/acp-nats/src/error.rs                                                     84       0  100.00%
crates/acp-nats/src/jsonrpc.rs                                                    6       0  100.00%
crates/acp-nats/src/acp_prefix.rs                                                51       0  100.00%
crates/acp-nats/src/client_proxy.rs                                             200       0  100.00%
crates/acp-nats/src/lib.rs                                                       73       0  100.00%
crates/acp-telemetry/src/log.rs                                                  70       2  97.14%   39-40
crates/acp-telemetry/src/service_name.rs                                         46       0  100.00%
crates/acp-telemetry/src/lib.rs                                                 153      22  85.62%   39-46, 81, 86, 91, 105-120
crates/acp-telemetry/src/trace.rs                                                32       4  87.50%   23-24, 31-32
crates/acp-telemetry/src/signal.rs                                                3       3  0.00%    4-43
crates/acp-telemetry/src/metric.rs                                               35       4  88.57%   30-31, 38-39
crates/trogon-source-github/src/server.rs                                       368       0  100.00%
crates/trogon-source-github/src/config.rs                                       104       0  100.00%
crates/trogon-source-github/src/signature.rs                                     64       0  100.00%
crates/trogon-source-github/src/main.rs                                           4       0  100.00%
crates/acp-nats-stdio/src/main.rs                                               141      27  80.85%   62, 114-121, 127-129, 146, 177-198
crates/acp-nats-stdio/src/config.rs                                              72       0  100.00%
crates/acp-nats/src/telemetry/metrics.rs                                         65       0  100.00%
crates/trogon-source-slack/src/main.rs                                            4       0  100.00%
crates/trogon-source-slack/src/config.rs                                        163       0  100.00%
crates/trogon-source-slack/src/server.rs                                        925       3  99.68%   119-121
crates/trogon-source-slack/src/signature.rs                                      80       0  100.00%
TOTAL                                                                         21731     219  98.99%

Diff against main

Filename                                         Stmts    Miss  Cover
---------------------------------------------  -------  ------  --------
crates/trogon-source-discord/src/server.rs        +632       0  +100.00%
crates/trogon-source-discord/src/config.rs        +305      +2  +99.34%
crates/trogon-source-discord/src/signature.rs     +103       0  +100.00%
crates/trogon-source-discord/src/main.rs            +4       0  +100.00%
crates/trogon-source-discord/src/gateway.rs       +440      +1  +99.77%
crates/acp-telemetry/src/service_name.rs            +5       0  +100.00%
TOTAL                                            +1489      +3  +0.06%

Results for commit: fa6ca2f

Minimum allowed coverage is 95%

♻️ This comment has been updated with latest results

@yordis yordis force-pushed the yordis/trogon-source-discord branch 3 times, most recently from b1d9f4b to e495480 Compare April 5, 2026 22:07
@yordis yordis force-pushed the yordis/trogon-source-discord branch from e495480 to 9a23880 Compare April 5, 2026 22:13
@yordis yordis force-pushed the yordis/trogon-source-discord branch from 9a23880 to 1c138b8 Compare April 5, 2026 22:26
@yordis yordis force-pushed the yordis/trogon-source-discord branch from 1c138b8 to 7ac50b0 Compare April 5, 2026 22:53
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: 3

🧹 Nitpick comments (9)
rsworkspace/crates/trogon-source-discord/src/gateway.rs (5)

130-140: Redundant wildcard arm after explicit Ping match.

The _ => None arm (Line 137) is unreachable after Interaction::Ping(_) => None (Line 136) unless Serenity adds new variants in the future. If the intent is forward compatibility, consider adding a comment; otherwise, the wildcard can be removed.

♻️ Option 1: Add comment for clarity
             Interaction::Modal(i) => i.guild_id.map(|g| g.get()),
             Interaction::Ping(_) => None,
-            _ => None,
+            _ => None, // Forward compatibility for future interaction types
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs` around lines 130 -
140, In interaction_create, the match includes an explicit Interaction::Ping(_)
=> None followed by an unreachable `_ => None` arm; remove the redundant
wildcard arm (or if you intentionally want forward-compatibility, replace it
with a short comment noting future Serenity variants) so the match only covers
the concrete Interaction::Command/Component/Autocomplete/Modal arms plus the
Ping arm; update the match in the interaction_create method accordingly to
eliminate the unreachable `_` pattern.

46-77: Consider simplifying the outcome handling.

The is_ok() check followed by log_on_error works but is slightly redundant since log_on_error already handles the Published case as a no-op. You could simplify this.

♻️ Suggested simplification
         let outcome = publish_event(
             self.js.as_ref(),
             subject,
             headers,
             Bytes::from(json),
             self.nats_ack_timeout,
         )
         .await;
 
-        if outcome.is_ok() {
-            debug!(event = event_name, "published gateway event to NATS");
-        } else {
-            outcome.log_on_error(event_name);
+        outcome.log_on_error(event_name);
+        if outcome.is_ok() {
+            debug!(event = event_name, "published gateway event to NATS");
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs` around lines 46 -
77, In publish (async fn publish) simplify the outcome handling by removing the
explicit if outcome.is_ok() { debug!(...) } else {
outcome.log_on_error(event_name); } branch and instead call
outcome.log_on_error(event_name) unconditionally after awaiting publish_event;
this relies on publish_event's return type and its log_on_error method to be a
no-op on Published and to log errors otherwise, preserving behaviour while
removing redundancy.

378-380: thread_member_update also publishes with guild_id: None.

Similar to thread_delete, the ThreadMember struct might contain guild context. This could impact downstream consumers that filter by guild.

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

In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs` around lines 378 -
380, The handler thread_member_update currently calls publish with guild_id
hardcoded to None; change it to forward the guild context from the ThreadMember
by passing thread_member.guild_id (or the equivalent field on ThreadMember) into
publish so downstream consumers can filter by guild; keep the None fallback if
the ThreadMember.guild_id is None. Ensure the change is made in async fn
thread_member_update and references the ThreadMember argument.

152-163: Missing guild_id in reaction_remove_all event.

The reaction_remove_all handler publishes with guild_id: None, but this event can occur in guild channels. The Serenity reaction_remove_all callback doesn't provide guild_id directly, so this is a limitation of the Serenity API rather than a bug in this code. Consider documenting this limitation or potentially looking up the channel to retrieve the guild_id if needed downstream.

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

In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs` around lines 152 -
163, The reaction_remove_all handler currently omits guild_id in the published
payload; update the ReactionRemoveAll payload to include an Option<u64>
guild_id, attempt to resolve the channel's guild_id via the Context (e.g., look
up channel with ctx.cache or ctx.http to get Channel.guild_id), set guild_id to
Some(gid.get()) when present or None otherwise, and pass the new payload to
self.publish in async fn reaction_remove_all so downstream consumers receive
guild context when available; if lookup fails leave guild_id None and consider
adding a brief comment documenting the Serenity limitation.

369-371: Extract guild_id from PartialGuildChannel in thread_delete for consistency with other event handlers.

The PartialGuildChannel struct contains a guild_id field. Currently passing None prevents downstream filtering by guild. Extract it using the same pattern as thread_create, thread_update, and other handlers:

let guild_id = thread.guild_id.get();
self.publish("thread_delete", Some(guild_id), &thread).await;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs` around lines 369 -
371, The thread_delete handler currently calls self.publish with None for guild
id; change it to extract the guild id from the PartialGuildChannel and pass it
to publish like the other handlers. In the async fn thread_delete(&self, _ctx:
Context, thread: PartialGuildChannel, _full_thread_data: Option<GuildChannel>)
use thread.guild_id.get() to produce a guild_id and call
self.publish("thread_delete", Some(guild_id), &thread).await so downstream
filtering works consistently with thread_create/thread_update.
rsworkspace/crates/trogon-source-discord/src/server.rs (2)

122-126: router() panics if public_key is None.

The expect() will panic at runtime if public_key is not set. The caller (main.rs Line 67-68) guards this with if config.public_key.is_some(), but this is a runtime invariant that could be violated if refactored. Consider accepting VerifyingKey directly instead of &DiscordConfig.

♻️ Alternative: Accept VerifyingKey directly
-pub fn router<P: JetStreamPublisher>(js: P, config: &DiscordConfig) -> Router {
-    let public_key = config
-        .public_key
-        .expect("router requires DISCORD_PUBLIC_KEY to be set");
+pub fn router<P: JetStreamPublisher>(
+    js: P,
+    public_key: VerifyingKey,
+    subject_prefix: String,
+    nats_ack_timeout: Duration,
+    max_body_size: ByteSize,
+) -> Router {
     let state = AppState {
         js,
         public_key,
-        subject_prefix: config.subject_prefix.clone(),
-        nats_ack_timeout: config.nats_ack_timeout,
+        subject_prefix,
+        nats_ack_timeout,
     };
     // ...
-        .layer(RequestBodyLimitLayer::new(
-            config.max_body_size.as_u64() as usize,
-        ))
+        .layer(RequestBodyLimitLayer::new(max_body_size.as_u64() as usize))

This makes the requirement explicit at the type level.

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

In `@rsworkspace/crates/trogon-source-discord/src/server.rs` around lines 122 -
126, The router function currently calls expect on config.public_key which can
panic; change the router signature from fn router<P: JetStreamPublisher>(js: P,
config: &DiscordConfig) -> Router to accept the parsed VerifyingKey (or whatever
concrete type is used for public keys) directly — e.g. fn router<P:
JetStreamPublisher>(js: P, public_key: VerifyingKey) -> Router — then update
uses inside router (AppState construction and any references to
config.public_key) to use that public_key parameter and remove the expect() call
so the requirement is enforced at the type level (adjust callers of router to
pass the validated VerifyingKey).

58-66: INFO-level logging on every successful publish may be noisy.

Logging at INFO level for every published interaction could generate significant log volume in high-traffic environments. Consider DEBUG level for routine success.

♻️ Consider using DEBUG level for success
 fn outcome_to_status<E: fmt::Display>(outcome: PublishOutcome<E>) -> StatusCode {
     if outcome.is_ok() {
-        info!("Published Discord interaction to NATS");
+        tracing::debug!("Published Discord interaction to NATS");
         StatusCode::OK
     } else {
         outcome.log_on_error("discord");
         StatusCode::INTERNAL_SERVER_ERROR
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-discord/src/server.rs` around lines 58 - 66,
The function outcome_to_status currently logs every successful publish at INFO
level (info!("Published Discord interaction to NATS")), which is noisy; change
the success log to DEBUG level by replacing that info! call with debug! while
keeping the same message, so PublishOutcome successes are logged at debug level;
leave the error path using outcome.log_on_error("discord") and the StatusCode
returns unchanged.
rsworkspace/crates/trogon-source-discord/src/config.rs (1)

94-104: Default intents include privileged intents requiring Discord approval.

GUILD_MEMBERS and MESSAGE_CONTENT are privileged intents that require explicit enablement in the Discord Developer Portal. Consider documenting this requirement or logging a warning when privileged intents are used.

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

In `@rsworkspace/crates/trogon-source-discord/src/config.rs` around lines 94 -
104, The default_intents() function currently includes privileged
GatewayIntents::GUILD_MEMBERS and GatewayIntents::MESSAGE_CONTENT; update the
code to not enable these privileged intents by default and instead require
explicit opt-in or surface a warning: remove GUILD_MEMBERS and MESSAGE_CONTENT
from default_intents(), add documentation comment in config.rs explaining that
those two intents are privileged and must be enabled in the Discord Developer
Portal, and, where configuration is parsed (the code that calls
default_intents() or builds GatewayIntents), log or return a clear warning if
the user explicitly requests GUILD_MEMBERS or MESSAGE_CONTENT so the operator is
informed about the approval requirement.
rsworkspace/crates/trogon-source-discord/src/main.rs (1)

111-111: unreachable!() depends on config validation invariant.

This relies on DiscordConfig::from_env panicking if neither token nor key is provided. While currently valid, if the config validation changes or is bypassed, this would cause an unclear panic. Consider a more descriptive error message.

♻️ Consider a more descriptive message
-        (None, None) => unreachable!("config validation ensures at least one is set"),
+        (None, None) => {
+            error!("Neither gateway nor interactions configured - this should have been caught by config validation");
+            return Err("No input paths configured".into());
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-discord/src/main.rs` at line 111, The match
arm using unreachable!("config validation ensures at least one is set") is
fragile; replace it with a clear, descriptive failure that includes context
(e.g., panic! or return Err) so the error is informative if validation is
bypassed — update the (None, None) arm to panic!("Neither Discord token nor key
were provided: DiscordConfig::from_env must supply at least one") or,
preferably, return a Result::Err with that message from main so callers get a
descriptive error instead of an ambiguous unreachable! panic; reference the
match arm and DiscordConfig::from_env when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@devops/docker/compose/services/trogon-source-discord/README.md`:
- Around line 79-90: The environment variables table in README.md is missing
DISCORD_STREAM_MAX_AGE_SECS which is documented in .env.example; add a table row
for `DISCORD_STREAM_MAX_AGE_SECS` (required: no, Default: `604800`, Description:
max age in seconds for JetStream stream messages or similar) so the README
matches the .env.example and code expectations.

In `@rsworkspace/crates/trogon-source-discord/src/main.rs`:
- Around line 86-112: The current match over (gateway_handle,
interactions_handle) only logs Ok(Err(e)) from JoinHandle results and ignores
Err(join_error) which means the task panicked; update all places that consume
the join handle (the tokio::select! arms and the single-await branches for gw
and http) to also match and log Err(join_error) (and include the join_error
details in the log) in addition to the existing Ok(Err(e)) handling so panics
are not silently ignored — specifically adjust the logic around gateway_handle /
interactions_handle (variables gw and http) and the tokio::select! arms to
handle both Ok(Err(e)) and Err(join_error) cases with appropriate error!
logging.

In `@rsworkspace/crates/trogon-source-discord/src/server.rs`:
- Around line 249-253: The code currently defaults interaction_id to the literal
"unknown" when parsed.get("id") is missing, which can cause NATS deduplication
collisions; update the handling in the function/block that assigns
interaction_id (the parsed.get("id").and_then(|v| v.as_str()).unwrap_or(...)
code) to either (a) reject the request early with an error/response when the id
field is missing, or (b) generate a unique fallback ID (e.g., a UUID/v4 or a
monotonic counter) and use that as the NATS message ID instead of the fixed
"unknown", ensuring the rest of the flow that publishes to NATS uses the new
variable.

---

Nitpick comments:
In `@rsworkspace/crates/trogon-source-discord/src/config.rs`:
- Around line 94-104: The default_intents() function currently includes
privileged GatewayIntents::GUILD_MEMBERS and GatewayIntents::MESSAGE_CONTENT;
update the code to not enable these privileged intents by default and instead
require explicit opt-in or surface a warning: remove GUILD_MEMBERS and
MESSAGE_CONTENT from default_intents(), add documentation comment in config.rs
explaining that those two intents are privileged and must be enabled in the
Discord Developer Portal, and, where configuration is parsed (the code that
calls default_intents() or builds GatewayIntents), log or return a clear warning
if the user explicitly requests GUILD_MEMBERS or MESSAGE_CONTENT so the operator
is informed about the approval requirement.

In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs`:
- Around line 130-140: In interaction_create, the match includes an explicit
Interaction::Ping(_) => None followed by an unreachable `_ => None` arm; remove
the redundant wildcard arm (or if you intentionally want forward-compatibility,
replace it with a short comment noting future Serenity variants) so the match
only covers the concrete Interaction::Command/Component/Autocomplete/Modal arms
plus the Ping arm; update the match in the interaction_create method accordingly
to eliminate the unreachable `_` pattern.
- Around line 46-77: In publish (async fn publish) simplify the outcome handling
by removing the explicit if outcome.is_ok() { debug!(...) } else {
outcome.log_on_error(event_name); } branch and instead call
outcome.log_on_error(event_name) unconditionally after awaiting publish_event;
this relies on publish_event's return type and its log_on_error method to be a
no-op on Published and to log errors otherwise, preserving behaviour while
removing redundancy.
- Around line 378-380: The handler thread_member_update currently calls publish
with guild_id hardcoded to None; change it to forward the guild context from the
ThreadMember by passing thread_member.guild_id (or the equivalent field on
ThreadMember) into publish so downstream consumers can filter by guild; keep the
None fallback if the ThreadMember.guild_id is None. Ensure the change is made in
async fn thread_member_update and references the ThreadMember argument.
- Around line 152-163: The reaction_remove_all handler currently omits guild_id
in the published payload; update the ReactionRemoveAll payload to include an
Option<u64> guild_id, attempt to resolve the channel's guild_id via the Context
(e.g., look up channel with ctx.cache or ctx.http to get Channel.guild_id), set
guild_id to Some(gid.get()) when present or None otherwise, and pass the new
payload to self.publish in async fn reaction_remove_all so downstream consumers
receive guild context when available; if lookup fails leave guild_id None and
consider adding a brief comment documenting the Serenity limitation.
- Around line 369-371: The thread_delete handler currently calls self.publish
with None for guild id; change it to extract the guild id from the
PartialGuildChannel and pass it to publish like the other handlers. In the async
fn thread_delete(&self, _ctx: Context, thread: PartialGuildChannel,
_full_thread_data: Option<GuildChannel>) use thread.guild_id.get() to produce a
guild_id and call self.publish("thread_delete", Some(guild_id), &thread).await
so downstream filtering works consistently with thread_create/thread_update.

In `@rsworkspace/crates/trogon-source-discord/src/main.rs`:
- Line 111: The match arm using unreachable!("config validation ensures at least
one is set") is fragile; replace it with a clear, descriptive failure that
includes context (e.g., panic! or return Err) so the error is informative if
validation is bypassed — update the (None, None) arm to panic!("Neither Discord
token nor key were provided: DiscordConfig::from_env must supply at least one")
or, preferably, return a Result::Err with that message from main so callers get
a descriptive error instead of an ambiguous unreachable! panic; reference the
match arm and DiscordConfig::from_env when making the change.

In `@rsworkspace/crates/trogon-source-discord/src/server.rs`:
- Around line 122-126: The router function currently calls expect on
config.public_key which can panic; change the router signature from fn router<P:
JetStreamPublisher>(js: P, config: &DiscordConfig) -> Router to accept the
parsed VerifyingKey (or whatever concrete type is used for public keys) directly
— e.g. fn router<P: JetStreamPublisher>(js: P, public_key: VerifyingKey) ->
Router — then update uses inside router (AppState construction and any
references to config.public_key) to use that public_key parameter and remove the
expect() call so the requirement is enforced at the type level (adjust callers
of router to pass the validated VerifyingKey).
- Around line 58-66: The function outcome_to_status currently logs every
successful publish at INFO level (info!("Published Discord interaction to
NATS")), which is noisy; change the success log to DEBUG level by replacing that
info! call with debug! while keeping the same message, so PublishOutcome
successes are logged at debug level; leave the error path using
outcome.log_on_error("discord") and the StatusCode returns unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 292b0834-438a-40a3-a94b-b22a3c098490

📥 Commits

Reviewing files that changed from the base of the PR and between 1c17914 and 7ac50b0.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • devops/docker/compose/.env.example
  • devops/docker/compose/compose.yml
  • devops/docker/compose/services/trogon-source-discord/Dockerfile
  • devops/docker/compose/services/trogon-source-discord/README.md
  • rsworkspace/crates/acp-telemetry/src/service_name.rs
  • rsworkspace/crates/trogon-source-discord/Cargo.toml
  • rsworkspace/crates/trogon-source-discord/src/config.rs
  • rsworkspace/crates/trogon-source-discord/src/constants.rs
  • rsworkspace/crates/trogon-source-discord/src/gateway.rs
  • rsworkspace/crates/trogon-source-discord/src/lib.rs
  • rsworkspace/crates/trogon-source-discord/src/main.rs
  • rsworkspace/crates/trogon-source-discord/src/server.rs
  • rsworkspace/crates/trogon-source-discord/src/signature.rs

@yordis yordis force-pushed the yordis/trogon-source-discord branch from 7ac50b0 to 50a4735 Compare April 5, 2026 22:59
@yordis yordis changed the title feat(trogon-source-discord): add Discord interaction webhook receiver feat(trogon-source-discord): add Discord source with Gateway and Interactions support Apr 5, 2026
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: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rsworkspace/crates/trogon-source-discord/src/config.rs`:
- Around line 25-34: DiscordConfig currently accepts subject_prefix and
stream_name as raw Strings (fields subject_prefix and stream_name), allowing
blank/invalid NATS identifiers; replace or guard these by introducing/using
domain value objects (e.g., NatsSubjectPrefix, StreamName) or perform
trimming+validation in the config factory/constructor (reject empty after trim
and validate allowed NATS identifier characters/length) so invalid instances are
unrepresentable; update all call sites that construct DiscordConfig to use the
validated factories and apply the same change/validation to the other config
struct fields referenced around lines 81-86.
- Around line 50-55: Change the default_intents() implementation so it returns
only non-privileged intents (use GatewayIntents::non_privileged()) instead of
including GUILD_MEMBERS and MESSAGE_CONTENT; keep parse_gateway_intents(&s) and
the DISCORD_GATEWAY_INTENTS env var behavior the same but document in comments
that privileged intents must be explicitly opted into via
DISCORD_GATEWAY_INTENTS (e.g. "non_privileged,guild_members,message_content") so
bots without portal-enabled privileged intents won’t be disconnected on startup.
- Around line 15-23: The current SourceMode enum (variants Gateway and Webhook)
forces mutually exclusive ingestion paths; change it to a configuration struct
that allows both paths to be enabled simultaneously by replacing pub enum
SourceMode { Gateway { bot_token: String, intents: GatewayIntents }, Webhook {
public_key: VerifyingKey }, } with a struct like SourceConfig { gateway:
Option<GatewayConfig>, webhook: Option<WebhookConfig> } (define GatewayConfig
and WebhookConfig with the existing fields), update serde (if present) and all
usages of SourceMode (constructors, match arms, functions expecting SourceMode)
to handle Option<GatewayConfig> and Option<WebhookConfig>, and ensure code that
previously assumed exclusivity (e.g., connection setup/validation logic) now
permits both to coexist and prefers the interactions receiver when an
interactions endpoint/public_key is configured while still running the gateway
bridge when gateway config is present.

In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs`:
- Around line 170-180: Update the wrapper payloads to preserve and forward
optional context instead of dropping it: in guild_member_removal add a
member_data_if_available: Option<Member> field to the MemberRemove payload and
pass the incoming _member_data_if_available through to publish; in
guild_role_delete include the role (or optional role data param) on the payload
instead of discarding _role; in integration_delete include the optional
application_id on the payload and forward the incoming _application_id; in
thread_delete add fields for full_thread_data: Option<Channel> (or the thread
type used) and guild_id: Option<u64>, extract guild_id from the thread parameter
(e.g. thread.guild_id() or thread.guild_id) when present and forward the
full_thread_data instead of hard-coding None; update the corresponding publish
calls (event names: "guild_member_remove", "guild_role_delete",
"integration_delete", "thread_delete") to send these enriched payloads. Ensure
the new payload structs are serde::Serialize and use .get() on IDs only when
present.
- Around line 46-76: The gateway publish path currently awaits publish_event
(which waits for JetStream ACK) inside async fn publish, blocking callbacks;
change it to enqueue the prepared message into a bounded mpsc channel and return
immediately. Create a background publisher task (spawned when the gateway/Js
client is created) that reads from the channel and calls
publish_event(self.js.as_ref(), subject, headers, Bytes::from(json),
self.nats_ack_timeout). Use a bounded tokio::sync::mpsc channel to provide
backpressure, use try_send (or try_send + a drop/log strategy) in publish to
avoid blocking the callback, and have the background task log
outcome.log_on_error(event_name) / debug on success. Ensure unique symbols:
modify async fn publish to build subject/header/json then send to the new
channel instead of awaiting publish_event; add a publisher loop that uses
publish_event and handles errors.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f594c8de-77d7-4c41-80a1-3c8d5e2c322a

📥 Commits

Reviewing files that changed from the base of the PR and between 7ac50b0 and 50a4735.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • devops/docker/compose/.env.example
  • devops/docker/compose/compose.yml
  • devops/docker/compose/services/trogon-source-discord/Dockerfile
  • devops/docker/compose/services/trogon-source-discord/README.md
  • rsworkspace/crates/acp-telemetry/src/service_name.rs
  • rsworkspace/crates/trogon-source-discord/Cargo.toml
  • rsworkspace/crates/trogon-source-discord/src/config.rs
  • rsworkspace/crates/trogon-source-discord/src/constants.rs
  • rsworkspace/crates/trogon-source-discord/src/gateway.rs
  • rsworkspace/crates/trogon-source-discord/src/lib.rs
  • rsworkspace/crates/trogon-source-discord/src/main.rs
  • rsworkspace/crates/trogon-source-discord/src/server.rs
  • rsworkspace/crates/trogon-source-discord/src/signature.rs
✅ Files skipped from review due to trivial changes (6)
  • devops/docker/compose/services/trogon-source-discord/README.md
  • rsworkspace/crates/trogon-source-discord/Cargo.toml
  • rsworkspace/crates/trogon-source-discord/src/lib.rs
  • devops/docker/compose/services/trogon-source-discord/Dockerfile
  • rsworkspace/crates/trogon-source-discord/src/constants.rs
  • rsworkspace/crates/trogon-source-discord/src/server.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • rsworkspace/crates/acp-telemetry/src/service_name.rs
  • devops/docker/compose/.env.example
  • devops/docker/compose/compose.yml
  • rsworkspace/crates/trogon-source-discord/src/main.rs

@yordis yordis force-pushed the yordis/trogon-source-discord branch from 50a4735 to af3ba04 Compare April 5, 2026 23:32
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.

🧹 Nitpick comments (1)
rsworkspace/crates/trogon-source-discord/src/config.rs (1)

81-86: Inconsistent empty-string handling for optional fields.

bot_token and public_key_hex correctly filter empty strings (lines 47, 63), but subject_prefix and stream_name do not. If a user explicitly sets DISCORD_SUBJECT_PREFIX="", the empty string is accepted rather than falling back to the default, which could cause cryptic NATS errors at runtime.

🔧 Proposed fix to filter empty strings consistently
             subject_prefix: env
                 .var("DISCORD_SUBJECT_PREFIX")
+                .ok()
+                .filter(|s| !s.is_empty())
-                .unwrap_or_else(|_| DEFAULT_SUBJECT_PREFIX.to_string()),
+                .unwrap_or_else(|| DEFAULT_SUBJECT_PREFIX.to_string()),
             stream_name: env
                 .var("DISCORD_STREAM_NAME")
+                .ok()
+                .filter(|s| !s.is_empty())
-                .unwrap_or_else(|_| DEFAULT_STREAM_NAME.to_string()),
+                .unwrap_or_else(|| DEFAULT_STREAM_NAME.to_string()),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-discord/src/config.rs` around lines 81 - 86,
subject_prefix and stream_name accept explicit empty strings from the
environment; update their parsing to mirror bot_token/public_key_hex by
filtering out empty values and falling back to
DEFAULT_SUBJECT_PREFIX/DEFAULT_STREAM_NAME. Locate the code that sets
subject_prefix and stream_name in config.rs and replace the direct
unwrap_or_else(...) with a pattern that reads the env var, maps/ok() it, filters
out empty strings (e.g., .ok().filter(|s| !s.is_empty())), and then unwraps to
the default constants DEFAULT_SUBJECT_PREFIX and DEFAULT_STREAM_NAME so empty
env values behave like missing ones.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@rsworkspace/crates/trogon-source-discord/src/config.rs`:
- Around line 81-86: subject_prefix and stream_name accept explicit empty
strings from the environment; update their parsing to mirror
bot_token/public_key_hex by filtering out empty values and falling back to
DEFAULT_SUBJECT_PREFIX/DEFAULT_STREAM_NAME. Locate the code that sets
subject_prefix and stream_name in config.rs and replace the direct
unwrap_or_else(...) with a pattern that reads the env var, maps/ok() it, filters
out empty strings (e.g., .ok().filter(|s| !s.is_empty())), and then unwraps to
the default constants DEFAULT_SUBJECT_PREFIX and DEFAULT_STREAM_NAME so empty
env values behave like missing ones.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 53e376c9-79c4-4dba-aac2-9226cbbabdf7

📥 Commits

Reviewing files that changed from the base of the PR and between 50a4735 and af3ba04.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • devops/docker/compose/.env.example
  • devops/docker/compose/compose.yml
  • devops/docker/compose/services/trogon-source-discord/Dockerfile
  • devops/docker/compose/services/trogon-source-discord/README.md
  • rsworkspace/crates/acp-telemetry/src/service_name.rs
  • rsworkspace/crates/trogon-source-discord/Cargo.toml
  • rsworkspace/crates/trogon-source-discord/src/config.rs
  • rsworkspace/crates/trogon-source-discord/src/constants.rs
  • rsworkspace/crates/trogon-source-discord/src/gateway.rs
  • rsworkspace/crates/trogon-source-discord/src/lib.rs
  • rsworkspace/crates/trogon-source-discord/src/main.rs
  • rsworkspace/crates/trogon-source-discord/src/server.rs
  • rsworkspace/crates/trogon-source-discord/src/signature.rs
✅ Files skipped from review due to trivial changes (7)
  • devops/docker/compose/.env.example
  • devops/docker/compose/services/trogon-source-discord/Dockerfile
  • rsworkspace/crates/trogon-source-discord/Cargo.toml
  • devops/docker/compose/services/trogon-source-discord/README.md
  • rsworkspace/crates/trogon-source-discord/src/signature.rs
  • rsworkspace/crates/trogon-source-discord/src/main.rs
  • rsworkspace/crates/trogon-source-discord/src/constants.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • rsworkspace/crates/acp-telemetry/src/service_name.rs
  • rsworkspace/crates/trogon-source-discord/src/lib.rs
  • devops/docker/compose/compose.yml
  • rsworkspace/crates/trogon-source-discord/src/server.rs
  • rsworkspace/crates/trogon-source-discord/src/gateway.rs

@yordis yordis force-pushed the yordis/trogon-source-discord branch from af3ba04 to 2e2e20b Compare April 5, 2026 23:36
@yordis yordis force-pushed the yordis/trogon-source-discord branch 4 times, most recently from 2bb5fab to 0668af3 Compare April 6, 2026 01:46
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: 1

♻️ Duplicate comments (1)
rsworkspace/crates/trogon-source-discord/src/gateway.rs (1)

54-55: ⚠️ Potential issue | 🟠 Major

Gateway intake is still blocked on JetStream ACK round-trips.

Line 54 and Line 77 await publishing/ACK on the hot event path; combined with bridge.dispatch(&event).await in the shard loop, broker slowness can stall gateway event processing up to nats_ack_timeout per event.

#!/bin/bash
set -euo pipefail

gw="$(fd -p 'gateway.rs' | rg 'trogon-source-discord/src/gateway.rs$')"
mainf="$(fd -p 'main.rs' | rg 'trogon-source-discord/src/main.rs$')"
pubf="$(fd -p 'publish.rs' | rg 'trogon-nats/src/jetstream/publish.rs$')"

echo "=== gateway dispatch/publish await chain ==="
rg -n -C2 'dispatch\(|publish_bytes\(|publish_event\(' "$gw"

echo
echo "=== gateway event loop call site ==="
rg -n -C2 'next_event|dispatch\(&event\)\.await' "$mainf"

echo
echo "=== publish_event ack timeout behavior ==="
rg -n -C3 'publish_with_headers|timeout\(ack_timeout' "$pubf"

Also applies to: 77-84

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

In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs` around lines 54 -
55, The gateway is blocked because the shard loop awaits publish_bytes (and
similarly the other publish path), which ties broker ACK latency into
bridge.dispatch(&event).await; change the hot-path publish to fire-and-forget:
spawn a detached task (e.g., tokio::spawn) that calls publish_bytes
asynchronously and logs any error instead of awaiting the ACK inline; ensure you
clone/move required values (name, guild_id, dedup_id, json or a prepared Bytes)
or the client handle (e.g., Arc/clone of the publisher) so the spawned task does
not borrow the shard loop or self, and leave bridge.dispatch(&event).await
unchanged so gateway intake is no longer blocked by JetStream ACKs.
🧹 Nitpick comments (1)
rsworkspace/crates/trogon-source-discord/src/signature.rs (1)

7-11: Consider renaming InvalidSignatureHex to InvalidHex for semantic accuracy.

This variant is reused in parse_public_key() (line 60) for public key hex decoding errors, but its name and display message ("invalid hex in signature") are signature-specific. This could confuse callers who see a signature-related error when parsing a public key.

♻️ Suggested refactor
 pub enum SignatureError {
     InvalidPublicKey,
-    InvalidSignatureHex(hex::FromHexError),
+    InvalidHex(hex::FromHexError),
     InvalidSignatureLength,
     Mismatch,
 }

And update the Display impl accordingly:

-            SignatureError::InvalidSignatureHex(_) => f.write_str("invalid hex in signature"),
+            SignatureError::InvalidHex(_) => f.write_str("invalid hex encoding"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-discord/src/signature.rs` around lines 7 -
11, The enum variant name InvalidSignatureHex in SignatureError is misleading
because it’s also used for public-key hex decoding in parse_public_key(); rename
the variant to InvalidHex (and update any pattern matches) and update the
Display implementation to use a generic message (e.g., "invalid hex") rather
than "invalid hex in signature" so both signature and public-key hex decode
errors are accurately represented; ensure you update all references to
SignatureError::InvalidSignatureHex (including in parse_public_key and any
tests) to SignatureError::InvalidHex.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs`:
- Around line 301-304: The current deduplication uses dedup_key2(event_name,
guild_id, user.id) for lifecycle events (Event::MemberAdd, Event::MemberRemove,
Event::BanAdd, Event::BanRemove) which can erroneously drop legitimate repeat
events; change the logic in the match/handler that generates the NATS-Msg-Id so
these lifecycle events do not call dedup_key2 and instead return no dedup key
(or a truly unique key if you can derive one per event) for
MemberAdd/MemberRemove/BanAdd/BanRemove; update the branch handling these
variants in the function that builds the dedup key (where dedup_key2 is
currently invoked) to skip deduplication for those event variants.

---

Duplicate comments:
In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs`:
- Around line 54-55: The gateway is blocked because the shard loop awaits
publish_bytes (and similarly the other publish path), which ties broker ACK
latency into bridge.dispatch(&event).await; change the hot-path publish to
fire-and-forget: spawn a detached task (e.g., tokio::spawn) that calls
publish_bytes asynchronously and logs any error instead of awaiting the ACK
inline; ensure you clone/move required values (name, guild_id, dedup_id, json or
a prepared Bytes) or the client handle (e.g., Arc/clone of the publisher) so the
spawned task does not borrow the shard loop or self, and leave
bridge.dispatch(&event).await unchanged so gateway intake is no longer blocked
by JetStream ACKs.

---

Nitpick comments:
In `@rsworkspace/crates/trogon-source-discord/src/signature.rs`:
- Around line 7-11: The enum variant name InvalidSignatureHex in SignatureError
is misleading because it’s also used for public-key hex decoding in
parse_public_key(); rename the variant to InvalidHex (and update any pattern
matches) and update the Display implementation to use a generic message (e.g.,
"invalid hex") rather than "invalid hex in signature" so both signature and
public-key hex decode errors are accurately represented; ensure you update all
references to SignatureError::InvalidSignatureHex (including in parse_public_key
and any tests) to SignatureError::InvalidHex.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a46c8889-3725-401f-bca8-43a6570d60b3

📥 Commits

Reviewing files that changed from the base of the PR and between af3ba04 and c5ff00f.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • devops/docker/compose/.env.example
  • devops/docker/compose/compose.yml
  • devops/docker/compose/services/trogon-source-discord/Dockerfile
  • devops/docker/compose/services/trogon-source-discord/README.md
  • rsworkspace/Cargo.toml
  • rsworkspace/crates/acp-telemetry/src/service_name.rs
  • rsworkspace/crates/trogon-source-discord/Cargo.toml
  • rsworkspace/crates/trogon-source-discord/src/config.rs
  • rsworkspace/crates/trogon-source-discord/src/constants.rs
  • rsworkspace/crates/trogon-source-discord/src/gateway.rs
  • rsworkspace/crates/trogon-source-discord/src/lib.rs
  • rsworkspace/crates/trogon-source-discord/src/main.rs
  • rsworkspace/crates/trogon-source-discord/src/server.rs
  • rsworkspace/crates/trogon-source-discord/src/signature.rs
✅ Files skipped from review due to trivial changes (9)
  • rsworkspace/Cargo.toml
  • devops/docker/compose/.env.example
  • devops/docker/compose/services/trogon-source-discord/Dockerfile
  • rsworkspace/crates/trogon-source-discord/Cargo.toml
  • rsworkspace/crates/trogon-source-discord/src/lib.rs
  • devops/docker/compose/services/trogon-source-discord/README.md
  • rsworkspace/crates/trogon-source-discord/src/constants.rs
  • rsworkspace/crates/trogon-source-discord/src/server.rs
  • rsworkspace/crates/trogon-source-discord/src/config.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • rsworkspace/crates/acp-telemetry/src/service_name.rs
  • devops/docker/compose/compose.yml
  • rsworkspace/crates/trogon-source-discord/src/main.rs

@yordis yordis force-pushed the yordis/trogon-source-discord branch from 0dfcf98 to ea3af55 Compare April 6, 2026 02:23
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.

♻️ Duplicate comments (1)
rsworkspace/crates/trogon-source-discord/src/gateway.rs (1)

50-51: ⚠️ Potential issue | 🟠 Major

Don’t wait for JetStream ACKs on the gateway hot path.

dispatch() blocks until publish_event() finishes its ACK round-trip (or timeout). Since this bridge forwards every gateway event, one slow NATS ACK will backpressure the entire Discord feed. Hand the payload off to a bounded channel and let a background publisher task own the JetStream publish/ACK path instead.

Also applies to: 73-80

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

In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs` around lines 50 -
51, The gateway currently awaits JetStream ACKs on the hot path (calls to
self.publish_bytes(...) from dispatch()), which blocks dispatch() and causes
backpressure; instead create a bounded mpsc channel and a background publisher
task (e.g., spawn a publisher owning the JetStream client used by
publish_bytes/publish_event) and change dispatch() to enqueue the Bytes payload
+ metadata (name, guild_id, dedup_id) into that channel without awaiting ACKs;
apply the same change to the other hot-path call sites (the similar calls around
lines 73-80) so those places also send to the channel, and ensure the background
task performs the actual publish_bytes/publish_event calls and handles
ACKs/retries/timeouts there.
🧹 Nitpick comments (1)
rsworkspace/crates/trogon-source-discord/src/gateway.rs (1)

468-543: Please add a regression test for the member/ban dedup carve-out.

The new extract_dedup_id() branch is the subtle part of this change, but the suite still only proves generic msg-id present/absent behavior. A targeted dispatch() case for MemberAdd / MemberRemove / BanAdd / BanRemove would lock in the false-collision fix.

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

In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs` around lines 468 -
543, Add a regression test that exercises the new extract_dedup_id() branch by
calling bridge_with_mock() and b.dispatch() with Event::MemberAdd,
Event::MemberRemove, Event::BanAdd, and Event::BanRemove and asserting that
their published NATS message IDs
(headers.get(async_nats::header::NATS_MESSAGE_ID)) are either omitted or unique
per the dedup carve-out; locate this behavior around the dispatch() function and
the extract_dedup_id() logic and add assertions similar to the existing
publish/dispatch tests to ensure no false collisions occur for member/ban
events.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs`:
- Around line 50-51: The gateway currently awaits JetStream ACKs on the hot path
(calls to self.publish_bytes(...) from dispatch()), which blocks dispatch() and
causes backpressure; instead create a bounded mpsc channel and a background
publisher task (e.g., spawn a publisher owning the JetStream client used by
publish_bytes/publish_event) and change dispatch() to enqueue the Bytes payload
+ metadata (name, guild_id, dedup_id) into that channel without awaiting ACKs;
apply the same change to the other hot-path call sites (the similar calls around
lines 73-80) so those places also send to the channel, and ensure the background
task performs the actual publish_bytes/publish_event calls and handles
ACKs/retries/timeouts there.

---

Nitpick comments:
In `@rsworkspace/crates/trogon-source-discord/src/gateway.rs`:
- Around line 468-543: Add a regression test that exercises the new
extract_dedup_id() branch by calling bridge_with_mock() and b.dispatch() with
Event::MemberAdd, Event::MemberRemove, Event::BanAdd, and Event::BanRemove and
asserting that their published NATS message IDs
(headers.get(async_nats::header::NATS_MESSAGE_ID)) are either omitted or unique
per the dedup carve-out; locate this behavior around the dispatch() function and
the extract_dedup_id() logic and add assertions similar to the existing
publish/dispatch tests to ensure no false collisions occur for member/ban
events.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b034b71-92a7-4782-9e51-0723aec3f48d

📥 Commits

Reviewing files that changed from the base of the PR and between c5ff00f and ea3af55.

📒 Files selected for processing (2)
  • rsworkspace/crates/trogon-source-discord/src/config.rs
  • rsworkspace/crates/trogon-source-discord/src/gateway.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rsworkspace/crates/trogon-source-discord/src/config.rs

@yordis yordis force-pushed the yordis/trogon-source-discord branch 2 times, most recently from bfec406 to 6c42d35 Compare April 6, 2026 02:43
@yordis yordis force-pushed the yordis/trogon-source-discord branch 7 times, most recently from 8511f18 to 26f462b Compare April 6, 2026 05:03
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 26f462b. Configure here.

@yordis yordis force-pushed the yordis/trogon-source-discord branch from 26f462b to 91c4b29 Compare April 6, 2026 05:09
…t-reply

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis force-pushed the yordis/trogon-source-discord branch from 91c4b29 to fa6ca2f Compare April 6, 2026 05:13
@yordis yordis added the rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline label Apr 6, 2026
@yordis yordis merged commit efe6f29 into main Apr 6, 2026
9 of 10 checks passed
@yordis yordis deleted the yordis/trogon-source-discord branch April 6, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant