mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 16:47:29 +00:00
Discord: add gateway proxy docs and tests (#10400) (thanks @winter-loo)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -330,6 +330,37 @@ See [Slash commands](/tools/slash-commands) for command catalog and behavior.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Gateway proxy">
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="PluralKit support">
|
||||
Enable PluralKit resolution to map proxied messages to system member identity:
|
||||
|
||||
|
||||
@@ -293,6 +293,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"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.<id>.proxy.",
|
||||
"channels.whatsapp.configWrites":
|
||||
"Allow WhatsApp to write config in response to channel events/commands (default: true).",
|
||||
"channels.signal.configWrites":
|
||||
|
||||
105
src/discord/monitor/provider.proxy.test.ts
Normal file
105
src/discord/monitor/provider.proxy.test.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user