fix(discord): enrich allowlist resolution logs

This commit is contained in:
Peter Steinberger
2026-03-02 04:19:26 +00:00
parent d17f4432b3
commit 619dfa88cb
7 changed files with 211 additions and 14 deletions

View File

@@ -27,6 +27,15 @@ describe("buildAllowlistResolutionSummary", () => {
});
expect(result.mapping).toEqual(["a→1 (note)"]);
});
it("supports custom unresolved formatting", () => {
const resolvedUsers = [{ input: "a", resolved: false, note: "missing" }];
const result = buildAllowlistResolutionSummary(resolvedUsers, {
formatUnresolved: (entry) =>
`${entry.input}${(entry as { note?: string }).note ? " (missing)" : ""}`,
});
expect(result.unresolved).toEqual(["a (missing)"]);
});
});
describe("addAllowlistUserEntriesFromConfigEntry", () => {

View File

@@ -36,7 +36,7 @@ export function mergeAllowlist(params: {
export function buildAllowlistResolutionSummary<T extends AllowlistUserResolutionLike>(
resolvedUsers: T[],
opts?: { formatResolved?: (entry: T) => string },
opts?: { formatResolved?: (entry: T) => string; formatUnresolved?: (entry: T) => string },
): {
resolvedMap: Map<string, T>;
mapping: string[];
@@ -46,14 +46,13 @@ export function buildAllowlistResolutionSummary<T extends AllowlistUserResolutio
const resolvedMap = new Map(resolvedUsers.map((entry) => [entry.input, entry]));
const resolvedOk = (entry: T) => Boolean(entry.resolved && entry.id);
const formatResolved = opts?.formatResolved ?? ((entry: T) => `${entry.input}${entry.id}`);
const formatUnresolved = opts?.formatUnresolved ?? ((entry: T) => entry.input);
const mapping = resolvedUsers.filter(resolvedOk).map(formatResolved);
const additions = resolvedUsers
.filter(resolvedOk)
.map((entry) => entry.id)
.filter((entry): entry is string => Boolean(entry));
const unresolved = resolvedUsers
.filter((entry) => !resolvedOk(entry))
.map((entry) => entry.input);
const unresolved = resolvedUsers.filter((entry) => !resolvedOk(entry)).map(formatUnresolved);
return { resolvedMap, mapping, unresolved, additions };
}

View File

@@ -2,7 +2,9 @@ import { describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../../runtime.js";
const { resolveDiscordChannelAllowlistMock, resolveDiscordUserAllowlistMock } = vi.hoisted(() => ({
resolveDiscordChannelAllowlistMock: vi.fn(async () => []),
resolveDiscordChannelAllowlistMock: vi.fn(
async (_params: { entries: string[] }) => [] as Array<Record<string, unknown>>,
),
resolveDiscordUserAllowlistMock: vi.fn(async (params: { entries: string[] }) =>
params.entries.map((entry) => {
switch (entry) {
@@ -12,6 +14,8 @@ const { resolveDiscordChannelAllowlistMock, resolveDiscordUserAllowlistMock } =
return { input: entry, resolved: true, id: "222" };
case "Carol":
return { input: entry, resolved: false };
case "387":
return { input: entry, resolved: true, id: "387", name: "Peter" };
default:
return { input: entry, resolved: true, id: entry };
}
@@ -54,4 +58,39 @@ describe("resolveDiscordAllowlistConfig", () => {
expect(result.guildEntries?.["*"]?.channels?.["*"]?.users).toEqual(["Carol", "888"]);
expect(resolveDiscordUserAllowlistMock).toHaveBeenCalledTimes(2);
});
it("logs discord name metadata for resolved and unresolved allowlist entries", async () => {
resolveDiscordChannelAllowlistMock.mockResolvedValueOnce([
{
input: "145/c404",
resolved: false,
guildId: "145",
guildName: "Ops",
channelName: "missing-room",
},
]);
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv;
await resolveDiscordAllowlistConfig({
token: "token",
allowFrom: ["387"],
guildEntries: {
"145": {
channels: {
c404: {},
},
},
},
fetcher: vi.fn() as unknown as typeof fetch,
runtime,
});
const logs = (runtime.log as ReturnType<typeof vi.fn>).mock.calls
.map(([line]) => String(line))
.join("\n");
expect(logs).toContain(
"discord channels unresolved: 145/c404 (guild:Ops; channel:missing-room)",
);
expect(logs).toContain("discord users resolved: 387→387 (name:Peter)");
});
});

View File

@@ -13,6 +13,71 @@ import { resolveDiscordUserAllowlist } from "../resolve-users.js";
type GuildEntries = Record<string, DiscordGuildEntry>;
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 | undefined>): 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 base = `${entry.input}${entry.id}`;
return formatResolutionLogDetails(base, [
entry.name ? `name:${entry.name}` : 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") {
@@ -90,14 +155,10 @@ async function resolveGuildEntriesByChannelAllowlist(params: {
}
const sourceGuild = params.guildEntries[source.guildKey] ?? {};
if (!entry.resolved || !entry.guildId) {
unresolved.push(entry.input);
unresolved.push(formatDiscordChannelUnresolved(entry));
continue;
}
mapping.push(
entry.channelId
? `${entry.input}${entry.guildId}/${entry.channelId}`
: `${entry.input}${entry.guildId}`,
);
mapping.push(formatDiscordChannelResolved(entry));
const existing = nextGuilds[entry.guildId] ?? {};
const mergedChannels = {
...sourceGuild.channels,
@@ -153,7 +214,10 @@ async function resolveAllowFromByUserAllowlist(params: {
entries: allowEntries.map((entry) => String(entry)),
fetcher: params.fetcher,
});
const { resolvedMap, mapping, unresolved } = buildAllowlistResolutionSummary(resolvedUsers);
const { resolvedMap, mapping, unresolved } = buildAllowlistResolutionSummary(resolvedUsers, {
formatResolved: formatDiscordUserResolved,
formatUnresolved: formatDiscordUserUnresolved,
});
const allowFrom = canonicalizeAllowlistWithResolvedIds({
existing: params.allowFrom,
resolvedMap,
@@ -199,7 +263,10 @@ async function resolveGuildEntriesByUserAllowlist(params: {
entries: Array.from(userEntries),
fetcher: params.fetcher,
});
const { resolvedMap, mapping, unresolved } = buildAllowlistResolutionSummary(resolvedUsers);
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") {

View File

@@ -549,6 +549,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const logger = createSubsystemLogger("discord/monitor");
const guildHistories = new Map<string, HistoryEntry[]>();
let botUserId: string | undefined;
let botUserName: string | undefined;
let voiceManager: DiscordVoiceManager | null = null;
if (nativeDisabledExplicit) {
@@ -562,6 +563,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
try {
const botUser = await client.fetchUser("@me");
botUserId = botUser?.id;
botUserName = botUser?.username?.trim() || botUser?.globalName?.trim() || undefined;
} catch (err) {
runtime.error?.(danger(`discord: failed to fetch bot identity: ${String(err)}`));
}
@@ -657,7 +659,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
runtime.log?.("discord: GuildPresences intent enabled — presence listener registered");
}
runtime.log?.(`logged in to discord${botUserId ? ` as ${botUserId}` : ""}`);
const botIdentity =
botUserId && botUserName ? `${botUserId} (${botUserName})` : (botUserId ?? botUserName ?? "");
runtime.log?.(`logged in to discord${botIdentity ? ` as ${botIdentity}` : ""}`);
lifecycleStarted = true;
await runDiscordGatewayLifecycle({

View File

@@ -53,6 +53,66 @@ describe("resolveDiscordChannelAllowlist", () => {
expect(res[0]?.channelId).toBe("123");
});
it("resolves guildId/channelId entries via channel lookup", async () => {
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
const url = urlToString(input);
if (url.endsWith("/users/@me/guilds")) {
return jsonResponse([{ id: "111", name: "Guild One" }]);
}
if (url.endsWith("/channels/222")) {
return jsonResponse({ id: "222", name: "general", guild_id: "111", type: 0 });
}
return new Response("not found", { status: 404 });
});
const res = await resolveDiscordChannelAllowlist({
token: "test",
entries: ["111/222"],
fetcher,
});
expect(res[0]).toMatchObject({
input: "111/222",
resolved: true,
guildId: "111",
channelId: "222",
channelName: "general",
guildName: "Guild One",
});
});
it("reports unresolved when channel id belongs to a different guild", async () => {
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
const url = urlToString(input);
if (url.endsWith("/users/@me/guilds")) {
return jsonResponse([
{ id: "111", name: "Guild One" },
{ id: "333", name: "Guild Two" },
]);
}
if (url.endsWith("/channels/222")) {
return jsonResponse({ id: "222", name: "general", guild_id: "333", type: 0 });
}
return new Response("not found", { status: 404 });
});
const res = await resolveDiscordChannelAllowlist({
token: "test",
entries: ["111/222"],
fetcher,
});
expect(res[0]).toMatchObject({
input: "111/222",
resolved: false,
guildId: "111",
guildName: "Guild One",
channelId: "222",
channelName: "general",
note: "channel belongs to guild Guild Two",
});
});
it("resolves guild: prefixed id as guild (not channel)", async () => {
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
const url = urlToString(input);

View File

@@ -61,6 +61,9 @@ function parseDiscordChannelInput(raw: string): {
return guild ? { guild: guild.trim(), guildOnly: true } : {};
}
if (guild && /^\d+$/.test(guild)) {
if (/^\d+$/.test(channel)) {
return { guildId: guild, channelId: channel };
}
return { guildId: guild, channel };
}
return { guild, channel };
@@ -191,6 +194,22 @@ export async function resolveDiscordChannelAllowlist(params: {
if (parsed.channelId) {
const channel = await fetchChannel(token, fetcher, parsed.channelId);
if (channel?.guildId) {
if (parsed.guildId && parsed.guildId !== channel.guildId) {
const expectedGuild = guilds.find((entry) => entry.id === parsed.guildId);
const actualGuild = guilds.find((entry) => entry.id === channel.guildId);
results.push({
input,
resolved: false,
guildId: parsed.guildId,
guildName: expectedGuild?.name,
channelId: parsed.channelId,
channelName: channel.name,
note: actualGuild?.name
? `channel belongs to guild ${actualGuild.name}`
: "channel belongs to a different guild",
});
continue;
}
const guild = guilds.find((entry) => entry.id === channel.guildId);
results.push({
input,