From bfad32aa16df98d063c8a199d12806bc4b166b4c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Mar 2026 22:58:23 +0000 Subject: [PATCH] refactor: share directory config listers --- extensions/discord/src/directory-config.ts | 68 +++++++-------- extensions/irc/src/channel.ts | 53 ++++++------ extensions/matrix/src/channel.ts | 85 ++++++++++--------- extensions/slack/src/directory-config.ts | 72 ++++++++-------- extensions/telegram/src/directory-config.ts | 17 ++-- .../plugins/directory-config-helpers.test.ts | 31 +++++++ .../plugins/directory-config-helpers.ts | 29 +++++++ src/plugin-sdk/directory-runtime.ts | 2 + 8 files changed, 205 insertions(+), 152 deletions(-) diff --git a/extensions/discord/src/directory-config.ts b/extensions/discord/src/directory-config.ts index 74d8b725b70..369e55b263e 100644 --- a/extensions/discord/src/directory-config.ts +++ b/extensions/discord/src/directory-config.ts @@ -1,6 +1,6 @@ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import { - listResolvedDirectoryEntriesFromSources, + createResolvedDirectoryEntriesLister, type DirectoryConfigParams, } from "openclaw/plugin-sdk/directory-runtime"; import { mergeDiscordAccountConfig, resolveDefaultDiscordAccountId } from "./accounts.js"; @@ -18,38 +18,36 @@ function resolveDiscordDirectoryConfigAccount( }; } -export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfigParams) { - return listResolvedDirectoryEntriesFromSources({ - ...params, - kind: "user", - resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId), - resolveSources: (account) => { - const allowFrom = account.config.allowFrom ?? account.config.dm?.allowFrom ?? []; - const guildUsers = Object.values(account.config.guilds ?? {}).flatMap((guild) => [ - ...(guild.users ?? []), - ...Object.values(guild.channels ?? {}).flatMap((channel) => channel.users ?? []), - ]); - return [allowFrom, Object.keys(account.config.dms ?? {}), guildUsers]; - }, - normalizeId: (raw) => { - const mention = raw.match(/^<@!?(\d+)>$/); - const cleaned = (mention?.[1] ?? raw).replace(/^(discord|user):/i, "").trim(); - return /^\d+$/.test(cleaned) ? `user:${cleaned}` : null; - }, - }); -} +export const listDiscordDirectoryPeersFromConfig = createResolvedDirectoryEntriesLister< + ReturnType +>({ + kind: "user", + resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId), + resolveSources: (account) => { + const allowFrom = account.config.allowFrom ?? account.config.dm?.allowFrom ?? []; + const guildUsers = Object.values(account.config.guilds ?? {}).flatMap((guild) => [ + ...(guild.users ?? []), + ...Object.values(guild.channels ?? {}).flatMap((channel) => channel.users ?? []), + ]); + return [allowFrom, Object.keys(account.config.dms ?? {}), guildUsers]; + }, + normalizeId: (raw) => { + const mention = raw.match(/^<@!?(\d+)>$/); + const cleaned = (mention?.[1] ?? raw).replace(/^(discord|user):/i, "").trim(); + return /^\d+$/.test(cleaned) ? `user:${cleaned}` : null; + }, +}); -export async function listDiscordDirectoryGroupsFromConfig(params: DirectoryConfigParams) { - return listResolvedDirectoryEntriesFromSources({ - ...params, - kind: "group", - resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId), - resolveSources: (account) => - Object.values(account.config.guilds ?? {}).map((guild) => Object.keys(guild.channels ?? {})), - normalizeId: (raw) => { - const mention = raw.match(/^<#(\d+)>$/); - const cleaned = (mention?.[1] ?? raw).replace(/^(discord|channel|group):/i, "").trim(); - return /^\d+$/.test(cleaned) ? `channel:${cleaned}` : null; - }, - }); -} +export const listDiscordDirectoryGroupsFromConfig = createResolvedDirectoryEntriesLister< + ReturnType +>({ + kind: "group", + resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId), + resolveSources: (account) => + Object.values(account.config.guilds ?? {}).map((guild) => Object.keys(guild.channels ?? {})), + normalizeId: (raw) => { + const mention = raw.match(/^<#(\d+)>$/); + const cleaned = (mention?.[1] ?? raw).replace(/^(discord|channel|group):/i, "").trim(); + return /^\d+$/.test(cleaned) ? `channel:${cleaned}` : null; + }, +}); diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index 2621c93bfab..751b48b5434 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -13,7 +13,7 @@ import { import { createChatChannelPlugin } from "openclaw/plugin-sdk/core"; import { createChannelDirectoryAdapter, - listResolvedDirectoryEntriesFromSources, + createResolvedDirectoryEntriesLister, } from "openclaw/plugin-sdk/directory-runtime"; import { runStoppablePassiveMonitor } from "openclaw/plugin-sdk/extension-shared"; import { @@ -61,6 +61,30 @@ function normalizePairingTarget(raw: string): string { return normalized.split(/[!@]/, 1)[0]?.trim() ?? ""; } +const listIrcDirectoryPeersFromConfig = createResolvedDirectoryEntriesLister({ + kind: "user", + resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount), + resolveSources: (account) => [ + account.config.allowFrom ?? [], + account.config.groupAllowFrom ?? [], + ...Object.values(account.config.groups ?? {}).map((group) => group.allowFrom ?? []), + ], + normalizeId: (entry) => normalizePairingTarget(entry) || null, +}); + +const listIrcDirectoryGroupsFromConfig = createResolvedDirectoryEntriesLister({ + kind: "group", + resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount), + resolveSources: (account) => [ + account.config.channels ?? [], + Object.keys(account.config.groups ?? {}), + ], + normalizeId: (entry) => { + const normalized = normalizeIrcMessagingTarget(entry); + return normalized && isChannelTarget(normalized) ? normalized : null; + }, +}); + const ircConfigAdapter = createScopedChannelConfigAdapter< ResolvedIrcAccount, ResolvedIrcAccount, @@ -227,32 +251,9 @@ export const ircPlugin: ChannelPlugin = createChat }, }, directory: createChannelDirectoryAdapter({ - listPeers: async (params) => - listResolvedDirectoryEntriesFromSources({ - ...params, - kind: "user", - resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount), - resolveSources: (account) => [ - account.config.allowFrom ?? [], - account.config.groupAllowFrom ?? [], - ...Object.values(account.config.groups ?? {}).map((group) => group.allowFrom ?? []), - ], - normalizeId: (entry) => normalizePairingTarget(entry) || null, - }), + listPeers: async (params) => listIrcDirectoryPeersFromConfig(params), listGroups: async (params) => { - const entries = listResolvedDirectoryEntriesFromSources({ - ...params, - kind: "group", - resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount), - resolveSources: (account) => [ - account.config.channels ?? [], - Object.keys(account.config.groups ?? {}), - ], - normalizeId: (entry) => { - const normalized = normalizeIrcMessagingTarget(entry); - return normalized && isChannelTarget(normalized) ? normalized : null; - }, - }); + const entries = await listIrcDirectoryGroupsFromConfig(params); return entries.map((entry) => ({ ...entry, name: entry.id })); }, }), diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index 4dba35df6d2..981994a8655 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -16,8 +16,8 @@ import { createScopedAccountReplyToModeResolver } from "openclaw/plugin-sdk/conv import { createChatChannelPlugin } from "openclaw/plugin-sdk/core"; import { createChannelDirectoryAdapter, + createResolvedDirectoryEntriesLister, createRuntimeDirectoryLiveAdapter, - listResolvedDirectoryEntriesFromSources, } from "openclaw/plugin-sdk/directory-runtime"; import { buildTrafficStatusSummary } from "openclaw/plugin-sdk/extension-shared"; import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime"; @@ -78,6 +78,46 @@ const meta = { quickstartAllowFrom: true, }; +const listMatrixDirectoryPeersFromConfig = + createResolvedDirectoryEntriesLister({ + kind: "user", + resolveAccount: adaptScopedAccountAccessor(resolveMatrixAccount), + resolveSources: (account) => [ + account.config.dm?.allowFrom ?? [], + account.config.groupAllowFrom ?? [], + ...Object.values(account.config.groups ?? account.config.rooms ?? {}).map( + (room) => room.users ?? [], + ), + ], + normalizeId: (entry) => { + const raw = entry.replace(/^matrix:/i, "").trim(); + if (!raw || raw === "*") { + return null; + } + const lowered = raw.toLowerCase(); + const cleaned = lowered.startsWith("user:") ? raw.slice("user:".length).trim() : raw; + return cleaned.startsWith("@") ? `user:${cleaned}` : cleaned; + }, + }); + +const listMatrixDirectoryGroupsFromConfig = + createResolvedDirectoryEntriesLister({ + kind: "group", + resolveAccount: adaptScopedAccountAccessor(resolveMatrixAccount), + resolveSources: (account) => [Object.keys(account.config.groups ?? account.config.rooms ?? {})], + normalizeId: (entry) => { + const raw = entry.replace(/^matrix:/i, "").trim(); + if (!raw || raw === "*") { + return null; + } + const lowered = raw.toLowerCase(); + if (lowered.startsWith("room:") || lowered.startsWith("channel:")) { + return raw; + } + return raw.startsWith("!") ? `room:${raw}` : raw; + }, + }); + const matrixConfigAdapter = createScopedChannelConfigAdapter< ResolvedMatrixAccount, ReturnType, @@ -246,53 +286,14 @@ export const matrixPlugin: ChannelPlugin = }, directory: createChannelDirectoryAdapter({ listPeers: async (params) => { - const entries = listResolvedDirectoryEntriesFromSources({ - ...params, - kind: "user", - resolveAccount: adaptScopedAccountAccessor(resolveMatrixAccount), - resolveSources: (account) => [ - account.config.dm?.allowFrom ?? [], - account.config.groupAllowFrom ?? [], - ...Object.values(account.config.groups ?? account.config.rooms ?? {}).map( - (room) => room.users ?? [], - ), - ], - normalizeId: (entry) => { - const raw = entry.replace(/^matrix:/i, "").trim(); - if (!raw || raw === "*") { - return null; - } - const lowered = raw.toLowerCase(); - const cleaned = lowered.startsWith("user:") ? raw.slice("user:".length).trim() : raw; - return cleaned.startsWith("@") ? `user:${cleaned}` : cleaned; - }, - }); + const entries = await listMatrixDirectoryPeersFromConfig(params); return entries.map((entry) => { const raw = entry.id.startsWith("user:") ? entry.id.slice("user:".length) : entry.id; const incomplete = !raw.startsWith("@") || !raw.includes(":"); return incomplete ? { ...entry, name: "incomplete id; expected @user:server" } : entry; }); }, - listGroups: async (params) => - listResolvedDirectoryEntriesFromSources({ - ...params, - kind: "group", - resolveAccount: adaptScopedAccountAccessor(resolveMatrixAccount), - resolveSources: (account) => [ - Object.keys(account.config.groups ?? account.config.rooms ?? {}), - ], - normalizeId: (entry) => { - const raw = entry.replace(/^matrix:/i, "").trim(); - if (!raw || raw === "*") { - return null; - } - const lowered = raw.toLowerCase(); - if (lowered.startsWith("room:") || lowered.startsWith("channel:")) { - return raw; - } - return raw.startsWith("!") ? `room:${raw}` : raw; - }, - }), + listGroups: async (params) => await listMatrixDirectoryGroupsFromConfig(params), ...createRuntimeDirectoryLiveAdapter({ getRuntime: loadMatrixChannelRuntime, listPeersLive: (runtime) => runtime.listMatrixDirectoryPeersLive, diff --git a/extensions/slack/src/directory-config.ts b/extensions/slack/src/directory-config.ts index 0bf3cf35e1c..dcfa85d860f 100644 --- a/extensions/slack/src/directory-config.ts +++ b/extensions/slack/src/directory-config.ts @@ -1,6 +1,6 @@ import { normalizeAccountId } from "openclaw/plugin-sdk/account-resolution"; import { - listResolvedDirectoryEntriesFromSources, + createResolvedDirectoryEntriesLister, type DirectoryConfigParams, } from "openclaw/plugin-sdk/directory-runtime"; import { mergeSlackAccountConfig, resolveDefaultSlackAccountId } from "./accounts.js"; @@ -19,40 +19,38 @@ function resolveSlackDirectoryConfigAccount( }; } -export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigParams) { - return listResolvedDirectoryEntriesFromSources({ - ...params, - kind: "user", - resolveAccount: (cfg, accountId) => resolveSlackDirectoryConfigAccount(cfg, accountId), - resolveSources: (account) => { - const allowFrom = account.config.allowFrom ?? account.dm?.allowFrom ?? []; - const channelUsers = Object.values(account.config.channels ?? {}).flatMap( - (channel) => channel.users ?? [], - ); - return [allowFrom, Object.keys(account.config.dms ?? {}), channelUsers]; - }, - normalizeId: (raw) => { - const mention = raw.match(/^<@([A-Z0-9]+)>$/i); - const normalizedUserId = (mention?.[1] ?? raw).replace(/^(slack|user):/i, "").trim(); - if (!normalizedUserId) { - return null; - } - const target = `user:${normalizedUserId}`; - const normalized = parseSlackTarget(target, { defaultKind: "user" }); - return normalized?.kind === "user" ? `user:${normalized.id.toLowerCase()}` : null; - }, - }); -} +export const listSlackDirectoryPeersFromConfig = createResolvedDirectoryEntriesLister< + ReturnType +>({ + kind: "user", + resolveAccount: (cfg, accountId) => resolveSlackDirectoryConfigAccount(cfg, accountId), + resolveSources: (account) => { + const allowFrom = account.config.allowFrom ?? account.dm?.allowFrom ?? []; + const channelUsers = Object.values(account.config.channels ?? {}).flatMap( + (channel) => channel.users ?? [], + ); + return [allowFrom, Object.keys(account.config.dms ?? {}), channelUsers]; + }, + normalizeId: (raw) => { + const mention = raw.match(/^<@([A-Z0-9]+)>$/i); + const normalizedUserId = (mention?.[1] ?? raw).replace(/^(slack|user):/i, "").trim(); + if (!normalizedUserId) { + return null; + } + const target = `user:${normalizedUserId}`; + const normalized = parseSlackTarget(target, { defaultKind: "user" }); + return normalized?.kind === "user" ? `user:${normalized.id.toLowerCase()}` : null; + }, +}); -export async function listSlackDirectoryGroupsFromConfig(params: DirectoryConfigParams) { - return listResolvedDirectoryEntriesFromSources({ - ...params, - kind: "group", - resolveAccount: (cfg, accountId) => resolveSlackDirectoryConfigAccount(cfg, accountId), - resolveSources: (account) => [Object.keys(account.config.channels ?? {})], - normalizeId: (raw) => { - const normalized = parseSlackTarget(raw, { defaultKind: "channel" }); - return normalized?.kind === "channel" ? `channel:${normalized.id.toLowerCase()}` : null; - }, - }); -} +export const listSlackDirectoryGroupsFromConfig = createResolvedDirectoryEntriesLister< + ReturnType +>({ + kind: "group", + resolveAccount: (cfg, accountId) => resolveSlackDirectoryConfigAccount(cfg, accountId), + resolveSources: (account) => [Object.keys(account.config.channels ?? {})], + normalizeId: (raw) => { + const normalized = parseSlackTarget(raw, { defaultKind: "channel" }); + return normalized?.kind === "channel" ? `channel:${normalized.id.toLowerCase()}` : null; + }, +}); diff --git a/extensions/telegram/src/directory-config.ts b/extensions/telegram/src/directory-config.ts index cbd15569117..aba2fca3de1 100644 --- a/extensions/telegram/src/directory-config.ts +++ b/extensions/telegram/src/directory-config.ts @@ -1,13 +1,9 @@ import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers"; -import { - listInspectedDirectoryEntriesFromSources, - type DirectoryConfigParams, -} from "openclaw/plugin-sdk/directory-runtime"; +import { createInspectedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-runtime"; import { inspectTelegramAccount, type InspectedTelegramAccount } from "./account-inspect.js"; -export async function listTelegramDirectoryPeersFromConfig(params: DirectoryConfigParams) { - return listInspectedDirectoryEntriesFromSources({ - ...params, +export const listTelegramDirectoryPeersFromConfig = + createInspectedDirectoryEntriesLister({ kind: "user", inspectAccount: (cfg, accountId) => inspectTelegramAccount({ cfg, accountId }) as InspectedTelegramAccount | null, @@ -26,15 +22,12 @@ export async function listTelegramDirectoryPeersFromConfig(params: DirectoryConf return trimmed.startsWith("@") ? trimmed : `@${trimmed}`; }, }); -} -export async function listTelegramDirectoryGroupsFromConfig(params: DirectoryConfigParams) { - return listInspectedDirectoryEntriesFromSources({ - ...params, +export const listTelegramDirectoryGroupsFromConfig = + createInspectedDirectoryEntriesLister({ kind: "group", inspectAccount: (cfg, accountId) => inspectTelegramAccount({ cfg, accountId }) as InspectedTelegramAccount | null, resolveSources: (account) => [Object.keys(account.config.groups ?? {})], normalizeId: (entry) => entry.trim() || null, }); -} diff --git a/src/channels/plugins/directory-config-helpers.test.ts b/src/channels/plugins/directory-config-helpers.test.ts index 5fadc922328..ae6f5064d5c 100644 --- a/src/channels/plugins/directory-config-helpers.test.ts +++ b/src/channels/plugins/directory-config-helpers.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; import { + createInspectedDirectoryEntriesLister, + createResolvedDirectoryEntriesLister, listDirectoryEntriesFromSources, listInspectedDirectoryEntriesFromSources, listDirectoryGroupEntriesFromMapKeysAndAllowFrom, @@ -128,6 +130,21 @@ describe("listInspectedDirectoryEntriesFromSources", () => { }); }); +describe("createInspectedDirectoryEntriesLister", () => { + it("builds a reusable inspected-account lister", async () => { + const listGroups = createInspectedDirectoryEntriesLister({ + kind: "group", + inspectAccount: () => ({ ids: [["room:a"], ["room:b", "room:a"]] }), + resolveSources: (account) => account.ids, + normalizeId: (entry) => entry.replace(/^room:/i, ""), + }); + + await expect(listGroups({ cfg: {} as never, query: "a" })).resolves.toEqual([ + { kind: "group", id: "a" }, + ]); + }); +}); + describe("resolved account directory helpers", () => { const cfg = {} as never; const resolveAccount = () => ({ @@ -174,4 +191,18 @@ describe("resolved account directory helpers", () => { expectUserDirectoryEntries(entries); }); + + it("builds a reusable resolved-account lister", async () => { + const listUsers = createResolvedDirectoryEntriesLister({ + kind: "user", + resolveAccount, + resolveSources: (account) => [account.allowFrom, ["user:carla", "user:alice"]], + normalizeId: (entry) => entry.replace(/^user:/i, ""), + }); + + await expect(listUsers({ cfg, query: "a", limit: 2 })).resolves.toEqual([ + { kind: "user", id: "alice" }, + { kind: "user", id: "carla" }, + ]); + }); }); diff --git a/src/channels/plugins/directory-config-helpers.ts b/src/channels/plugins/directory-config-helpers.ts index 6ee329e578a..3b2cde638a8 100644 --- a/src/channels/plugins/directory-config-helpers.ts +++ b/src/channels/plugins/directory-config-helpers.ts @@ -121,6 +121,22 @@ export function listInspectedDirectoryEntriesFromSources( }); } +export function createInspectedDirectoryEntriesLister(params: { + kind: "user" | "group"; + inspectAccount: ( + cfg: OpenClawConfig, + accountId?: string | null, + ) => InspectedAccount | null | undefined; + resolveSources: (account: InspectedAccount) => Iterable[]; + normalizeId: (entry: string) => string | null | undefined; +}) { + return async (configParams: DirectoryConfigParams): Promise => + listInspectedDirectoryEntriesFromSources({ + ...configParams, + ...params, + }); +} + export function listResolvedDirectoryEntriesFromSources( params: DirectoryConfigParams & { kind: "user" | "group"; @@ -139,6 +155,19 @@ export function listResolvedDirectoryEntriesFromSources( }); } +export function createResolvedDirectoryEntriesLister(params: { + kind: "user" | "group"; + resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => ResolvedAccount; + resolveSources: (account: ResolvedAccount) => Iterable[]; + normalizeId: (entry: string) => string | null | undefined; +}) { + return async (configParams: DirectoryConfigParams): Promise => + listResolvedDirectoryEntriesFromSources({ + ...configParams, + ...params, + }); +} + export function listDirectoryUserEntriesFromAllowFrom(params: { allowFrom?: readonly unknown[]; query?: string | null; diff --git a/src/plugin-sdk/directory-runtime.ts b/src/plugin-sdk/directory-runtime.ts index 31209a89561..308de99ff67 100644 --- a/src/plugin-sdk/directory-runtime.ts +++ b/src/plugin-sdk/directory-runtime.ts @@ -14,6 +14,8 @@ export { export { applyDirectoryQueryAndLimit, collectNormalizedDirectoryIds, + createInspectedDirectoryEntriesLister, + createResolvedDirectoryEntriesLister, listDirectoryEntriesFromSources, listDirectoryGroupEntriesFromMapKeys, listDirectoryGroupEntriesFromMapKeysAndAllowFrom,