import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { IdentityRegistry } from "./identity-registry.js"; const PERM_VIEW_CHANNEL = 1n << 10n; const PERM_ADMINISTRATOR = 1n << 3n; function toBigIntPerm(v: unknown): bigint { if (typeof v === "bigint") return v; if (typeof v === "number") return BigInt(Math.trunc(v)); if (typeof v === "string" && v.trim()) { try { return BigInt(v.trim()); } catch { return 0n; } } return 0n; } function roleOrMemberType(v: unknown): number { if (typeof v === "number") return v; if (typeof v === "string" && v.toLowerCase() === "member") return 1; return 0; } async function discordRequest(token: string, method: string, path: string): Promise<{ ok: boolean; status: number; json: any; text: string }> { const r = await fetch(`https://discord.com/api/v10${path}`, { method, headers: { Authorization: `Bot ${token}`, "Content-Type": "application/json", }, }); const text = await r.text(); let json: any = null; try { json = text ? JSON.parse(text) : null; } catch { json = null; } return { ok: r.ok, status: r.status, json, text }; } function canViewChannel(member: any, guildId: string, guildRoles: Map, channelOverwrites: any[]): boolean { const roleIds: string[] = Array.isArray(member?.roles) ? member.roles : []; let perms = guildRoles.get(guildId) || 0n; for (const rid of roleIds) perms |= guildRoles.get(rid) || 0n; if ((perms & PERM_ADMINISTRATOR) !== 0n) return true; let everyoneAllow = 0n; let everyoneDeny = 0n; for (const ow of channelOverwrites) { if (String(ow?.id || "") === guildId && roleOrMemberType(ow?.type) === 0) { everyoneAllow = toBigIntPerm(ow?.allow); everyoneDeny = toBigIntPerm(ow?.deny); break; } } perms = (perms & ~everyoneDeny) | everyoneAllow; let roleAllow = 0n; let roleDeny = 0n; for (const ow of channelOverwrites) { if (roleOrMemberType(ow?.type) !== 0) continue; const id = String(ow?.id || ""); if (id !== guildId && roleIds.includes(id)) { roleAllow |= toBigIntPerm(ow?.allow); roleDeny |= toBigIntPerm(ow?.deny); } } perms = (perms & ~roleDeny) | roleAllow; for (const ow of channelOverwrites) { if (roleOrMemberType(ow?.type) !== 1) continue; if (String(ow?.id || "") === String(member?.user?.id || "")) { const allow = toBigIntPerm(ow?.allow); const deny = toBigIntPerm(ow?.deny); perms = (perms & ~deny) | allow; break; } } return (perms & PERM_VIEW_CHANNEL) !== 0n; } function getDiscoveryToken(api: OpenClawPluginApi): string | undefined { // Prefer moderator bot token from pluginConfig — it has guild member access const pluginCfg = (api.pluginConfig as Record) || {}; const moderatorToken = pluginCfg.moderatorBotToken; if (typeof moderatorToken === "string" && moderatorToken) { return moderatorToken; } // Fall back to any discord account token const root = (api.config as Record) || {}; const channels = (root.channels as Record) || {}; const discord = (channels.discord as Record) || {}; const accounts = (discord.accounts as Record>) || {}; for (const rec of Object.values(accounts)) { if (typeof rec?.token === "string" && rec.token) return rec.token; } return undefined; } /** * Returns agentIds for all agents visible in the channel, resolved via the identity registry. */ export async function fetchVisibleChannelBotAccountIds( api: OpenClawPluginApi, channelId: string, identityRegistry?: IdentityRegistry, ): Promise { const token = getDiscoveryToken(api); if (!token) return []; const ch = await discordRequest(token, "GET", `/channels/${channelId}`); if (!ch.ok) return []; const guildId = String(ch.json?.guild_id || ""); if (!guildId) return []; const rolesResp = await discordRequest(token, "GET", `/guilds/${guildId}/roles`); if (!rolesResp.ok) return []; const rolePerms = new Map(); for (const r of Array.isArray(rolesResp.json) ? rolesResp.json : []) { rolePerms.set(String(r?.id || ""), toBigIntPerm(r?.permissions)); } const members: any[] = []; let after = ""; while (true) { const q = new URLSearchParams({ limit: "1000" }); if (after) q.set("after", after); const mResp = await discordRequest(token, "GET", `/guilds/${guildId}/members?${q.toString()}`); if (!mResp.ok) return []; const batch = Array.isArray(mResp.json) ? mResp.json : []; members.push(...batch); if (batch.length < 1000) break; after = String(batch[batch.length - 1]?.user?.id || ""); if (!after) break; } const overwrites = Array.isArray(ch.json?.permission_overwrites) ? ch.json.permission_overwrites : []; const visibleUserIds = members .filter((m) => canViewChannel(m, guildId, rolePerms, overwrites)) .map((m) => String(m?.user?.id || "")) .filter(Boolean); const out = new Set(); if (identityRegistry) { const discordToAgent = identityRegistry.buildDiscordToAgentMap(); for (const uid of visibleUserIds) { const aid = discordToAgent.get(uid); if (aid) out.add(aid); } } return [...out]; }