import { addAllowlistUserEntriesFromConfigEntry, buildAllowlistResolutionSummary, canonicalizeAllowlistWithResolvedIds, patchAllowlistUsersInConfigEntries, summarizeMapping, } from "../../../../src/channels/allowlists/resolve-utils.js"; import type { DiscordGuildEntry } from "../../../../src/config/types.discord.js"; import { formatErrorMessage } from "../../../../src/infra/errors.js"; import type { RuntimeEnv } from "../../../../src/runtime.js"; import { normalizeStringEntries } from "../../../../src/shared/string-normalization.js"; import { resolveDiscordChannelAllowlist } from "../resolve-channels.js"; import { resolveDiscordUserAllowlist } from "../resolve-users.js"; type GuildEntries = Record; type ChannelResolutionInput = { input: string; guildKey: string; channelKey?: string }; type DiscordChannelLogEntry = { input: string; guildId?: string; guildName?: string; channelId?: string; channelName?: string; note?: string; }; type DiscordUserLogEntry = { input: string; id?: string; name?: string; guildName?: string; note?: string; }; function formatResolutionLogDetails(base: string, details: Array): string { const nonEmpty = details .map((value) => value?.trim()) .filter((value): value is string => Boolean(value)); return nonEmpty.length > 0 ? `${base} (${nonEmpty.join("; ")})` : base; } function formatDiscordChannelResolved(entry: DiscordChannelLogEntry): string { const target = entry.channelId ? `${entry.guildId}/${entry.channelId}` : entry.guildId; const base = `${entry.input}→${target}`; return formatResolutionLogDetails(base, [ entry.guildName ? `guild:${entry.guildName}` : undefined, entry.channelName ? `channel:${entry.channelName}` : undefined, entry.note, ]); } function formatDiscordChannelUnresolved(entry: DiscordChannelLogEntry): string { return formatResolutionLogDetails(entry.input, [ entry.guildName ? `guild:${entry.guildName}` : entry.guildId ? `guildId:${entry.guildId}` : undefined, entry.channelName ? `channel:${entry.channelName}` : entry.channelId ? `channelId:${entry.channelId}` : undefined, entry.note, ]); } function formatDiscordUserResolved(entry: DiscordUserLogEntry): string { const displayName = entry.name?.trim(); const target = displayName || entry.id; const base = `${entry.input}→${target}`; return formatResolutionLogDetails(base, [ displayName && entry.id ? `id:${entry.id}` : undefined, entry.guildName ? `guild:${entry.guildName}` : undefined, entry.note, ]); } function formatDiscordUserUnresolved(entry: DiscordUserLogEntry): string { return formatResolutionLogDetails(entry.input, [ entry.name ? `name:${entry.name}` : undefined, entry.guildName ? `guild:${entry.guildName}` : undefined, entry.note, ]); } function toGuildEntries(value: unknown): GuildEntries { if (!value || typeof value !== "object") { return {}; } const out: GuildEntries = {}; for (const [key, entry] of Object.entries(value)) { if (!entry || typeof entry !== "object") { continue; } out[key] = entry as DiscordGuildEntry; } return out; } function toAllowlistEntries(value: unknown): string[] | undefined { if (!Array.isArray(value)) { return undefined; } return value.map((entry) => String(entry).trim()).filter((entry) => Boolean(entry)); } function hasGuildEntries(value: GuildEntries): boolean { return Object.keys(value).length > 0; } function collectChannelResolutionInputs(guildEntries: GuildEntries): ChannelResolutionInput[] { const entries: ChannelResolutionInput[] = []; for (const [guildKey, guildCfg] of Object.entries(guildEntries)) { if (guildKey === "*") { continue; } const channels = guildCfg?.channels ?? {}; const channelKeys = Object.keys(channels).filter((key) => key !== "*"); if (channelKeys.length === 0) { const input = /^\d+$/.test(guildKey) ? `guild:${guildKey}` : guildKey; entries.push({ input, guildKey }); continue; } for (const channelKey of channelKeys) { entries.push({ input: `${guildKey}/${channelKey}`, guildKey, channelKey, }); } } return entries; } async function resolveGuildEntriesByChannelAllowlist(params: { token: string; guildEntries: GuildEntries; fetcher: typeof fetch; runtime: RuntimeEnv; }): Promise { const entries = collectChannelResolutionInputs(params.guildEntries); if (entries.length === 0) { return params.guildEntries; } try { const resolved = await resolveDiscordChannelAllowlist({ token: params.token, entries: entries.map((entry) => entry.input), fetcher: params.fetcher, }); const sourceByInput = new Map(entries.map((entry) => [entry.input, entry])); const nextGuilds = { ...params.guildEntries }; const mapping: string[] = []; const unresolved: string[] = []; for (const entry of resolved) { const source = sourceByInput.get(entry.input); if (!source) { continue; } const sourceGuild = params.guildEntries[source.guildKey] ?? {}; if (!entry.resolved || !entry.guildId) { unresolved.push(formatDiscordChannelUnresolved(entry)); continue; } mapping.push(formatDiscordChannelResolved(entry)); const existing = nextGuilds[entry.guildId] ?? {}; const mergedChannels = { ...sourceGuild.channels, ...existing.channels, }; const mergedGuild: DiscordGuildEntry = { ...sourceGuild, ...existing, channels: mergedChannels, }; nextGuilds[entry.guildId] = mergedGuild; if (source.channelKey && entry.channelId) { const sourceChannel = sourceGuild.channels?.[source.channelKey]; if (sourceChannel) { nextGuilds[entry.guildId] = { ...mergedGuild, channels: { ...mergedChannels, [entry.channelId]: { ...sourceChannel, ...mergedChannels[entry.channelId], }, }, }; } } } summarizeMapping("discord channels", mapping, unresolved, params.runtime); return nextGuilds; } catch (err) { params.runtime.log?.( `discord channel resolve failed; using config entries. ${formatErrorMessage(err)}`, ); return params.guildEntries; } } async function resolveAllowFromByUserAllowlist(params: { token: string; allowFrom: string[] | undefined; fetcher: typeof fetch; runtime: RuntimeEnv; }): Promise { const allowEntries = normalizeStringEntries(params.allowFrom).filter((entry) => entry !== "*"); if (allowEntries.length === 0) { return params.allowFrom; } try { const resolvedUsers = await resolveDiscordUserAllowlist({ token: params.token, entries: allowEntries, fetcher: params.fetcher, }); const { resolvedMap, mapping, unresolved } = buildAllowlistResolutionSummary(resolvedUsers, { formatResolved: formatDiscordUserResolved, formatUnresolved: formatDiscordUserUnresolved, }); const allowFrom = canonicalizeAllowlistWithResolvedIds({ existing: params.allowFrom, resolvedMap, }); summarizeMapping("discord users", mapping, unresolved, params.runtime); return allowFrom; } catch (err) { params.runtime.log?.( `discord user resolve failed; using config entries. ${formatErrorMessage(err)}`, ); return params.allowFrom; } } function collectGuildUserEntries(guildEntries: GuildEntries): Set { const userEntries = new Set(); for (const guild of Object.values(guildEntries)) { if (!guild || typeof guild !== "object") { continue; } addAllowlistUserEntriesFromConfigEntry(userEntries, guild); const channels = (guild as { channels?: Record }).channels ?? {}; for (const channel of Object.values(channels)) { addAllowlistUserEntriesFromConfigEntry(userEntries, channel); } } return userEntries; } async function resolveGuildEntriesByUserAllowlist(params: { token: string; guildEntries: GuildEntries; fetcher: typeof fetch; runtime: RuntimeEnv; }): Promise { const userEntries = collectGuildUserEntries(params.guildEntries); if (userEntries.size === 0) { return params.guildEntries; } try { const resolvedUsers = await resolveDiscordUserAllowlist({ token: params.token, entries: Array.from(userEntries), fetcher: params.fetcher, }); const { resolvedMap, mapping, unresolved } = buildAllowlistResolutionSummary(resolvedUsers, { formatResolved: formatDiscordUserResolved, formatUnresolved: formatDiscordUserUnresolved, }); const nextGuilds = { ...params.guildEntries }; for (const [guildKey, guildConfig] of Object.entries(params.guildEntries)) { if (!guildConfig || typeof guildConfig !== "object") { continue; } const nextGuild = { ...guildConfig } as Record; const users = (guildConfig as { users?: string[] }).users; if (Array.isArray(users) && users.length > 0) { nextGuild.users = canonicalizeAllowlistWithResolvedIds({ existing: users, resolvedMap, }); } const channels = (guildConfig as { channels?: Record }).channels ?? {}; if (channels && typeof channels === "object") { nextGuild.channels = patchAllowlistUsersInConfigEntries({ entries: channels, resolvedMap, strategy: "canonicalize", }); } nextGuilds[guildKey] = nextGuild as DiscordGuildEntry; } summarizeMapping("discord channel users", mapping, unresolved, params.runtime); return nextGuilds; } catch (err) { params.runtime.log?.( `discord channel user resolve failed; using config entries. ${formatErrorMessage(err)}`, ); return params.guildEntries; } } export async function resolveDiscordAllowlistConfig(params: { token: string; guildEntries: unknown; allowFrom: unknown; fetcher: typeof fetch; runtime: RuntimeEnv; }): Promise<{ guildEntries: GuildEntries | undefined; allowFrom: string[] | undefined }> { let guildEntries = toGuildEntries(params.guildEntries); let allowFrom = toAllowlistEntries(params.allowFrom); if (hasGuildEntries(guildEntries)) { guildEntries = await resolveGuildEntriesByChannelAllowlist({ token: params.token, guildEntries, fetcher: params.fetcher, runtime: params.runtime, }); } allowFrom = await resolveAllowFromByUserAllowlist({ token: params.token, allowFrom, fetcher: params.fetcher, runtime: params.runtime, }); if (hasGuildEntries(guildEntries)) { guildEntries = await resolveGuildEntriesByUserAllowlist({ token: params.token, guildEntries, fetcher: params.fetcher, runtime: params.runtime, }); } return { guildEntries: hasGuildEntries(guildEntries) ? guildEntries : undefined, allowFrom, }; }