mirror of https://github.com/openclaw/openclaw.git
fix: lazy-load Discord allowlist guilds (#20208) (thanks @zhangjunmengyang)
This commit is contained in:
parent
844d84a7f5
commit
866b33e0d3
|
|
@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Telegram/Status reactions: refresh stall timers on repeated phase updates and honor ack-reaction scope when lifecycle reactions are enabled, preventing false stall emojis and unwanted group reactions. Thanks @wolly-tundracube and @thewilloftheshadow.
|
||||
- Discord/Streaming: apply `replyToMode: first` only to the first Discord chunk so block-streamed replies do not spam mention pings. (#20726) Thanks @thewilloftheshadow for the report.
|
||||
- Discord/Components: map DM channel targets back to user-scoped component sessions so button/select interactions stay in the main DM session. Thanks @thewilloftheshadow.
|
||||
- Discord/Allowlist: lazy-load guild lists when resolving Discord user allowlists so ID-only entries resolve even if guild fetch fails. (#20208) Thanks @zhangjunmengyang.
|
||||
|
||||
- Discord/Gateway: handle close code 4014 (missing privileged gateway intents) without crashing the gateway. Thanks @thewilloftheshadow.
|
||||
- Discord: ingest inbound stickers as media so sticker-only messages and forwarded stickers are visible to agents. Thanks @thewilloftheshadow.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,242 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
|
||||
|
||||
function jsonResponse(body: unknown) {
|
||||
return new Response(JSON.stringify(body), { status: 200 });
|
||||
}
|
||||
|
||||
const urlToString = (url: Request | URL | string): string => {
|
||||
if (typeof url === "string") {
|
||||
return url;
|
||||
}
|
||||
return "url" in url ? url.url : String(url);
|
||||
};
|
||||
|
||||
describe("resolveDiscordUserAllowlist", () => {
|
||||
it("resolves plain user ids without calling listGuilds", async () => {
|
||||
let guildsCalled = false;
|
||||
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url.endsWith("/users/@me/guilds")) {
|
||||
guildsCalled = true;
|
||||
return jsonResponse([]);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
const results = await resolveDiscordUserAllowlist({
|
||||
token: "test",
|
||||
entries: ["123456789012345678"],
|
||||
fetcher,
|
||||
});
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
input: "123456789012345678",
|
||||
resolved: true,
|
||||
id: "123456789012345678",
|
||||
},
|
||||
]);
|
||||
expect(guildsCalled).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves mention-format ids without calling listGuilds", async () => {
|
||||
let guildsCalled = false;
|
||||
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url.endsWith("/users/@me/guilds")) {
|
||||
guildsCalled = true;
|
||||
return jsonResponse([]);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
const results = await resolveDiscordUserAllowlist({
|
||||
token: "test",
|
||||
entries: ["<@!123456789012345678>"],
|
||||
fetcher,
|
||||
});
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
input: "<@!123456789012345678>",
|
||||
resolved: true,
|
||||
id: "123456789012345678",
|
||||
},
|
||||
]);
|
||||
expect(guildsCalled).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves prefixed ids (user:, discord:) without calling listGuilds", async () => {
|
||||
let guildsCalled = false;
|
||||
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url.endsWith("/users/@me/guilds")) {
|
||||
guildsCalled = true;
|
||||
return jsonResponse([]);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
const results = await resolveDiscordUserAllowlist({
|
||||
token: "test",
|
||||
entries: ["user:111", "discord:222"],
|
||||
fetcher,
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]).toMatchObject({ resolved: true, id: "111" });
|
||||
expect(results[1]).toMatchObject({ resolved: true, id: "222" });
|
||||
expect(guildsCalled).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves user ids even when listGuilds would fail", async () => {
|
||||
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url.endsWith("/users/@me/guilds")) {
|
||||
throw new Error("Forbidden: Missing Access");
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
// Before the fix, this would throw because listGuilds() was called eagerly
|
||||
const results = await resolveDiscordUserAllowlist({
|
||||
token: "test",
|
||||
entries: ["994979735488692324"],
|
||||
fetcher,
|
||||
});
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
input: "994979735488692324",
|
||||
resolved: true,
|
||||
id: "994979735488692324",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("calls listGuilds lazily when resolving usernames", async () => {
|
||||
let guildsCalled = false;
|
||||
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url.endsWith("/users/@me/guilds")) {
|
||||
guildsCalled = true;
|
||||
return jsonResponse([{ id: "g1", name: "Test Guild" }]);
|
||||
}
|
||||
if (url.includes("/guilds/g1/members/search")) {
|
||||
return jsonResponse([
|
||||
{
|
||||
user: { id: "u1", username: "alice", bot: false },
|
||||
nick: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
const results = await resolveDiscordUserAllowlist({
|
||||
token: "test",
|
||||
entries: ["alice"],
|
||||
fetcher,
|
||||
});
|
||||
|
||||
expect(guildsCalled).toBe(true);
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0]).toMatchObject({
|
||||
input: "alice",
|
||||
resolved: true,
|
||||
id: "u1",
|
||||
name: "alice",
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches guilds only once for multiple username entries", async () => {
|
||||
let guildsCallCount = 0;
|
||||
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url.endsWith("/users/@me/guilds")) {
|
||||
guildsCallCount++;
|
||||
return jsonResponse([{ id: "g1", name: "Test Guild" }]);
|
||||
}
|
||||
if (url.includes("/guilds/g1/members/search")) {
|
||||
const params = new URL(url).searchParams;
|
||||
const query = params.get("query") ?? "";
|
||||
return jsonResponse([
|
||||
{
|
||||
user: { id: `u-${query}`, username: query, bot: false },
|
||||
nick: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
const results = await resolveDiscordUserAllowlist({
|
||||
token: "test",
|
||||
entries: ["alice", "bob"],
|
||||
fetcher,
|
||||
});
|
||||
|
||||
expect(guildsCallCount).toBe(1);
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]).toMatchObject({ resolved: true, id: "u-alice" });
|
||||
expect(results[1]).toMatchObject({ resolved: true, id: "u-bob" });
|
||||
});
|
||||
|
||||
it("handles mixed ids and usernames — ids resolve even if guilds fail", async () => {
|
||||
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url.endsWith("/users/@me/guilds")) {
|
||||
throw new Error("Forbidden: Missing Access");
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
// IDs should succeed, username should fail (listGuilds throws)
|
||||
await expect(
|
||||
resolveDiscordUserAllowlist({
|
||||
token: "test",
|
||||
entries: ["123456789012345678", "alice"],
|
||||
fetcher,
|
||||
}),
|
||||
).rejects.toThrow("Forbidden");
|
||||
|
||||
// But if we only pass IDs, it should work fine
|
||||
const results = await resolveDiscordUserAllowlist({
|
||||
token: "test",
|
||||
entries: ["123456789012345678", "<@999>"],
|
||||
fetcher,
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]).toMatchObject({ resolved: true, id: "123456789012345678" });
|
||||
expect(results[1]).toMatchObject({ resolved: true, id: "999" });
|
||||
});
|
||||
|
||||
it("returns unresolved for empty/blank entries", async () => {
|
||||
const fetcher = withFetchPreconnect(async () => {
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
const results = await resolveDiscordUserAllowlist({
|
||||
token: "test",
|
||||
entries: ["", " "],
|
||||
fetcher,
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]).toMatchObject({ resolved: false });
|
||||
expect(results[1]).toMatchObject({ resolved: false });
|
||||
});
|
||||
|
||||
it("returns all unresolved when token is empty", async () => {
|
||||
const results = await resolveDiscordUserAllowlist({
|
||||
token: "",
|
||||
entries: ["123456789012345678", "alice"],
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.every((r) => !r.resolved)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -88,7 +88,18 @@ export async function resolveDiscordUserAllowlist(params: {
|
|||
}));
|
||||
}
|
||||
const fetcher = params.fetcher ?? fetch;
|
||||
const guilds = await listGuilds(token, fetcher);
|
||||
|
||||
// Lazy-load guilds: only fetch when an entry actually needs username search.
|
||||
// This prevents listGuilds() failures (permissions, network) from blocking
|
||||
// resolution of plain user-id entries that don't need guild data at all.
|
||||
let guilds: DiscordGuildSummary[] | null = null;
|
||||
const getGuilds = async (): Promise<DiscordGuildSummary[]> => {
|
||||
if (!guilds) {
|
||||
guilds = await listGuilds(token, fetcher);
|
||||
}
|
||||
return guilds;
|
||||
};
|
||||
|
||||
const results: DiscordUserResolution[] = [];
|
||||
|
||||
for (const input of params.entries) {
|
||||
|
|
@ -109,11 +120,12 @@ export async function resolveDiscordUserAllowlist(params: {
|
|||
}
|
||||
|
||||
const guildName = parsed.guildName?.trim();
|
||||
const allGuilds = await getGuilds();
|
||||
const guildList = parsed.guildId
|
||||
? guilds.filter((g) => g.id === parsed.guildId)
|
||||
? allGuilds.filter((g) => g.id === parsed.guildId)
|
||||
: guildName
|
||||
? guilds.filter((g) => g.slug === normalizeDiscordSlug(guildName))
|
||||
: guilds;
|
||||
? allGuilds.filter((g) => g.slug === normalizeDiscordSlug(guildName))
|
||||
: allGuilds;
|
||||
|
||||
let best: { member: DiscordMember; guild: DiscordGuildSummary; score: number } | null = null;
|
||||
let matches = 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue