From 5645f227f6e7822121bb40b01ac8eadba1738308 Mon Sep 17 00:00:00 2001 From: Shadow Date: Fri, 13 Feb 2026 13:14:19 -0600 Subject: [PATCH] Discord: add gateway proxy docs and tests (#10400) (thanks @winter-loo) --- CHANGELOG.md | 1 + docs/channels/discord.md | 31 ++++++ src/config/schema.help.ts | 2 + src/discord/monitor/provider.proxy.test.ts | 105 +++++++++++++++++++++ src/discord/monitor/provider.ts | 9 +- 5 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 src/discord/monitor/provider.proxy.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d88975b75..34fe13e837 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,7 @@ Docs: https://docs.openclaw.ai - Discord: process DM reactions instead of silently dropping them. (#10418) Thanks @mcaxtr. - Discord: treat Administrator as full permissions in channel permission checks. Thanks @thewilloftheshadow. - Discord: respect replyToMode in threads. (#11062) Thanks @cordx56. +- Discord: add optional gateway proxy support for WebSocket connections via `channels.discord.proxy`. (#10400) Thanks @winter-loo, @thewilloftheshadow. - Browser: add Chrome launch flag `--disable-blink-features=AutomationControlled` to reduce `navigator.webdriver` automation detection issues on reCAPTCHA-protected sites. (#10735) Thanks @Milofax. - Heartbeat: filter noise-only system events so scheduled reminder notifications do not fire when cron runs carry only heartbeat markers. (#13317) Thanks @pvtclawn. - Signal: render mention placeholders as `@uuid`/`@phone` so mention gating and Clawdbot targeting work. (#2013) Thanks @alexgleason. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 358deeac23..e55b03a10f 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -330,6 +330,37 @@ See [Slash commands](/tools/slash-commands) for command catalog and behavior. + + Route Discord gateway WebSocket traffic through an HTTP(S) proxy with `channels.discord.proxy`. + +```json5 +{ + channels: { + discord: { + proxy: "http://proxy.example:8080", + }, + }, +} +``` + + Per-account override: + +```json5 +{ + channels: { + discord: { + accounts: { + primary: { + proxy: "http://proxy.example:8080", + }, + }, + }, + }, +} +``` + + + Enable PluralKit resolution to map proxied messages to system member identity: diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 222cd7f454..52841428c0 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -293,6 +293,8 @@ export const FIELD_HELP: Record = { "Allow Mattermost to write config in response to channel events/commands (default: true).", "channels.discord.configWrites": "Allow Discord to write config in response to channel events/commands (default: true).", + "channels.discord.proxy": + "Proxy URL for Discord gateway WebSocket connections. Set per account via channels.discord.accounts..proxy.", "channels.whatsapp.configWrites": "Allow WhatsApp to write config in response to channel events/commands (default: true).", "channels.signal.configWrites": diff --git a/src/discord/monitor/provider.proxy.test.ts b/src/discord/monitor/provider.proxy.test.ts new file mode 100644 index 0000000000..caed864629 --- /dev/null +++ b/src/discord/monitor/provider.proxy.test.ts @@ -0,0 +1,105 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { HttpsProxyAgent, getLastAgent, proxyAgentSpy, resetLastAgent, webSocketSpy } = vi.hoisted( + () => { + const proxyAgentSpy = vi.fn(); + const webSocketSpy = vi.fn(); + + class HttpsProxyAgent { + static lastCreated: HttpsProxyAgent | undefined; + proxyUrl: string; + constructor(proxyUrl: string) { + if (proxyUrl === "bad-proxy") { + throw new Error("bad proxy"); + } + this.proxyUrl = proxyUrl; + HttpsProxyAgent.lastCreated = this; + proxyAgentSpy(proxyUrl); + } + } + + return { + HttpsProxyAgent, + getLastAgent: () => HttpsProxyAgent.lastCreated, + proxyAgentSpy, + resetLastAgent: () => { + HttpsProxyAgent.lastCreated = undefined; + }, + webSocketSpy, + }; + }, +); + +vi.mock("https-proxy-agent", () => ({ + HttpsProxyAgent, +})); + +vi.mock("ws", () => ({ + default: class MockWebSocket { + constructor(url: string, options?: { agent?: unknown }) { + webSocketSpy(url, options); + } + }, +})); + +describe("createDiscordGatewayPlugin", () => { + beforeEach(() => { + proxyAgentSpy.mockReset(); + webSocketSpy.mockReset(); + resetLastAgent(); + }); + + it("uses proxy agent for gateway WebSocket when configured", async () => { + const { __testing } = await import("./provider.js"); + const { GatewayPlugin } = await import("@buape/carbon/gateway"); + + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(() => { + throw new Error("exit"); + }), + }; + + const plugin = __testing.createDiscordGatewayPlugin({ + discordConfig: { proxy: "http://proxy.test:8080" }, + runtime, + }); + + expect(Object.getPrototypeOf(plugin)).not.toBe(GatewayPlugin.prototype); + + const createWebSocket = (plugin as unknown as { createWebSocket: (url: string) => unknown }) + .createWebSocket; + createWebSocket("wss://gateway.discord.gg"); + + expect(proxyAgentSpy).toHaveBeenCalledWith("http://proxy.test:8080"); + expect(webSocketSpy).toHaveBeenCalledWith( + "wss://gateway.discord.gg", + expect.objectContaining({ agent: getLastAgent() }), + ); + expect(runtime.log).toHaveBeenCalledWith("discord: gateway proxy enabled"); + expect(runtime.error).not.toHaveBeenCalled(); + }); + + it("falls back to the default gateway plugin when proxy is invalid", async () => { + const { __testing } = await import("./provider.js"); + const { GatewayPlugin } = await import("@buape/carbon/gateway"); + + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(() => { + throw new Error("exit"); + }), + }; + + const plugin = __testing.createDiscordGatewayPlugin({ + discordConfig: { proxy: "bad-proxy" }, + runtime, + }); + + expect(Object.getPrototypeOf(plugin)).toBe(GatewayPlugin.prototype); + expect(runtime.error).toHaveBeenCalled(); + expect(runtime.log).not.toHaveBeenCalled(); + }); +}); diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index 7cf384940e..24391c1731 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -85,10 +85,7 @@ function createDiscordGatewayPlugin(params: { this.#proxyAgent = proxyAgent; } - createWebSocket(url?: string) { - if (!url) { - throw new Error("Gateway URL is required"); - } + createWebSocket(url: string) { return new WebSocket(url, { agent: this.#proxyAgent }); } } @@ -753,3 +750,7 @@ async function clearDiscordNativeCommands(params: { params.runtime.error?.(danger(`discord: failed to clear native commands: ${String(err)}`)); } } + +export const __testing = { + createDiscordGatewayPlugin, +};