Skip to content

Commit 9a23880

Browse files
committed
feat(trogon-source-discord): add Discord interaction webhook receiver
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 430b2db commit 9a23880

12 files changed

Lines changed: 1621 additions & 0 deletions

File tree

devops/docker/compose/compose.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,31 @@ services:
6767
start_period: 10s
6868
retries: 3
6969

70+
trogon-source-discord:
71+
build:
72+
context: ../../../rsworkspace
73+
dockerfile: ../devops/docker/compose/services/trogon-source-discord/Dockerfile
74+
environment:
75+
NATS_URL: "nats:4222"
76+
DISCORD_PUBLIC_KEY: "${DISCORD_PUBLIC_KEY:-}"
77+
DISCORD_WEBHOOK_PORT: "${DISCORD_WEBHOOK_PORT:-8080}"
78+
DISCORD_SUBJECT_PREFIX: "${DISCORD_SUBJECT_PREFIX:-discord}"
79+
DISCORD_STREAM_NAME: "${DISCORD_STREAM_NAME:-DISCORD}"
80+
DISCORD_STREAM_MAX_AGE_SECS: "${DISCORD_STREAM_MAX_AGE_SECS:-604800}"
81+
DISCORD_NATS_ACK_TIMEOUT_SECS: "${DISCORD_NATS_ACK_TIMEOUT_SECS:-10}"
82+
DISCORD_MAX_BODY_SIZE: "${DISCORD_MAX_BODY_SIZE:-4194304}"
83+
RUST_LOG: "${RUST_LOG:-info}"
84+
depends_on:
85+
nats:
86+
condition: service_healthy
87+
restart: unless-stopped
88+
healthcheck:
89+
test: ["CMD", "curl", "-sf", "http://localhost:${DISCORD_WEBHOOK_PORT:-8080}/health"]
90+
interval: 10s
91+
timeout: 3s
92+
start_period: 10s
93+
retries: 3
94+
7095
trogon-source-slack:
7196
build:
7297
context: ../../../rsworkspace
@@ -97,6 +122,7 @@ services:
97122
image: ngrok/ngrok:alpine
98123
environment:
99124
NGROK_AUTHTOKEN: "${NGROK_AUTHTOKEN:-}"
125+
DISCORD_ADDR: "trogon-source-discord:${DISCORD_WEBHOOK_PORT:-8080}"
100126
GITHUB_ADDR: "trogon-source-github:${GITHUB_WEBHOOK_PORT:-8080}"
101127
LINEAR_ADDR: "trogon-source-linear:${LINEAR_WEBHOOK_PORT:-8080}"
102128
SLACK_ADDR: "trogon-source-slack:${SLACK_WEBHOOK_PORT:-3000}"
@@ -107,6 +133,9 @@ services:
107133
cat > /tmp/ngrok.yml <<EOF
108134
version: 3
109135
tunnels:
136+
discord:
137+
addr: $${DISCORD_ADDR}
138+
proto: http
110139
github:
111140
addr: $${GITHUB_ADDR}
112141
proto: http
@@ -119,6 +148,8 @@ services:
119148
EOF
120149
exec ngrok start --all --config /tmp/ngrok.yml
121150
depends_on:
151+
trogon-source-discord:
152+
condition: service_healthy
122153
trogon-source-github:
123154
condition: service_healthy
124155
trogon-source-linear:
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# ── Stage 1: chef — generate dependency recipe ──────────────────────────────
2+
FROM rust:1.93-slim AS chef
3+
4+
RUN cargo install cargo-chef --locked
5+
6+
WORKDIR /build
7+
8+
# ── Stage 2: planner — capture dependency graph ─────────────────────────────
9+
FROM chef AS planner
10+
11+
COPY Cargo.toml Cargo.lock ./
12+
COPY crates/ crates/
13+
14+
RUN cargo chef prepare --recipe-path recipe.json
15+
16+
# ── Stage 3: builder — cached dependency build + final compile ──────────────
17+
FROM chef AS builder
18+
19+
COPY --from=planner /build/recipe.json recipe.json
20+
RUN cargo chef cook --release --recipe-path recipe.json -p trogon-source-discord
21+
22+
COPY Cargo.toml Cargo.lock ./
23+
COPY crates/ crates/
24+
25+
RUN cargo build --release -p trogon-source-discord && \
26+
strip target/release/trogon-source-discord
27+
28+
# ── Stage 4: runtime ────────────────────────────────────────────────────────
29+
FROM debian:bookworm-20250317-slim AS runtime
30+
31+
RUN apt-get update && apt-get install -y --no-install-recommends \
32+
ca-certificates curl \
33+
&& rm -rf /var/lib/apt/lists/*
34+
35+
RUN useradd --no-create-home --shell /usr/sbin/nologin trogon
36+
37+
COPY --from=builder /build/target/release/trogon-source-discord /usr/local/bin/trogon-source-discord
38+
39+
USER trogon
40+
41+
EXPOSE 8080
42+
43+
STOPSIGNAL SIGTERM
44+
45+
ENTRYPOINT ["/usr/local/bin/trogon-source-discord"]
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Receiving Discord Interactions Locally
2+
3+
## Prerequisites
4+
5+
- Docker Compose
6+
- A [Discord Application](https://discord.com/developers/applications) with an
7+
Interactions Endpoint configured
8+
- An [ngrok](https://ngrok.com) account (free tier works)
9+
10+
## 1. Get your public key
11+
12+
1. Go to **Discord Developer Portal → Applications → your app → General Information**
13+
2. Copy the **Public Key** (hex string)
14+
15+
## 2. Start the stack
16+
17+
```bash
18+
DISCORD_PUBLIC_KEY=<your-hex-public-key> docker compose --profile dev up
19+
```
20+
21+
This starts NATS, the webhook receiver, and ngrok. Find the public tunnel URL
22+
in the ngrok container logs:
23+
24+
```bash
25+
docker compose logs ngrok
26+
```
27+
28+
Look for the `discord` tunnel URL (e.g. `https://abc123.ngrok-free.app`).
29+
30+
## 3. Configure your Discord Application
31+
32+
1. Go to **Discord Developer Portal → Applications → your app → General Information**
33+
2. Set **Interactions Endpoint URL** to `https://<ngrok-url>/webhook`
34+
3. Discord will send a PING to verify the endpoint — the receiver handles this
35+
automatically
36+
37+
## 4. Verify
38+
39+
Create a slash command or trigger an interaction. You should see:
40+
41+
- The webhook receiver log the incoming interaction
42+
- The interaction published to NATS on `discord.{interaction_type}`
43+
44+
You can inspect NATS with:
45+
46+
```bash
47+
nats sub -s nats://nats.trogonai.orb.local:4222 "discord.>"
48+
```
49+
50+
Without `--profile dev`, ngrok is excluded and only the core services start.
51+
52+
## Environment variables
53+
54+
| Variable | Required | Default | Description |
55+
|---|---|---|---|
56+
| `DISCORD_PUBLIC_KEY` | yes || Ed25519 public key (hex) from Discord app settings |
57+
| `NGROK_AUTHTOKEN` | yes (dev profile) || ngrok auth token |
58+
| `DISCORD_WEBHOOK_PORT` | no | `8080` | HTTP port for the webhook receiver |
59+
| `DISCORD_SUBJECT_PREFIX` | no | `discord` | NATS subject prefix |
60+
| `DISCORD_STREAM_NAME` | no | `DISCORD` | JetStream stream name |
61+
| `RUST_LOG` | no | `info` | Log level |

rsworkspace/Cargo.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rsworkspace/crates/acp-telemetry/src/service_name.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
pub enum ServiceName {
66
AcpNatsStdio,
77
AcpNatsWs,
8+
TrogonSourceDiscord,
89
TrogonSourceGithub,
910
TrogonSourceLinear,
1011
TrogonSourceSlack,
@@ -15,6 +16,7 @@ impl ServiceName {
1516
match self {
1617
Self::AcpNatsStdio => "acp-nats-stdio",
1718
Self::AcpNatsWs => "acp-nats-ws",
19+
Self::TrogonSourceDiscord => "trogon-source-discord",
1820
Self::TrogonSourceGithub => "trogon-source-github",
1921
Self::TrogonSourceLinear => "trogon-source-linear",
2022
Self::TrogonSourceSlack => "trogon-source-slack",
@@ -36,6 +38,10 @@ mod tests {
3638
fn as_str_returns_expected_values() {
3739
assert_eq!(ServiceName::AcpNatsStdio.as_str(), "acp-nats-stdio");
3840
assert_eq!(ServiceName::AcpNatsWs.as_str(), "acp-nats-ws");
41+
assert_eq!(
42+
ServiceName::TrogonSourceDiscord.as_str(),
43+
"trogon-source-discord"
44+
);
3945
assert_eq!(
4046
ServiceName::TrogonSourceGithub.as_str(),
4147
"trogon-source-github"
@@ -54,6 +60,10 @@ mod tests {
5460
fn display_delegates_to_as_str() {
5561
assert_eq!(format!("{}", ServiceName::AcpNatsStdio), "acp-nats-stdio");
5662
assert_eq!(format!("{}", ServiceName::AcpNatsWs), "acp-nats-ws");
63+
assert_eq!(
64+
format!("{}", ServiceName::TrogonSourceDiscord),
65+
"trogon-source-discord"
66+
);
5767
assert_eq!(
5868
format!("{}", ServiceName::TrogonSourceGithub),
5969
"trogon-source-github"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "trogon-source-discord"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lints]
7+
workspace = true
8+
9+
[[bin]]
10+
name = "trogon-source-discord"
11+
path = "src/main.rs"
12+
13+
[dependencies]
14+
acp-telemetry = { workspace = true }
15+
async-nats = { workspace = true, features = ["jetstream"] }
16+
axum = { workspace = true }
17+
bytes = { workspace = true }
18+
bytesize = "2.3.1"
19+
ed25519-dalek = { version = "2.1", features = ["std"] }
20+
hex = "0.4"
21+
serde_json = { workspace = true }
22+
tokio = { workspace = true, features = ["full"] }
23+
tower-http = { workspace = true, features = ["limit"] }
24+
tracing = { workspace = true }
25+
trogon-nats = { workspace = true }
26+
trogon-std = { workspace = true }
27+
28+
[dev-dependencies]
29+
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
30+
tower = "0.5"
31+
tracing-subscriber = { workspace = true }
32+
trogon-nats = { workspace = true, features = ["test-support"] }
33+
trogon-std = { workspace = true, features = ["test-support"] }

0 commit comments

Comments
 (0)