mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(discord): enrich allowlist resolution logs
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user