mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 17:01:53 +03: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: process DM reactions instead of silently dropping them. (#10418) Thanks @mcaxtr.
|
||||||
- Discord: treat Administrator as full permissions in channel permission checks. Thanks @thewilloftheshadow.
|
- Discord: treat Administrator as full permissions in channel permission checks. Thanks @thewilloftheshadow.
|
||||||
- Discord: respect replyToMode in threads. (#11062) Thanks @cordx56.
|
- 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.
|
- 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.
|
- 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.
|
- 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>
|
||||||
|
|
||||||
|
<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">
|
<Accordion title="PluralKit support">
|
||||||
Enable PluralKit resolution to map proxied messages to system member identity:
|
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).",
|
"Allow Mattermost to write config in response to channel events/commands (default: true).",
|
||||||
"channels.discord.configWrites":
|
"channels.discord.configWrites":
|
||||||
"Allow Discord to write config in response to channel events/commands (default: true).",
|
"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":
|
"channels.whatsapp.configWrites":
|
||||||
"Allow WhatsApp to write config in response to channel events/commands (default: true).",
|
"Allow WhatsApp to write config in response to channel events/commands (default: true).",
|
||||||
"channels.signal.configWrites":
|
"channels.signal.configWrites":
|
||||||
|
|||||||
@@ -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;
|
this.#proxyAgent = proxyAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
createWebSocket(url?: string) {
|
createWebSocket(url: string) {
|
||||||
if (!url) {
|
|
||||||
throw new Error("Gateway URL is required");
|
|
||||||
}
|
|
||||||
return new WebSocket(url, { agent: this.#proxyAgent });
|
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)}`));
|
params.runtime.error?.(danger(`discord: failed to clear native commands: ${String(err)}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const __testing = {
|
||||||
|
createDiscordGatewayPlugin,
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user