mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 23:04:32 +00:00
refactor(discord): split allowlist resolution flow
This commit is contained in:
@@ -108,17 +108,19 @@ export function patchAllowlistUsersInConfigEntries<
|
|||||||
if (!Array.isArray(users) || users.length === 0) {
|
if (!Array.isArray(users) || users.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const additions = resolveAllowlistIdAdditions({
|
|
||||||
existing: users,
|
|
||||||
resolvedMap: params.resolvedMap,
|
|
||||||
});
|
|
||||||
const resolvedUsers =
|
const resolvedUsers =
|
||||||
params.strategy === "canonicalize"
|
params.strategy === "canonicalize"
|
||||||
? canonicalizeAllowlistWithResolvedIds({
|
? canonicalizeAllowlistWithResolvedIds({
|
||||||
existing: users,
|
existing: users,
|
||||||
resolvedMap: params.resolvedMap,
|
resolvedMap: params.resolvedMap,
|
||||||
})
|
})
|
||||||
: mergeAllowlist({ existing: users, additions });
|
: mergeAllowlist({
|
||||||
|
existing: users,
|
||||||
|
additions: resolveAllowlistIdAdditions({
|
||||||
|
existing: users,
|
||||||
|
resolvedMap: params.resolvedMap,
|
||||||
|
}),
|
||||||
|
});
|
||||||
nextEntries[entryKey] = {
|
nextEntries[entryKey] = {
|
||||||
...entryConfig,
|
...entryConfig,
|
||||||
users: resolvedUsers,
|
users: resolvedUsers,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { resolveDiscordChannelAllowlist } from "../resolve-channels.js";
|
|||||||
import { resolveDiscordUserAllowlist } from "../resolve-users.js";
|
import { resolveDiscordUserAllowlist } from "../resolve-users.js";
|
||||||
|
|
||||||
type GuildEntries = Record<string, DiscordGuildEntry>;
|
type GuildEntries = Record<string, DiscordGuildEntry>;
|
||||||
|
type ChannelResolutionInput = { input: string; guildKey: string; channelKey?: string };
|
||||||
|
|
||||||
function toGuildEntries(value: unknown): GuildEntries {
|
function toGuildEntries(value: unknown): GuildEntries {
|
||||||
if (!value || typeof value !== "object") {
|
if (!value || typeof value !== "object") {
|
||||||
@@ -34,6 +35,204 @@ function toAllowlistEntries(value: unknown): string[] | undefined {
|
|||||||
return value.map((entry) => String(entry).trim()).filter((entry) => Boolean(entry));
|
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<GuildEntries> {
|
||||||
|
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(entry.input);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mapping.push(
|
||||||
|
entry.channelId
|
||||||
|
? `${entry.input}→${entry.guildId}/${entry.channelId}`
|
||||||
|
: `${entry.input}→${entry.guildId}`,
|
||||||
|
);
|
||||||
|
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<string[] | undefined> {
|
||||||
|
const allowEntries =
|
||||||
|
params.allowFrom?.filter((entry) => String(entry).trim() && String(entry).trim() !== "*") ?? [];
|
||||||
|
if (allowEntries.length === 0) {
|
||||||
|
return params.allowFrom;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const resolvedUsers = await resolveDiscordUserAllowlist({
|
||||||
|
token: params.token,
|
||||||
|
entries: allowEntries.map((entry) => String(entry)),
|
||||||
|
fetcher: params.fetcher,
|
||||||
|
});
|
||||||
|
const { resolvedMap, mapping, unresolved } = buildAllowlistResolutionSummary(resolvedUsers);
|
||||||
|
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<string> {
|
||||||
|
const userEntries = new Set<string>();
|
||||||
|
for (const guild of Object.values(guildEntries)) {
|
||||||
|
if (!guild || typeof guild !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
addAllowlistUserEntriesFromConfigEntry(userEntries, guild);
|
||||||
|
const channels = (guild as { channels?: Record<string, unknown> }).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<GuildEntries> {
|
||||||
|
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);
|
||||||
|
const nextGuilds = { ...params.guildEntries };
|
||||||
|
for (const [guildKey, guildConfig] of Object.entries(params.guildEntries)) {
|
||||||
|
if (!guildConfig || typeof guildConfig !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const nextGuild = { ...guildConfig } as Record<string, unknown>;
|
||||||
|
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<string, unknown> }).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: {
|
export async function resolveDiscordAllowlistConfig(params: {
|
||||||
token: string;
|
token: string;
|
||||||
guildEntries: unknown;
|
guildEntries: unknown;
|
||||||
@@ -44,169 +243,33 @@ export async function resolveDiscordAllowlistConfig(params: {
|
|||||||
let guildEntries = toGuildEntries(params.guildEntries);
|
let guildEntries = toGuildEntries(params.guildEntries);
|
||||||
let allowFrom = toAllowlistEntries(params.allowFrom);
|
let allowFrom = toAllowlistEntries(params.allowFrom);
|
||||||
|
|
||||||
if (Object.keys(guildEntries).length > 0) {
|
if (hasGuildEntries(guildEntries)) {
|
||||||
try {
|
guildEntries = await resolveGuildEntriesByChannelAllowlist({
|
||||||
const entries: Array<{ input: string; guildKey: string; channelKey?: string }> = [];
|
token: params.token,
|
||||||
for (const [guildKey, guildCfg] of Object.entries(guildEntries)) {
|
guildEntries,
|
||||||
if (guildKey === "*") {
|
fetcher: params.fetcher,
|
||||||
continue;
|
runtime: params.runtime,
|
||||||
}
|
});
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (entries.length > 0) {
|
|
||||||
const resolved = await resolveDiscordChannelAllowlist({
|
|
||||||
token: params.token,
|
|
||||||
entries: entries.map((entry) => entry.input),
|
|
||||||
fetcher: params.fetcher,
|
|
||||||
});
|
|
||||||
const nextGuilds = { ...guildEntries };
|
|
||||||
const mapping: string[] = [];
|
|
||||||
const unresolved: string[] = [];
|
|
||||||
for (const entry of resolved) {
|
|
||||||
const source = entries.find((item) => item.input === entry.input);
|
|
||||||
if (!source) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const sourceGuild = guildEntries[source.guildKey] ?? {};
|
|
||||||
if (!entry.resolved || !entry.guildId) {
|
|
||||||
unresolved.push(entry.input);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
mapping.push(
|
|
||||||
entry.channelId
|
|
||||||
? `${entry.input}→${entry.guildId}/${entry.channelId}`
|
|
||||||
: `${entry.input}→${entry.guildId}`,
|
|
||||||
);
|
|
||||||
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],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
guildEntries = nextGuilds;
|
|
||||||
summarizeMapping("discord channels", mapping, unresolved, params.runtime);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
params.runtime.log?.(
|
|
||||||
`discord channel resolve failed; using config entries. ${formatErrorMessage(err)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowEntries =
|
allowFrom = await resolveAllowFromByUserAllowlist({
|
||||||
allowFrom?.filter((entry) => String(entry).trim() && String(entry).trim() !== "*") ?? [];
|
token: params.token,
|
||||||
if (allowEntries.length > 0) {
|
allowFrom,
|
||||||
try {
|
fetcher: params.fetcher,
|
||||||
const resolvedUsers = await resolveDiscordUserAllowlist({
|
runtime: params.runtime,
|
||||||
token: params.token,
|
});
|
||||||
entries: allowEntries.map((entry) => String(entry)),
|
|
||||||
fetcher: params.fetcher,
|
|
||||||
});
|
|
||||||
const { resolvedMap, mapping, unresolved } = buildAllowlistResolutionSummary(resolvedUsers);
|
|
||||||
allowFrom = canonicalizeAllowlistWithResolvedIds({
|
|
||||||
existing: allowFrom,
|
|
||||||
resolvedMap,
|
|
||||||
});
|
|
||||||
summarizeMapping("discord users", mapping, unresolved, params.runtime);
|
|
||||||
} catch (err) {
|
|
||||||
params.runtime.log?.(
|
|
||||||
`discord user resolve failed; using config entries. ${formatErrorMessage(err)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(guildEntries).length > 0) {
|
if (hasGuildEntries(guildEntries)) {
|
||||||
const userEntries = new Set<string>();
|
guildEntries = await resolveGuildEntriesByUserAllowlist({
|
||||||
for (const guild of Object.values(guildEntries)) {
|
token: params.token,
|
||||||
if (!guild || typeof guild !== "object") {
|
guildEntries,
|
||||||
continue;
|
fetcher: params.fetcher,
|
||||||
}
|
runtime: params.runtime,
|
||||||
addAllowlistUserEntriesFromConfigEntry(userEntries, guild);
|
});
|
||||||
const channels = (guild as { channels?: Record<string, unknown> }).channels ?? {};
|
|
||||||
for (const channel of Object.values(channels)) {
|
|
||||||
addAllowlistUserEntriesFromConfigEntry(userEntries, channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userEntries.size > 0) {
|
|
||||||
try {
|
|
||||||
const resolvedUsers = await resolveDiscordUserAllowlist({
|
|
||||||
token: params.token,
|
|
||||||
entries: Array.from(userEntries),
|
|
||||||
fetcher: params.fetcher,
|
|
||||||
});
|
|
||||||
const { resolvedMap, mapping, unresolved } = buildAllowlistResolutionSummary(resolvedUsers);
|
|
||||||
|
|
||||||
const nextGuilds = { ...guildEntries };
|
|
||||||
for (const [guildKey, guildConfig] of Object.entries(guildEntries ?? {})) {
|
|
||||||
if (!guildConfig || typeof guildConfig !== "object") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const nextGuild = { ...guildConfig } as Record<string, unknown>;
|
|
||||||
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<string, unknown> }).channels ?? {};
|
|
||||||
if (channels && typeof channels === "object") {
|
|
||||||
nextGuild.channels = patchAllowlistUsersInConfigEntries({
|
|
||||||
entries: channels,
|
|
||||||
resolvedMap,
|
|
||||||
strategy: "canonicalize",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
nextGuilds[guildKey] = nextGuild as DiscordGuildEntry;
|
|
||||||
}
|
|
||||||
guildEntries = nextGuilds;
|
|
||||||
summarizeMapping("discord channel users", mapping, unresolved, params.runtime);
|
|
||||||
} catch (err) {
|
|
||||||
params.runtime.log?.(
|
|
||||||
`discord channel user resolve failed; using config entries. ${formatErrorMessage(err)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
guildEntries: Object.keys(guildEntries).length > 0 ? guildEntries : undefined,
|
guildEntries: hasGuildEntries(guildEntries) ? guildEntries : undefined,
|
||||||
allowFrom,
|
allowFrom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user