mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(discord): land proxy/media/reaction/model-picker regressions
Reimplements core Discord fixes from #25277 #25523 #25575 #25588 #25731 with expanded tests. - thread proxy-aware fetch into inbound attachment/sticker downloads - fetch /gateway/bot via proxy dispatcher before ws connect - wire statusReactions emojis/timing overrides into controller - compact model-picker custom_id keys with backward-compatible parsing Co-authored-by: openperf <openperf@users.noreply.github.com> Co-authored-by: chilu18 <chilu18@users.noreply.github.com> Co-authored-by: Yipsh <Yipsh@users.noreply.github.com> Co-authored-by: lbo728 <lbo728@users.noreply.github.com> Co-authored-by: s1korrrr <s1korrrr@users.noreply.github.com>
This commit is contained in:
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Onboarding/Telegram: keep core-channel onboarding available when plugin registry population is missing by falling back to built-in adapters and continuing wizard setup with actionable recovery guidance. (#25803) Thanks @Suko.
|
||||
- Models/Bedrock auth: normalize additional Bedrock provider aliases (`bedrock`, `aws-bedrock`, `aws_bedrock`, `amazon bedrock`) to canonical `amazon-bedrock`, ensuring auth-mode resolution consistently selects AWS SDK fallback. (#25756) Thanks @fwhite13.
|
||||
- Automation/Subagent/Cron reliability: honor `ANNOUNCE_SKIP` in `sessions_spawn` completion/direct announce flows (no user-visible token leaks), add transient direct-announce retries for channel unavailability (for example WhatsApp listener reconnect windows), and include `cron` in the `coding` tool profile so `/tools/invoke` can execute cron actions when explicitly allowed by gateway policy. (#25800, #25656, #25842, #25813, #25822, #25821) Thanks @astra-fer, @aaajiao, @dwight11232-coder, @kevinWangSheng, @widingmarcus-cyber, and @stakeswky.
|
||||
- Discord/Proxy + reactions + model picker: thread channel proxy fetch into inbound media/sticker downloads, use proxy-aware gateway metadata fetch for WSL/corporate proxy setups, wire `messages.statusReactions.{emojis,timing}` into Discord reaction lifecycle control, and compact model-picker `custom_id` keys to stay under Discord's 100-char limit while keeping backward-compatible parsing. (#25232, #25507, #25564, #25695) Thanks @openperf, @chilu18, @Yipsh, @lbo728, and @s1korrrr.
|
||||
- Discord/Block streaming: restore block-streamed reply delivery by suppressing only reasoning payloads (instead of all `block` payloads), fixing missing Discord replies in `channels.discord.streaming=block` mode. (#25839, #25836, #25792) Thanks @pewallin.
|
||||
- Matrix/Read receipts: send read receipts as soon as Matrix messages arrive (before handler pipeline work), so clients no longer show long-lived unread/sent states while replies are processing. (#25841, #25840) Thanks @joshjhall.
|
||||
- Sandbox/FS bridge: build canonical-path shell scripts with newline separators (not `; ` joins) to avoid POSIX `sh` `do;` syntax errors that broke sandbox file/image read-write operations. (#25737, #25824, #25868) Thanks @DennisGoldfinger and @peteragility.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway";
|
||||
import type { APIGatewayBotInfo } from "discord-api-types/v10";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
||||
import WebSocket from "ws";
|
||||
import type { DiscordAccountConfig } from "../../config/types.js";
|
||||
import { danger } from "../../globals.js";
|
||||
@@ -42,7 +44,8 @@ export function createDiscordGatewayPlugin(params: {
|
||||
}
|
||||
|
||||
try {
|
||||
const agent = new HttpsProxyAgent<string>(proxy);
|
||||
const wsAgent = new HttpsProxyAgent<string>(proxy);
|
||||
const fetchAgent = new ProxyAgent(proxy);
|
||||
|
||||
params.runtime.log?.("discord: gateway proxy enabled");
|
||||
|
||||
@@ -51,8 +54,28 @@ export function createDiscordGatewayPlugin(params: {
|
||||
super(options);
|
||||
}
|
||||
|
||||
createWebSocket(url: string) {
|
||||
return new WebSocket(url, { agent });
|
||||
override async registerClient(client: Parameters<GatewayPlugin["registerClient"]>[0]) {
|
||||
if (!this.gatewayInfo) {
|
||||
try {
|
||||
const response = await undiciFetch("https://discord.com/api/v10/gateway/bot", {
|
||||
headers: {
|
||||
Authorization: `Bot ${client.options.token}`,
|
||||
},
|
||||
dispatcher: fetchAgent,
|
||||
} as Record<string, unknown>);
|
||||
this.gatewayInfo = (await response.json()) as APIGatewayBotInfo;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to get gateway information from Discord: ${error instanceof Error ? error.message : String(error)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
return super.registerClient(client);
|
||||
}
|
||||
|
||||
override createWebSocket(url: string) {
|
||||
return new WebSocket(url, { agent: wsAgent });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -733,5 +733,6 @@ export async function preflightDiscordMessage(
|
||||
canDetectMention,
|
||||
historyEntry,
|
||||
threadBindings: params.threadBindings,
|
||||
discordRestFetch: params.discordRestFetch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export type DiscordMessagePreflightContext = {
|
||||
|
||||
historyEntry?: HistoryEntry;
|
||||
threadBindings: ThreadBindingManager;
|
||||
discordRestFetch?: typeof fetch;
|
||||
};
|
||||
|
||||
export type DiscordMessagePreflightParams = {
|
||||
@@ -106,6 +107,7 @@ export type DiscordMessagePreflightParams = {
|
||||
ackReactionScope: DiscordMessagePreflightContext["ackReactionScope"];
|
||||
groupPolicy: DiscordMessagePreflightContext["groupPolicy"];
|
||||
threadBindings: ThreadBindingManager;
|
||||
discordRestFetch?: typeof fetch;
|
||||
data: DiscordMessageEvent;
|
||||
client: Client;
|
||||
};
|
||||
|
||||
@@ -257,6 +257,35 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
expect(emojis).toContain(DEFAULT_EMOJIS.stallHard);
|
||||
expect(emojis).toContain(DEFAULT_EMOJIS.done);
|
||||
});
|
||||
|
||||
it("applies status reaction emoji/timing overrides from config", async () => {
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onReasoningStream?.();
|
||||
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 0 } };
|
||||
});
|
||||
|
||||
const ctx = await createBaseContext({
|
||||
cfg: {
|
||||
messages: {
|
||||
ackReaction: "👀",
|
||||
statusReactions: {
|
||||
emojis: { queued: "🟦", thinking: "🧪", done: "🏁" },
|
||||
timing: { debounceMs: 0 },
|
||||
},
|
||||
},
|
||||
session: { store: "/tmp/openclaw-discord-process-test-sessions.json" },
|
||||
},
|
||||
});
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
await processDiscordMessage(ctx as any);
|
||||
|
||||
const emojis = (
|
||||
sendMocks.reactMessageDiscord.mock.calls as unknown as Array<[unknown, unknown, string]>
|
||||
).map((call) => call[2]);
|
||||
expect(emojis).toContain("🟦");
|
||||
expect(emojis).toContain("🏁");
|
||||
});
|
||||
});
|
||||
|
||||
describe("processDiscordMessage session routing", () => {
|
||||
|
||||
@@ -101,10 +101,15 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
threadBindings,
|
||||
route,
|
||||
commandAuthorized,
|
||||
discordRestFetch,
|
||||
} = ctx;
|
||||
|
||||
const mediaList = await resolveMediaList(message, mediaMaxBytes);
|
||||
const forwardedMediaList = await resolveForwardedMediaList(message, mediaMaxBytes);
|
||||
const mediaList = await resolveMediaList(message, mediaMaxBytes, discordRestFetch);
|
||||
const forwardedMediaList = await resolveForwardedMediaList(
|
||||
message,
|
||||
mediaMaxBytes,
|
||||
discordRestFetch,
|
||||
);
|
||||
mediaList.push(...forwardedMediaList);
|
||||
const text = messageText;
|
||||
if (!text) {
|
||||
@@ -147,6 +152,8 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
enabled: statusReactionsEnabled,
|
||||
adapter: discordAdapter,
|
||||
initialEmoji: ackReaction,
|
||||
emojis: cfg.messages?.statusReactions?.emojis,
|
||||
timing: cfg.messages?.statusReactions?.timing,
|
||||
onError: (err) => {
|
||||
logAckFailure({
|
||||
log: logVerbose,
|
||||
|
||||
@@ -93,6 +93,7 @@ describe("resolveForwardedMediaList", () => {
|
||||
url: attachment.url,
|
||||
filePathHint: attachment.filename,
|
||||
maxBytes: 512,
|
||||
fetchImpl: undefined,
|
||||
});
|
||||
expect(saveMediaBuffer).toHaveBeenCalledTimes(1);
|
||||
expect(saveMediaBuffer).toHaveBeenCalledWith(expect.any(Buffer), "image/png", "inbound", 512);
|
||||
@@ -105,6 +106,38 @@ describe("resolveForwardedMediaList", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("forwards fetchImpl to forwarded attachment downloads", async () => {
|
||||
const proxyFetch = vi.fn() as unknown as typeof fetch;
|
||||
const attachment = {
|
||||
id: "att-proxy",
|
||||
url: "https://cdn.discordapp.com/attachments/1/proxy.png",
|
||||
filename: "proxy.png",
|
||||
content_type: "image/png",
|
||||
};
|
||||
fetchRemoteMedia.mockResolvedValueOnce({
|
||||
buffer: Buffer.from("image"),
|
||||
contentType: "image/png",
|
||||
});
|
||||
saveMediaBuffer.mockResolvedValueOnce({
|
||||
path: "/tmp/proxy.png",
|
||||
contentType: "image/png",
|
||||
});
|
||||
|
||||
await resolveForwardedMediaList(
|
||||
asMessage({
|
||||
rawData: {
|
||||
message_snapshots: [{ message: { attachments: [attachment] } }],
|
||||
},
|
||||
}),
|
||||
512,
|
||||
proxyFetch,
|
||||
);
|
||||
|
||||
expect(fetchRemoteMedia).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ fetchImpl: proxyFetch }),
|
||||
);
|
||||
});
|
||||
|
||||
it("downloads forwarded stickers", async () => {
|
||||
const sticker = {
|
||||
id: "sticker-1",
|
||||
@@ -134,6 +167,7 @@ describe("resolveForwardedMediaList", () => {
|
||||
url: "https://media.discordapp.net/stickers/sticker-1.png",
|
||||
filePathHint: "wave.png",
|
||||
maxBytes: 512,
|
||||
fetchImpl: undefined,
|
||||
});
|
||||
expect(saveMediaBuffer).toHaveBeenCalledTimes(1);
|
||||
expect(saveMediaBuffer).toHaveBeenCalledWith(expect.any(Buffer), "image/png", "inbound", 512);
|
||||
@@ -201,6 +235,7 @@ describe("resolveMediaList", () => {
|
||||
url: "https://media.discordapp.net/stickers/sticker-2.png",
|
||||
filePathHint: "hello.png",
|
||||
maxBytes: 512,
|
||||
fetchImpl: undefined,
|
||||
});
|
||||
expect(saveMediaBuffer).toHaveBeenCalledTimes(1);
|
||||
expect(saveMediaBuffer).toHaveBeenCalledWith(expect.any(Buffer), "image/png", "inbound", 512);
|
||||
@@ -212,6 +247,35 @@ describe("resolveMediaList", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("forwards fetchImpl to sticker downloads", async () => {
|
||||
const proxyFetch = vi.fn() as unknown as typeof fetch;
|
||||
const sticker = {
|
||||
id: "sticker-proxy",
|
||||
name: "proxy-sticker",
|
||||
format_type: StickerFormatType.PNG,
|
||||
};
|
||||
fetchRemoteMedia.mockResolvedValueOnce({
|
||||
buffer: Buffer.from("sticker"),
|
||||
contentType: "image/png",
|
||||
});
|
||||
saveMediaBuffer.mockResolvedValueOnce({
|
||||
path: "/tmp/sticker-proxy.png",
|
||||
contentType: "image/png",
|
||||
});
|
||||
|
||||
await resolveMediaList(
|
||||
asMessage({
|
||||
stickers: [sticker],
|
||||
}),
|
||||
512,
|
||||
proxyFetch,
|
||||
);
|
||||
|
||||
expect(fetchRemoteMedia).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ fetchImpl: proxyFetch }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDiscordMessageText", () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ChannelType, Client, Message } from "@buape/carbon";
|
||||
import { StickerFormatType, type APIAttachment, type APIStickerItem } from "discord-api-types/v10";
|
||||
import { buildMediaPayload } from "../../channels/plugins/media-payload.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { fetchRemoteMedia } from "../../media/fetch.js";
|
||||
import { fetchRemoteMedia, type FetchLike } from "../../media/fetch.js";
|
||||
import { saveMediaBuffer } from "../../media/store.js";
|
||||
|
||||
export type DiscordMediaInfo = {
|
||||
@@ -161,6 +161,7 @@ export function hasDiscordMessageStickers(message: Message): boolean {
|
||||
export async function resolveMediaList(
|
||||
message: Message,
|
||||
maxBytes: number,
|
||||
fetchImpl?: FetchLike,
|
||||
): Promise<DiscordMediaInfo[]> {
|
||||
const out: DiscordMediaInfo[] = [];
|
||||
await appendResolvedMediaFromAttachments({
|
||||
@@ -168,12 +169,14 @@ export async function resolveMediaList(
|
||||
maxBytes,
|
||||
out,
|
||||
errorPrefix: "discord: failed to download attachment",
|
||||
fetchImpl,
|
||||
});
|
||||
await appendResolvedMediaFromStickers({
|
||||
stickers: resolveDiscordMessageStickers(message),
|
||||
maxBytes,
|
||||
out,
|
||||
errorPrefix: "discord: failed to download sticker",
|
||||
fetchImpl,
|
||||
});
|
||||
return out;
|
||||
}
|
||||
@@ -181,6 +184,7 @@ export async function resolveMediaList(
|
||||
export async function resolveForwardedMediaList(
|
||||
message: Message,
|
||||
maxBytes: number,
|
||||
fetchImpl?: FetchLike,
|
||||
): Promise<DiscordMediaInfo[]> {
|
||||
const snapshots = resolveDiscordMessageSnapshots(message);
|
||||
if (snapshots.length === 0) {
|
||||
@@ -193,12 +197,14 @@ export async function resolveForwardedMediaList(
|
||||
maxBytes,
|
||||
out,
|
||||
errorPrefix: "discord: failed to download forwarded attachment",
|
||||
fetchImpl,
|
||||
});
|
||||
await appendResolvedMediaFromStickers({
|
||||
stickers: snapshot.message ? resolveDiscordSnapshotStickers(snapshot.message) : [],
|
||||
maxBytes,
|
||||
out,
|
||||
errorPrefix: "discord: failed to download forwarded sticker",
|
||||
fetchImpl,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
@@ -209,6 +215,7 @@ async function appendResolvedMediaFromAttachments(params: {
|
||||
maxBytes: number;
|
||||
out: DiscordMediaInfo[];
|
||||
errorPrefix: string;
|
||||
fetchImpl?: FetchLike;
|
||||
}) {
|
||||
const attachments = params.attachments;
|
||||
if (!attachments || attachments.length === 0) {
|
||||
@@ -220,6 +227,7 @@ async function appendResolvedMediaFromAttachments(params: {
|
||||
url: attachment.url,
|
||||
filePathHint: attachment.filename ?? attachment.url,
|
||||
maxBytes: params.maxBytes,
|
||||
fetchImpl: params.fetchImpl,
|
||||
});
|
||||
const saved = await saveMediaBuffer(
|
||||
fetched.buffer,
|
||||
@@ -296,6 +304,7 @@ async function appendResolvedMediaFromStickers(params: {
|
||||
maxBytes: number;
|
||||
out: DiscordMediaInfo[];
|
||||
errorPrefix: string;
|
||||
fetchImpl?: FetchLike;
|
||||
}) {
|
||||
const stickers = params.stickers;
|
||||
if (!stickers || stickers.length === 0) {
|
||||
@@ -310,6 +319,7 @@ async function appendResolvedMediaFromStickers(params: {
|
||||
url: candidate.url,
|
||||
filePathHint: candidate.fileName,
|
||||
maxBytes: params.maxBytes,
|
||||
fetchImpl: params.fetchImpl,
|
||||
});
|
||||
const saved = await saveMediaBuffer(
|
||||
fetched.buffer,
|
||||
|
||||
@@ -117,6 +117,28 @@ describe("Discord model picker custom_id", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("parses compact custom_id aliases", () => {
|
||||
const parsed = parseDiscordModelPickerData({
|
||||
c: "models",
|
||||
a: "submit",
|
||||
v: "models",
|
||||
u: "42",
|
||||
p: "openai",
|
||||
g: "3",
|
||||
mi: "2",
|
||||
});
|
||||
|
||||
expect(parsed).toEqual({
|
||||
command: "models",
|
||||
action: "submit",
|
||||
view: "models",
|
||||
userId: "42",
|
||||
provider: "openai",
|
||||
page: 3,
|
||||
modelIndex: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses optional submit model index", () => {
|
||||
const parsed = parseDiscordModelPickerData({
|
||||
cmd: "models",
|
||||
@@ -179,6 +201,21 @@ describe("Discord model picker custom_id", () => {
|
||||
}),
|
||||
).toThrow(/custom_id exceeds/i);
|
||||
});
|
||||
|
||||
it("keeps typical submit ids under Discord max length", () => {
|
||||
const customId = buildDiscordModelPickerCustomId({
|
||||
command: "models",
|
||||
action: "submit",
|
||||
view: "models",
|
||||
provider: "azure-openai-responses",
|
||||
page: 1,
|
||||
providerPage: 1,
|
||||
modelIndex: 10,
|
||||
userId: "12345678901234567890",
|
||||
});
|
||||
|
||||
expect(customId.length).toBeLessThanOrEqual(DISCORD_CUSTOM_ID_MAX_CHARS);
|
||||
});
|
||||
});
|
||||
|
||||
describe("provider paging", () => {
|
||||
@@ -325,7 +362,7 @@ describe("Discord model picker rendering", () => {
|
||||
return parsed?.action === "provider";
|
||||
});
|
||||
expect(providerButtons).toHaveLength(Object.keys(entries).length);
|
||||
expect(allButtons.some((component) => (component.custom_id ?? "").includes(":act=nav:"))).toBe(
|
||||
expect(allButtons.some((component) => (component.custom_id ?? "").includes(";a=nav;"))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -352,7 +389,7 @@ describe("Discord model picker rendering", () => {
|
||||
expect(rows.length).toBeGreaterThan(0);
|
||||
|
||||
const allButtons = rows.flatMap((row) => row.components ?? []);
|
||||
expect(allButtons.some((component) => (component.custom_id ?? "").includes(":act=nav:"))).toBe(
|
||||
expect(allButtons.some((component) => (component.custom_id ?? "").includes(";a=nav;"))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -577,11 +577,11 @@ export function buildDiscordModelPickerCustomId(params: {
|
||||
: undefined;
|
||||
|
||||
const parts = [
|
||||
`${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:cmd=${encodeCustomIdValue(params.command)}`,
|
||||
`act=${encodeCustomIdValue(params.action)}`,
|
||||
`view=${encodeCustomIdValue(params.view)}`,
|
||||
`${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:c=${encodeCustomIdValue(params.command)}`,
|
||||
`a=${encodeCustomIdValue(params.action)}`,
|
||||
`v=${encodeCustomIdValue(params.view)}`,
|
||||
`u=${encodeCustomIdValue(userId)}`,
|
||||
`pg=${String(page)}`,
|
||||
`g=${String(page)}`,
|
||||
];
|
||||
if (normalizedProvider) {
|
||||
parts.push(`p=${encodeCustomIdValue(normalizedProvider)}`);
|
||||
@@ -635,12 +635,12 @@ export function parseDiscordModelPickerData(data: ComponentData): DiscordModelPi
|
||||
return null;
|
||||
}
|
||||
|
||||
const command = decodeCustomIdValue(coerceString(data.cmd));
|
||||
const action = decodeCustomIdValue(coerceString(data.act));
|
||||
const view = decodeCustomIdValue(coerceString(data.view));
|
||||
const command = decodeCustomIdValue(coerceString(data.c ?? data.cmd));
|
||||
const action = decodeCustomIdValue(coerceString(data.a ?? data.act));
|
||||
const view = decodeCustomIdValue(coerceString(data.v ?? data.view));
|
||||
const userId = decodeCustomIdValue(coerceString(data.u));
|
||||
const providerRaw = decodeCustomIdValue(coerceString(data.p));
|
||||
const page = parseRawPage(data.pg);
|
||||
const page = parseRawPage(data.g ?? data.pg);
|
||||
const providerPage = parseRawPositiveInt(data.pp);
|
||||
const modelIndex = parseRawPositiveInt(data.mi);
|
||||
const recentSlot = parseRawPositiveInt(data.rs);
|
||||
|
||||
@@ -2,14 +2,22 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
GatewayIntents,
|
||||
baseRegisterClientSpy,
|
||||
GatewayPlugin,
|
||||
HttpsProxyAgent,
|
||||
getLastAgent,
|
||||
proxyAgentSpy,
|
||||
restProxyAgentSpy,
|
||||
undiciFetchMock,
|
||||
undiciProxyAgentSpy,
|
||||
resetLastAgent,
|
||||
webSocketSpy,
|
||||
wsProxyAgentSpy,
|
||||
} = vi.hoisted(() => {
|
||||
const proxyAgentSpy = vi.fn();
|
||||
const wsProxyAgentSpy = vi.fn();
|
||||
const undiciProxyAgentSpy = vi.fn();
|
||||
const restProxyAgentSpy = vi.fn();
|
||||
const undiciFetchMock = vi.fn();
|
||||
const baseRegisterClientSpy = vi.fn();
|
||||
const webSocketSpy = vi.fn();
|
||||
|
||||
const GatewayIntents = {
|
||||
@@ -23,7 +31,17 @@ const {
|
||||
GuildMembers: 1 << 7,
|
||||
} as const;
|
||||
|
||||
class GatewayPlugin {}
|
||||
class GatewayPlugin {
|
||||
options: unknown;
|
||||
gatewayInfo: unknown;
|
||||
constructor(options?: unknown, gatewayInfo?: unknown) {
|
||||
this.options = options;
|
||||
this.gatewayInfo = gatewayInfo;
|
||||
}
|
||||
async registerClient(client: unknown) {
|
||||
baseRegisterClientSpy(client);
|
||||
}
|
||||
}
|
||||
|
||||
class HttpsProxyAgent {
|
||||
static lastCreated: HttpsProxyAgent | undefined;
|
||||
@@ -34,20 +52,24 @@ const {
|
||||
}
|
||||
this.proxyUrl = proxyUrl;
|
||||
HttpsProxyAgent.lastCreated = this;
|
||||
proxyAgentSpy(proxyUrl);
|
||||
wsProxyAgentSpy(proxyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
baseRegisterClientSpy,
|
||||
GatewayIntents,
|
||||
GatewayPlugin,
|
||||
HttpsProxyAgent,
|
||||
getLastAgent: () => HttpsProxyAgent.lastCreated,
|
||||
proxyAgentSpy,
|
||||
restProxyAgentSpy,
|
||||
undiciFetchMock,
|
||||
undiciProxyAgentSpy,
|
||||
resetLastAgent: () => {
|
||||
HttpsProxyAgent.lastCreated = undefined;
|
||||
},
|
||||
webSocketSpy,
|
||||
wsProxyAgentSpy,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -61,6 +83,18 @@ vi.mock("https-proxy-agent", () => ({
|
||||
HttpsProxyAgent,
|
||||
}));
|
||||
|
||||
vi.mock("undici", () => ({
|
||||
ProxyAgent: class {
|
||||
proxyUrl: string;
|
||||
constructor(proxyUrl: string) {
|
||||
this.proxyUrl = proxyUrl;
|
||||
undiciProxyAgentSpy(proxyUrl);
|
||||
restProxyAgentSpy(proxyUrl);
|
||||
}
|
||||
},
|
||||
fetch: undiciFetchMock,
|
||||
}));
|
||||
|
||||
vi.mock("ws", () => ({
|
||||
default: class MockWebSocket {
|
||||
constructor(url: string, options?: { agent?: unknown }) {
|
||||
@@ -87,7 +121,11 @@ describe("createDiscordGatewayPlugin", () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
proxyAgentSpy.mockClear();
|
||||
baseRegisterClientSpy.mockClear();
|
||||
restProxyAgentSpy.mockClear();
|
||||
undiciFetchMock.mockClear();
|
||||
undiciProxyAgentSpy.mockClear();
|
||||
wsProxyAgentSpy.mockClear();
|
||||
webSocketSpy.mockClear();
|
||||
resetLastAgent();
|
||||
});
|
||||
@@ -106,7 +144,7 @@ describe("createDiscordGatewayPlugin", () => {
|
||||
.createWebSocket;
|
||||
createWebSocket("wss://gateway.discord.gg");
|
||||
|
||||
expect(proxyAgentSpy).toHaveBeenCalledWith("http://proxy.test:8080");
|
||||
expect(wsProxyAgentSpy).toHaveBeenCalledWith("http://proxy.test:8080");
|
||||
expect(webSocketSpy).toHaveBeenCalledWith(
|
||||
"wss://gateway.discord.gg",
|
||||
expect.objectContaining({ agent: getLastAgent() }),
|
||||
@@ -127,4 +165,33 @@ describe("createDiscordGatewayPlugin", () => {
|
||||
expect(runtime.error).toHaveBeenCalled();
|
||||
expect(runtime.log).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses proxy fetch for gateway metadata lookup before registering", async () => {
|
||||
const runtime = createRuntime();
|
||||
undiciFetchMock.mockResolvedValue({
|
||||
json: async () => ({ url: "wss://gateway.discord.gg" }),
|
||||
} as Response);
|
||||
const plugin = createDiscordGatewayPlugin({
|
||||
discordConfig: { proxy: "http://proxy.test:8080" },
|
||||
runtime,
|
||||
});
|
||||
|
||||
await (
|
||||
plugin as unknown as {
|
||||
registerClient: (client: { options: { token: string } }) => Promise<void>;
|
||||
}
|
||||
).registerClient({
|
||||
options: { token: "token-123" },
|
||||
});
|
||||
|
||||
expect(restProxyAgentSpy).toHaveBeenCalledWith("http://proxy.test:8080");
|
||||
expect(undiciFetchMock).toHaveBeenCalledWith(
|
||||
"https://discord.com/api/v10/gateway/bot",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bot token-123" },
|
||||
dispatcher: expect.objectContaining({ proxyUrl: "http://proxy.test:8080" }),
|
||||
}),
|
||||
);
|
||||
expect(baseRegisterClientSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -550,6 +550,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
allowFrom,
|
||||
guildEntries,
|
||||
threadBindings,
|
||||
discordRestFetch,
|
||||
});
|
||||
|
||||
registerDiscordListener(client.listeners, new DiscordMessageListener(messageHandler, logger));
|
||||
|
||||
Reference in New Issue
Block a user