mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(discord): apply proxy to app-id and allowlist REST lookups
This commit is contained in:
@@ -318,7 +318,7 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"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.",
|
||||
"Proxy URL for Discord gateway + API requests (app-id lookup and allowlist resolution). 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":
|
||||
|
||||
63
src/discord/monitor/provider.rest-proxy.test.ts
Normal file
63
src/discord/monitor/provider.rest-proxy.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { undiciFetchMock, proxyAgentSpy } = vi.hoisted(() => ({
|
||||
undiciFetchMock: vi.fn(),
|
||||
proxyAgentSpy: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("undici", () => {
|
||||
class ProxyAgent {
|
||||
proxyUrl: string;
|
||||
constructor(proxyUrl: string) {
|
||||
if (proxyUrl === "bad-proxy") {
|
||||
throw new Error("bad proxy");
|
||||
}
|
||||
this.proxyUrl = proxyUrl;
|
||||
proxyAgentSpy(proxyUrl);
|
||||
}
|
||||
}
|
||||
return {
|
||||
ProxyAgent,
|
||||
fetch: undiciFetchMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe("resolveDiscordRestFetch", () => {
|
||||
it("uses undici proxy fetch when a proxy URL is configured", async () => {
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as const;
|
||||
undiciFetchMock.mockReset().mockResolvedValue(new Response("ok", { status: 200 }));
|
||||
proxyAgentSpy.mockReset();
|
||||
|
||||
const { __testing } = await import("./provider.js");
|
||||
const fetcher = __testing.resolveDiscordRestFetch("http://proxy.test:8080", runtime);
|
||||
|
||||
await fetcher("https://discord.com/api/v10/oauth2/applications/@me");
|
||||
|
||||
expect(proxyAgentSpy).toHaveBeenCalledWith("http://proxy.test:8080");
|
||||
expect(undiciFetchMock).toHaveBeenCalledWith(
|
||||
"https://discord.com/api/v10/oauth2/applications/@me",
|
||||
expect.objectContaining({
|
||||
dispatcher: expect.objectContaining({ proxyUrl: "http://proxy.test:8080" }),
|
||||
}),
|
||||
);
|
||||
expect(runtime.log).toHaveBeenCalledWith("discord: rest proxy enabled");
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to global fetch when proxy URL is invalid", async () => {
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as const;
|
||||
const { __testing } = await import("./provider.js");
|
||||
|
||||
const fetcher = __testing.resolveDiscordRestFetch("bad-proxy", runtime);
|
||||
|
||||
expect(fetcher).toBe(fetch);
|
||||
expect(runtime.error).toHaveBeenCalled();
|
||||
expect(runtime.log).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@buape/carbon";
|
||||
import { Routes } from "discord-api-types/v10";
|
||||
import { inspect } from "node:util";
|
||||
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
||||
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
||||
import type { OpenClawConfig, ReplyToMode } from "../../config/config.js";
|
||||
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
||||
@@ -31,6 +32,7 @@ import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { createDiscordRetryRunner } from "../../infra/retry-policy.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { createNonExitingRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { wrapFetchWithAbortSignal } from "../../infra/fetch.js";
|
||||
import { resolveDiscordAccount } from "../accounts.js";
|
||||
import { attachDiscordGatewayLogging } from "../gateway-logging.js";
|
||||
import { getDiscordGatewayEmitter, waitForDiscordGatewayStop } from "../monitor.gateway.js";
|
||||
@@ -116,6 +118,26 @@ function dedupeSkillCommandsForDiscord(
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function resolveDiscordRestFetch(proxyUrl: string | undefined, runtime: RuntimeEnv): typeof fetch {
|
||||
const proxy = proxyUrl?.trim();
|
||||
if (!proxy) {
|
||||
return fetch;
|
||||
}
|
||||
try {
|
||||
const agent = new ProxyAgent(proxy);
|
||||
const fetcher = ((input: RequestInfo | URL, init?: RequestInit) =>
|
||||
undiciFetch(input as string | URL, {
|
||||
...(init as Record<string, unknown>),
|
||||
dispatcher: agent,
|
||||
}) as unknown as Promise<Response>) as typeof fetch;
|
||||
runtime.log?.("discord: rest proxy enabled");
|
||||
return wrapFetchWithAbortSignal(fetcher);
|
||||
} catch (err) {
|
||||
runtime.error?.(danger(`discord: invalid rest proxy: ${String(err)}`));
|
||||
return fetch;
|
||||
}
|
||||
}
|
||||
|
||||
async function deployDiscordCommands(params: {
|
||||
client: Client;
|
||||
runtime: RuntimeEnv;
|
||||
@@ -182,6 +204,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime();
|
||||
|
||||
const discordCfg = account.config;
|
||||
const discordRestFetch = resolveDiscordRestFetch(discordCfg.proxy, runtime);
|
||||
const dmConfig = discordCfg.dm;
|
||||
let guildEntries = discordCfg.guilds;
|
||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||
@@ -257,6 +280,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
const resolved = await resolveDiscordChannelAllowlist({
|
||||
token,
|
||||
entries: entries.map((entry) => entry.input),
|
||||
fetcher: discordRestFetch,
|
||||
});
|
||||
const nextGuilds = { ...guildEntries };
|
||||
const mapping: string[] = [];
|
||||
@@ -313,6 +337,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
const resolvedUsers = await resolveDiscordUserAllowlist({
|
||||
token,
|
||||
entries: allowEntries.map((entry) => String(entry)),
|
||||
fetcher: discordRestFetch,
|
||||
});
|
||||
const { mapping, unresolved, additions } = buildAllowlistResolutionSummary(resolvedUsers);
|
||||
allowFrom = mergeAllowlist({ existing: allowFrom, additions });
|
||||
@@ -342,6 +367,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
const resolvedUsers = await resolveDiscordUserAllowlist({
|
||||
token,
|
||||
entries: Array.from(userEntries),
|
||||
fetcher: discordRestFetch,
|
||||
});
|
||||
const { resolvedMap, mapping, unresolved } =
|
||||
buildAllowlistResolutionSummary(resolvedUsers);
|
||||
@@ -383,7 +409,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
const applicationId = await fetchDiscordApplicationId(token, 4000);
|
||||
const applicationId = await fetchDiscordApplicationId(token, 4000, discordRestFetch);
|
||||
if (!applicationId) {
|
||||
throw new Error("Failed to resolve Discord application id");
|
||||
}
|
||||
@@ -689,4 +715,5 @@ async function clearDiscordNativeCommands(params: {
|
||||
export const __testing = {
|
||||
createDiscordGatewayPlugin,
|
||||
dedupeSkillCommandsForDiscord,
|
||||
resolveDiscordRestFetch,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user