mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 19:18:26 +00: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":
|
"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":
|
"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":
|
"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":
|
||||||
|
|||||||
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";
|
} from "@buape/carbon";
|
||||||
import { Routes } from "discord-api-types/v10";
|
import { Routes } from "discord-api-types/v10";
|
||||||
import { inspect } from "node:util";
|
import { inspect } from "node:util";
|
||||||
|
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
||||||
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
||||||
import type { OpenClawConfig, ReplyToMode } from "../../config/config.js";
|
import type { OpenClawConfig, ReplyToMode } from "../../config/config.js";
|
||||||
import { resolveTextChunkLimit } from "../../auto-reply/chunk.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 { createDiscordRetryRunner } from "../../infra/retry-policy.js";
|
||||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||||
import { createNonExitingRuntime, type RuntimeEnv } from "../../runtime.js";
|
import { createNonExitingRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||||
|
import { wrapFetchWithAbortSignal } from "../../infra/fetch.js";
|
||||||
import { resolveDiscordAccount } from "../accounts.js";
|
import { resolveDiscordAccount } from "../accounts.js";
|
||||||
import { attachDiscordGatewayLogging } from "../gateway-logging.js";
|
import { attachDiscordGatewayLogging } from "../gateway-logging.js";
|
||||||
import { getDiscordGatewayEmitter, waitForDiscordGatewayStop } from "../monitor.gateway.js";
|
import { getDiscordGatewayEmitter, waitForDiscordGatewayStop } from "../monitor.gateway.js";
|
||||||
@@ -116,6 +118,26 @@ function dedupeSkillCommandsForDiscord(
|
|||||||
return deduped;
|
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: {
|
async function deployDiscordCommands(params: {
|
||||||
client: Client;
|
client: Client;
|
||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
@@ -182,6 +204,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime();
|
const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime();
|
||||||
|
|
||||||
const discordCfg = account.config;
|
const discordCfg = account.config;
|
||||||
|
const discordRestFetch = resolveDiscordRestFetch(discordCfg.proxy, runtime);
|
||||||
const dmConfig = discordCfg.dm;
|
const dmConfig = discordCfg.dm;
|
||||||
let guildEntries = discordCfg.guilds;
|
let guildEntries = discordCfg.guilds;
|
||||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||||
@@ -257,6 +280,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
const resolved = await resolveDiscordChannelAllowlist({
|
const resolved = await resolveDiscordChannelAllowlist({
|
||||||
token,
|
token,
|
||||||
entries: entries.map((entry) => entry.input),
|
entries: entries.map((entry) => entry.input),
|
||||||
|
fetcher: discordRestFetch,
|
||||||
});
|
});
|
||||||
const nextGuilds = { ...guildEntries };
|
const nextGuilds = { ...guildEntries };
|
||||||
const mapping: string[] = [];
|
const mapping: string[] = [];
|
||||||
@@ -313,6 +337,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
const resolvedUsers = await resolveDiscordUserAllowlist({
|
const resolvedUsers = await resolveDiscordUserAllowlist({
|
||||||
token,
|
token,
|
||||||
entries: allowEntries.map((entry) => String(entry)),
|
entries: allowEntries.map((entry) => String(entry)),
|
||||||
|
fetcher: discordRestFetch,
|
||||||
});
|
});
|
||||||
const { mapping, unresolved, additions } = buildAllowlistResolutionSummary(resolvedUsers);
|
const { mapping, unresolved, additions } = buildAllowlistResolutionSummary(resolvedUsers);
|
||||||
allowFrom = mergeAllowlist({ existing: allowFrom, additions });
|
allowFrom = mergeAllowlist({ existing: allowFrom, additions });
|
||||||
@@ -342,6 +367,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
const resolvedUsers = await resolveDiscordUserAllowlist({
|
const resolvedUsers = await resolveDiscordUserAllowlist({
|
||||||
token,
|
token,
|
||||||
entries: Array.from(userEntries),
|
entries: Array.from(userEntries),
|
||||||
|
fetcher: discordRestFetch,
|
||||||
});
|
});
|
||||||
const { resolvedMap, mapping, unresolved } =
|
const { resolvedMap, mapping, unresolved } =
|
||||||
buildAllowlistResolutionSummary(resolvedUsers);
|
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) {
|
if (!applicationId) {
|
||||||
throw new Error("Failed to resolve Discord application id");
|
throw new Error("Failed to resolve Discord application id");
|
||||||
}
|
}
|
||||||
@@ -689,4 +715,5 @@ async function clearDiscordNativeCommands(params: {
|
|||||||
export const __testing = {
|
export const __testing = {
|
||||||
createDiscordGatewayPlugin,
|
createDiscordGatewayPlugin,
|
||||||
dedupeSkillCommandsForDiscord,
|
dedupeSkillCommandsForDiscord,
|
||||||
|
resolveDiscordRestFetch,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user