diff --git a/CHANGELOG.md b/CHANGELOG.md index a859cb5740..beea429196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai - Sessions/Agents: pass `agentId` through status and usage transcript-resolution paths (auto-reply, gateway usage APIs, and session cost/log loaders) so non-default agents can resolve absolute session files without path-validation failures. (#15103) Thanks @jalehman. - Signal/Install: auto-install `signal-cli` via Homebrew on non-x64 Linux architectures, avoiding x86_64 native binary `Exec format error` failures on arm64/arm hosts. (#15443) Thanks @jogvan-k. - Discord: avoid misrouting numeric guild allowlist entries to `/channels/` by prefixing guild-only inputs with `guild:` during resolution. (#12326) Thanks @headswim. +- Discord: ignore partial guild entries when resolving invite allowlists and directory lookups to avoid crashes on missing `id`/`name` data. (#6824) Thanks @gavinbmoore. - Config: preserve `${VAR}` env references when writing config files so `openclaw config set/apply/patch` does not persist secrets to disk. Thanks @thewilloftheshadow. - Process/Exec: avoid shell execution for `.exe` commands on Windows so env overrides work reliably in `runCommandWithTimeout`. Thanks @thewilloftheshadow. - Web tools/web_fetch: prefer `text/markdown` responses for Cloudflare Markdown for Agents, add `cf-markdown` extraction for markdown bodies, and redact fetched URLs in `x-markdown-tokens` debug logs to avoid leaking raw paths/query params. (#15376) Thanks @Yaxuan42. diff --git a/src/discord/directory-live.ts b/src/discord/directory-live.ts index e17c9ae61e..0894127b6d 100644 --- a/src/discord/directory-live.ts +++ b/src/discord/directory-live.ts @@ -5,7 +5,12 @@ import { fetchDiscord } from "./api.js"; import { normalizeDiscordSlug } from "./monitor/allow-list.js"; import { normalizeDiscordToken } from "./token.js"; -type DiscordGuild = { id: string; name: string }; +type DiscordGuild = { id?: string; name?: string }; + +function isDiscordGuildWithId(guild: DiscordGuild): guild is { id: string; name?: string } { + return typeof guild.id === "string"; +} + type DiscordUser = { id: string; username: string; global_name?: string; bot?: boolean }; type DiscordMember = { user: DiscordUser; nick?: string | null }; type DiscordChannel = { id: string; name?: string | null }; @@ -28,7 +33,7 @@ export async function listDiscordDirectoryGroupsLive( } const query = normalizeQuery(params.query); const rawGuilds = await fetchDiscord("/users/@me/guilds", token); - const guilds = rawGuilds.filter((g) => g.id && g.name); + const guilds = rawGuilds.filter(isDiscordGuildWithId); const rows: ChannelDirectoryEntry[] = []; for (const guild of guilds) { @@ -71,7 +76,7 @@ export async function listDiscordDirectoryPeersLive( } const rawGuilds = await fetchDiscord("/users/@me/guilds", token); - const guilds = rawGuilds.filter((g) => g.id && g.name); + const guilds = rawGuilds.filter(isDiscordGuildWithId); const rows: ChannelDirectoryEntry[] = []; const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 25; diff --git a/src/discord/resolve-channels.test.ts b/src/discord/resolve-channels.test.ts index e3eaa55db4..149f47138d 100644 --- a/src/discord/resolve-channels.test.ts +++ b/src/discord/resolve-channels.test.ts @@ -31,6 +31,32 @@ describe("resolveDiscordChannelAllowlist", () => { expect(res[0]?.channelId).toBe("c1"); }); + it("ignores partial guild entries", async () => { + const fetcher = async (url: string) => { + if (url.endsWith("/users/@me/guilds")) { + return jsonResponse([ + { id: "g1", name: "Guild One" }, + { id: "g2" }, + { name: "Missing ID" }, + ]); + } + if (url.endsWith("/guilds/g1/channels")) { + return jsonResponse([{ id: "c1", name: "general", guild_id: "g1", type: 0 }]); + } + return new Response("not found", { status: 404 }); + }; + + const res = await resolveDiscordChannelAllowlist({ + token: "test", + entries: ["Guild One/general"], + fetcher, + }); + + expect(res[0]?.resolved).toBe(true); + expect(res[0]?.guildId).toBe("g1"); + expect(res[0]?.channelId).toBe("c1"); + }); + it("resolves channel id to guild", async () => { const fetcher = async (url: string) => { if (url.endsWith("/users/@me/guilds")) { diff --git a/src/discord/resolve-users.test.ts b/src/discord/resolve-users.test.ts new file mode 100644 index 0000000000..234b6a26ae --- /dev/null +++ b/src/discord/resolve-users.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { resolveDiscordUserAllowlist } from "./resolve-users.js"; + +function jsonResponse(body: unknown) { + return new Response(JSON.stringify(body), { status: 200 }); +} + +describe("resolveDiscordUserAllowlist", () => { + it("ignores partial guild entries", async () => { + const fetcher = async (url: string) => { + if (url.endsWith("/users/@me/guilds")) { + return jsonResponse([ + { id: "g1", name: "Guild One" }, + { id: "g2" }, + { name: "Missing ID" }, + ]); + } + if (url.includes("/guilds/g1/members/search")) { + return jsonResponse([ + { + user: { + id: "u1", + username: "alex", + }, + }, + ]); + } + return new Response("not found", { status: 404 }); + }; + + const res = await resolveDiscordUserAllowlist({ + token: "test", + entries: ["Guild One/alex"], + fetcher, + }); + + expect(res[0]?.resolved).toBe(true); + expect(res[0]?.id).toBe("u1"); + expect(res[0]?.guildId).toBe("g1"); + }); +});