From 2b5fa0931df6bf6ecd611fb3edd905904a026187 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 18 Mar 2026 04:27:29 +0000 Subject: [PATCH] Plugins: move config-backed directories behind channel plugins --- extensions/discord/src/channel.ts | 6 +- extensions/discord/src/directory-config.ts | 57 ++++ extensions/discord/src/directory-live.test.ts | 2 +- extensions/discord/src/runtime-api.ts | 6 +- extensions/slack/src/channel.ts | 6 +- extensions/slack/src/directory-config.ts | 60 ++++ extensions/telegram/src/channel.ts | 6 +- extensions/telegram/src/directory-config.ts | 52 ++++ extensions/whatsapp/src/channel.ts | 6 +- extensions/whatsapp/src/directory-config.ts | 32 +++ .../plugins/directory-config-helpers.ts | 21 ++ src/channels/plugins/directory-config.ts | 263 ------------------ src/channels/plugins/directory-types.ts | 8 + src/channels/plugins/index.ts | 10 - src/channels/plugins/plugins-core.test.ts | 26 +- src/channels/targets.ts | 2 +- src/plugin-sdk/channel-runtime.ts | 2 +- src/plugin-sdk/directory-runtime.ts | 2 + src/plugin-sdk/discord.ts | 9 +- src/plugin-sdk/slack.ts | 8 +- src/plugin-sdk/telegram.ts | 9 +- src/plugin-sdk/whatsapp.ts | 4 +- 22 files changed, 285 insertions(+), 312 deletions(-) create mode 100644 extensions/discord/src/directory-config.ts create mode 100644 extensions/slack/src/directory-config.ts create mode 100644 extensions/telegram/src/directory-config.ts create mode 100644 extensions/whatsapp/src/directory-config.ts delete mode 100644 src/channels/plugins/directory-config.ts create mode 100644 src/channels/plugins/directory-types.ts diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 05b9171c17e..5116b559b60 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -18,6 +18,10 @@ import { type ResolvedDiscordAccount, } from "./accounts.js"; import { auditDiscordChannelPermissions, collectDiscordAuditChannelIds } from "./audit.js"; +import { + listDiscordDirectoryGroupsFromConfig, + listDiscordDirectoryPeersFromConfig, +} from "./directory-config.js"; import { isDiscordExecApprovalClientEnabled, shouldSuppressLocalDiscordExecApprovalPrompt, @@ -41,8 +45,6 @@ import { type ChannelPlugin, DEFAULT_ACCOUNT_ID, getChatChannelMeta, - listDiscordDirectoryGroupsFromConfig, - listDiscordDirectoryPeersFromConfig, PAIRING_APPROVED_MESSAGE, projectCredentialSnapshotFields, resolveConfiguredFromCredentialStatuses, diff --git a/extensions/discord/src/directory-config.ts b/extensions/discord/src/directory-config.ts new file mode 100644 index 00000000000..8828a1854eb --- /dev/null +++ b/extensions/discord/src/directory-config.ts @@ -0,0 +1,57 @@ +import { + applyDirectoryQueryAndLimit, + collectNormalizedDirectoryIds, + toDirectoryEntries, + type DirectoryConfigParams, +} from "openclaw/plugin-sdk/directory-runtime"; +import type { InspectedDiscordAccount } from "../../../src/channels/read-only-account-inspect.discord.runtime.js"; +import { inspectReadOnlyChannelAccount } from "../../../src/channels/read-only-account-inspect.js"; + +export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfigParams) { + const account = (await inspectReadOnlyChannelAccount({ + channelId: "discord", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedDiscordAccount | null; + if (!account || !("config" in account)) { + return []; + } + + 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 ?? []), + ]); + const ids = collectNormalizedDirectoryIds({ + sources: [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; + }, + }); + return toDirectoryEntries("user", applyDirectoryQueryAndLimit(ids, params)); +} + +export async function listDiscordDirectoryGroupsFromConfig(params: DirectoryConfigParams) { + const account = (await inspectReadOnlyChannelAccount({ + channelId: "discord", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedDiscordAccount | null; + if (!account || !("config" in account)) { + return []; + } + + const ids = collectNormalizedDirectoryIds({ + sources: 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; + }, + }); + return toDirectoryEntries("group", applyDirectoryQueryAndLimit(ids, params)); +} diff --git a/extensions/discord/src/directory-live.test.ts b/extensions/discord/src/directory-live.test.ts index afc0fd94170..36f1f821795 100644 --- a/extensions/discord/src/directory-live.test.ts +++ b/extensions/discord/src/directory-live.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { DirectoryConfigParams } from "../../../src/channels/plugins/directory-config.js"; import type { OpenClawConfig } from "../../../src/config/config.js"; +import type { DirectoryConfigParams } from "../../../src/plugin-sdk/directory-runtime.js"; import { listDiscordDirectoryGroupsLive, listDiscordDirectoryPeersLive } from "./directory-live.js"; function makeParams(overrides: Partial = {}): DirectoryConfigParams { diff --git a/extensions/discord/src/runtime-api.ts b/extensions/discord/src/runtime-api.ts index 3e277cfea3c..f2676220cdb 100644 --- a/extensions/discord/src/runtime-api.ts +++ b/extensions/discord/src/runtime-api.ts @@ -1,8 +1,6 @@ export { buildComputedAccountStatusSnapshot, buildTokenChannelStatusSummary, - listDiscordDirectoryGroupsFromConfig, - listDiscordDirectoryPeersFromConfig, PAIRING_APPROVED_MESSAGE, projectCredentialSnapshotFields, resolveConfiguredFromCredentialStatuses, @@ -20,6 +18,10 @@ export { } from "openclaw/plugin-sdk/discord-core"; export { DiscordConfigSchema } from "openclaw/plugin-sdk/discord-core"; export { readBooleanParam } from "openclaw/plugin-sdk/boolean-param"; +export { + listDiscordDirectoryGroupsFromConfig, + listDiscordDirectoryPeersFromConfig, +} from "./directory-config.js"; export { createScopedAccountConfigAccessors, createScopedChannelConfigBase, diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 6024f7b5ed6..0548d835b21 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -13,8 +13,6 @@ import { resolveThreadSessionKeys, type RoutePeer } from "openclaw/plugin-sdk/ro import { buildComputedAccountStatusSnapshot, DEFAULT_ACCOUNT_ID, - listSlackDirectoryGroupsFromConfig, - listSlackDirectoryPeersFromConfig, looksLikeSlackTargetId, normalizeSlackMessagingTarget, PAIRING_APPROVED_MESSAGE, @@ -34,6 +32,10 @@ import { import { parseSlackBlocksInput } from "./blocks-input.js"; import { createSlackActions } from "./channel-actions.js"; import { createSlackWebClient } from "./client.js"; +import { + listSlackDirectoryGroupsFromConfig, + listSlackDirectoryPeersFromConfig, +} from "./directory-config.js"; import { resolveSlackGroupRequireMention, resolveSlackGroupToolPolicy } from "./group-policy.js"; import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js"; import { normalizeAllowListLower } from "./monitor/allow-list.js"; diff --git a/extensions/slack/src/directory-config.ts b/extensions/slack/src/directory-config.ts new file mode 100644 index 00000000000..635222f9c2e --- /dev/null +++ b/extensions/slack/src/directory-config.ts @@ -0,0 +1,60 @@ +import { + applyDirectoryQueryAndLimit, + collectNormalizedDirectoryIds, + listDirectoryGroupEntriesFromMapKeys, + toDirectoryEntries, + type DirectoryConfigParams, +} from "openclaw/plugin-sdk/directory-runtime"; +import { normalizeSlackMessagingTarget } from "../../../src/channels/plugins/normalize/slack.js"; +import { inspectReadOnlyChannelAccount } from "../../../src/channels/read-only-account-inspect.js"; +import type { InspectedSlackAccount } from "../../../src/channels/read-only-account-inspect.slack.runtime.js"; + +export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigParams) { + const account = (await inspectReadOnlyChannelAccount({ + channelId: "slack", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedSlackAccount | null; + if (!account || !("config" in account)) { + return []; + } + + const allowFrom = account.config.allowFrom ?? account.dm?.allowFrom ?? []; + const channelUsers = Object.values(account.config.channels ?? {}).flatMap( + (channel) => channel.users ?? [], + ); + const ids = collectNormalizedDirectoryIds({ + sources: [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 = normalizeSlackMessagingTarget(target) ?? target.toLowerCase(); + return normalized.startsWith("user:") ? normalized : null; + }, + }); + return toDirectoryEntries("user", applyDirectoryQueryAndLimit(ids, params)); +} + +export async function listSlackDirectoryGroupsFromConfig(params: DirectoryConfigParams) { + const account = (await inspectReadOnlyChannelAccount({ + channelId: "slack", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedSlackAccount | null; + if (!account || !("config" in account)) { + return []; + } + return listDirectoryGroupEntriesFromMapKeys({ + groups: account.config.channels, + query: params.query, + limit: params.limit, + normalizeId: (raw) => { + const normalized = normalizeSlackMessagingTarget(raw) ?? raw.toLowerCase(); + return normalized.startsWith("channel:") ? normalized : null; + }, + }); +} diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index f8b982e5276..f9946dfa1d6 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -15,8 +15,6 @@ import { buildTokenChannelStatusSummary, clearAccountEntryFields, DEFAULT_ACCOUNT_ID, - listTelegramDirectoryGroupsFromConfig, - listTelegramDirectoryPeersFromConfig, PAIRING_APPROVED_MESSAGE, projectCredentialSnapshotFields, resolveConfiguredFromCredentialStatuses, @@ -32,6 +30,10 @@ import { import { buildTelegramExecApprovalButtons } from "./approval-buttons.js"; import { auditTelegramGroupMembership, collectTelegramUnmentionedGroupIds } from "./audit.js"; import { buildTelegramGroupPeerId } from "./bot/helpers.js"; +import { + listTelegramDirectoryGroupsFromConfig, + listTelegramDirectoryPeersFromConfig, +} from "./directory-config.js"; import { isTelegramExecApprovalClientEnabled, resolveTelegramExecApprovalTarget, diff --git a/extensions/telegram/src/directory-config.ts b/extensions/telegram/src/directory-config.ts new file mode 100644 index 00000000000..10abc88d784 --- /dev/null +++ b/extensions/telegram/src/directory-config.ts @@ -0,0 +1,52 @@ +import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers"; +import { + applyDirectoryQueryAndLimit, + collectNormalizedDirectoryIds, + listDirectoryGroupEntriesFromMapKeys, + toDirectoryEntries, + type DirectoryConfigParams, +} from "openclaw/plugin-sdk/directory-runtime"; +import { inspectReadOnlyChannelAccount } from "../../../src/channels/read-only-account-inspect.js"; +import type { InspectedTelegramAccount } from "../../../src/channels/read-only-account-inspect.telegram.runtime.js"; + +export async function listTelegramDirectoryPeersFromConfig(params: DirectoryConfigParams) { + const account = (await inspectReadOnlyChannelAccount({ + channelId: "telegram", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedTelegramAccount | null; + if (!account || !("config" in account)) { + return []; + } + + const ids = collectNormalizedDirectoryIds({ + sources: [mapAllowFromEntries(account.config.allowFrom), Object.keys(account.config.dms ?? {})], + normalizeId: (entry) => { + const trimmed = entry.replace(/^(telegram|tg):/i, "").trim(); + if (!trimmed) { + return null; + } + if (/^-?\d+$/.test(trimmed)) { + return trimmed; + } + return trimmed.startsWith("@") ? trimmed : `@${trimmed}`; + }, + }); + return toDirectoryEntries("user", applyDirectoryQueryAndLimit(ids, params)); +} + +export async function listTelegramDirectoryGroupsFromConfig(params: DirectoryConfigParams) { + const account = (await inspectReadOnlyChannelAccount({ + channelId: "telegram", + cfg: params.cfg, + accountId: params.accountId, + })) as InspectedTelegramAccount | null; + if (!account || !("config" in account)) { + return []; + } + return listDirectoryGroupEntriesFromMapKeys({ + groups: account.config.groups, + query: params.query, + limit: params.limit, + }); +} diff --git a/extensions/whatsapp/src/channel.ts b/extensions/whatsapp/src/channel.ts index 45a5f6b61b8..6361d3de1a3 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -4,8 +4,6 @@ import { createWhatsAppOutboundBase, DEFAULT_ACCOUNT_ID, formatWhatsAppConfigAllowFromEntries, - listWhatsAppDirectoryGroupsFromConfig, - listWhatsAppDirectoryPeersFromConfig, readStringParam, resolveWhatsAppOutboundTarget, resolveWhatsAppHeartbeatRecipients, @@ -16,6 +14,10 @@ import { import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "openclaw/plugin-sdk/whatsapp"; // WhatsApp-specific imports from local extension code (moved from src/web/ and src/channels/plugins/) import { resolveWhatsAppAccount, type ResolvedWhatsAppAccount } from "./accounts.js"; +import { + listWhatsAppDirectoryGroupsFromConfig, + listWhatsAppDirectoryPeersFromConfig, +} from "./directory-config.js"; import { looksLikeWhatsAppTargetId, normalizeWhatsAppMessagingTarget } from "./normalize.js"; import { getWhatsAppRuntime } from "./runtime.js"; import { resolveWhatsAppOutboundSessionRoute } from "./session-route.js"; diff --git a/extensions/whatsapp/src/directory-config.ts b/extensions/whatsapp/src/directory-config.ts new file mode 100644 index 00000000000..ad7b7d257e7 --- /dev/null +++ b/extensions/whatsapp/src/directory-config.ts @@ -0,0 +1,32 @@ +import { + listDirectoryGroupEntriesFromMapKeys, + listDirectoryUserEntriesFromAllowFrom, + type DirectoryConfigParams, +} from "openclaw/plugin-sdk/directory-runtime"; +import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../../src/whatsapp/normalize.js"; +import { resolveWhatsAppAccount } from "./accounts.js"; + +export async function listWhatsAppDirectoryPeersFromConfig(params: DirectoryConfigParams) { + const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId }); + return listDirectoryUserEntriesFromAllowFrom({ + allowFrom: account.allowFrom, + query: params.query, + limit: params.limit, + normalizeId: (entry) => { + const normalized = normalizeWhatsAppTarget(entry); + if (!normalized || isWhatsAppGroupJid(normalized)) { + return null; + } + return normalized; + }, + }); +} + +export async function listWhatsAppDirectoryGroupsFromConfig(params: DirectoryConfigParams) { + const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId }); + return listDirectoryGroupEntriesFromMapKeys({ + groups: account.groups, + query: params.query, + limit: params.limit, + }); +} diff --git a/src/channels/plugins/directory-config-helpers.ts b/src/channels/plugins/directory-config-helpers.ts index edfab553677..94dc5c3324c 100644 --- a/src/channels/plugins/directory-config-helpers.ts +++ b/src/channels/plugins/directory-config-helpers.ts @@ -60,6 +60,27 @@ function dedupeDirectoryIds(ids: string[]): string[] { return Array.from(new Set(ids)); } +export function collectNormalizedDirectoryIds(params: { + sources: Iterable[]; + normalizeId: (entry: string) => string | null | undefined; +}): string[] { + const ids = new Set(); + for (const source of params.sources) { + for (const value of source) { + const raw = String(value).trim(); + if (!raw || raw === "*") { + continue; + } + const normalized = params.normalizeId(raw); + const trimmed = typeof normalized === "string" ? normalized.trim() : ""; + if (trimmed) { + ids.add(trimmed); + } + } + } + return Array.from(ids); +} + export function listDirectoryUserEntriesFromAllowFrom(params: { allowFrom?: readonly unknown[]; query?: string | null; diff --git a/src/channels/plugins/directory-config.ts b/src/channels/plugins/directory-config.ts deleted file mode 100644 index 04dc4c5afb2..00000000000 --- a/src/channels/plugins/directory-config.ts +++ /dev/null @@ -1,263 +0,0 @@ -import type { OpenClawConfig } from "../../config/types.js"; -import { mapAllowFromEntries } from "../../plugin-sdk/channel-config-helpers.js"; -import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js"; -import type { InspectedDiscordAccount } from "../read-only-account-inspect.discord.runtime.js"; -import { inspectReadOnlyChannelAccount } from "../read-only-account-inspect.js"; -import type { InspectedSlackAccount } from "../read-only-account-inspect.slack.runtime.js"; -import type { InspectedTelegramAccount } from "../read-only-account-inspect.telegram.runtime.js"; -import { applyDirectoryQueryAndLimit, toDirectoryEntries } from "./directory-config-helpers.js"; -import { normalizeSlackMessagingTarget } from "./normalize/slack.js"; -import { getChannelPlugin } from "./registry.js"; -import type { ChannelDirectoryEntry } from "./types.js"; - -export type DirectoryConfigParams = { - cfg: OpenClawConfig; - accountId?: string | null; - query?: string | null; - limit?: number | null; -}; - -function addAllowFromAndDmsIds( - ids: Set, - allowFrom: readonly unknown[] | undefined, - dms: Record | undefined, -) { - for (const entry of allowFrom ?? []) { - const raw = String(entry).trim(); - if (!raw || raw === "*") { - continue; - } - ids.add(raw); - } - addTrimmedEntries(ids, Object.keys(dms ?? {})); -} - -function addTrimmedId(ids: Set, value: unknown) { - const trimmed = String(value).trim(); - if (trimmed) { - ids.add(trimmed); - } -} - -function addTrimmedEntries(ids: Set, values: Iterable) { - for (const value of values) { - addTrimmedId(ids, value); - } -} - -function normalizeTrimmedSet( - ids: Set, - normalize: (raw: string) => string | null, -): string[] { - return Array.from(ids) - .map((raw) => raw.trim()) - .filter(Boolean) - .map((raw) => normalize(raw)) - .filter((id): id is string => Boolean(id)); -} - -function objectValues(value: Record | undefined): T[] { - return Object.values(value ?? {}); -} - -export async function listSlackDirectoryPeersFromConfig( - params: DirectoryConfigParams, -): Promise { - const account = (await inspectReadOnlyChannelAccount({ - channelId: "slack", - cfg: params.cfg, - accountId: params.accountId, - })) as InspectedSlackAccount | null; - if (!account || !("config" in account)) { - return []; - } - const ids = new Set(); - - addAllowFromAndDmsIds(ids, account.config.allowFrom ?? account.dm?.allowFrom, account.config.dms); - for (const channel of Object.values(account.config.channels ?? {})) { - addTrimmedEntries(ids, channel.users ?? []); - } - - const normalizedIds = normalizeTrimmedSet(ids, (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}`; - return normalizeSlackMessagingTarget(target) ?? target.toLowerCase(); - }).filter((id) => id.startsWith("user:")); - return toDirectoryEntries("user", applyDirectoryQueryAndLimit(normalizedIds, params)); -} - -export async function listSlackDirectoryGroupsFromConfig( - params: DirectoryConfigParams, -): Promise { - const account = (await inspectReadOnlyChannelAccount({ - channelId: "slack", - cfg: params.cfg, - accountId: params.accountId, - })) as InspectedSlackAccount | null; - if (!account || !("config" in account)) { - return []; - } - const ids = Object.keys(account.config.channels ?? {}) - .map((raw) => raw.trim()) - .filter(Boolean) - .map((raw) => normalizeSlackMessagingTarget(raw) ?? raw.toLowerCase()) - .filter((id) => id.startsWith("channel:")); - return toDirectoryEntries("group", applyDirectoryQueryAndLimit(ids, params)); -} - -export async function listDiscordDirectoryPeersFromConfig( - params: DirectoryConfigParams, -): Promise { - const account = (await inspectReadOnlyChannelAccount({ - channelId: "discord", - cfg: params.cfg, - accountId: params.accountId, - })) as InspectedDiscordAccount | null; - if (!account || !("config" in account)) { - return []; - } - const ids = new Set(); - - addAllowFromAndDmsIds( - ids, - account.config.allowFrom ?? account.config.dm?.allowFrom, - account.config.dms, - ); - for (const guild of objectValues(account.config.guilds)) { - addTrimmedEntries(ids, guild.users ?? []); - for (const channel of objectValues(guild.channels)) { - addTrimmedEntries(ids, channel.users ?? []); - } - } - - const normalizedIds = normalizeTrimmedSet(ids, (raw) => { - const mention = raw.match(/^<@!?(\d+)>$/); - const cleaned = (mention?.[1] ?? raw).replace(/^(discord|user):/i, "").trim(); - if (!/^\d+$/.test(cleaned)) { - return null; - } - return `user:${cleaned}`; - }); - return toDirectoryEntries("user", applyDirectoryQueryAndLimit(normalizedIds, params)); -} - -export async function listDiscordDirectoryGroupsFromConfig( - params: DirectoryConfigParams, -): Promise { - const account = (await inspectReadOnlyChannelAccount({ - channelId: "discord", - cfg: params.cfg, - accountId: params.accountId, - })) as InspectedDiscordAccount | null; - if (!account || !("config" in account)) { - return []; - } - const ids = new Set(); - for (const guild of objectValues(account.config.guilds)) { - addTrimmedEntries(ids, Object.keys(guild.channels ?? {})); - } - - const normalizedIds = normalizeTrimmedSet(ids, (raw) => { - const mention = raw.match(/^<#(\d+)>$/); - const cleaned = (mention?.[1] ?? raw).replace(/^(discord|channel|group):/i, "").trim(); - if (!/^\d+$/.test(cleaned)) { - return null; - } - return `channel:${cleaned}`; - }); - return toDirectoryEntries("group", applyDirectoryQueryAndLimit(normalizedIds, params)); -} - -export async function listTelegramDirectoryPeersFromConfig( - params: DirectoryConfigParams, -): Promise { - const account = (await inspectReadOnlyChannelAccount({ - channelId: "telegram", - cfg: params.cfg, - accountId: params.accountId, - })) as InspectedTelegramAccount | null; - if (!account || !("config" in account)) { - return []; - } - const raw = [ - ...mapAllowFromEntries(account.config.allowFrom), - ...Object.keys(account.config.dms ?? {}), - ]; - const ids = Array.from( - new Set( - raw - .map((entry) => entry.trim()) - .filter(Boolean) - .map((entry) => entry.replace(/^(telegram|tg):/i, "")), - ), - ) - .map((entry) => { - const trimmed = entry.trim(); - if (!trimmed) { - return null; - } - if (/^-?\d+$/.test(trimmed)) { - return trimmed; - } - const withAt = trimmed.startsWith("@") ? trimmed : `@${trimmed}`; - return withAt; - }) - .filter((id): id is string => Boolean(id)); - return toDirectoryEntries("user", applyDirectoryQueryAndLimit(ids, params)); -} - -export async function listTelegramDirectoryGroupsFromConfig( - params: DirectoryConfigParams, -): Promise { - const account = (await inspectReadOnlyChannelAccount({ - channelId: "telegram", - cfg: params.cfg, - accountId: params.accountId, - })) as InspectedTelegramAccount | null; - if (!account || !("config" in account)) { - return []; - } - const ids = Object.keys(account.config.groups ?? {}) - .map((id) => id.trim()) - .filter((id) => Boolean(id) && id !== "*"); - return toDirectoryEntries("group", applyDirectoryQueryAndLimit(ids, params)); -} - -export async function listWhatsAppDirectoryPeersFromConfig( - params: DirectoryConfigParams, -): Promise { - const account = getChannelPlugin("whatsapp")?.config.resolveAccount( - params.cfg, - params.accountId, - ) as { allowFrom?: unknown[] } | null | undefined; - if (!account || typeof account !== "object") { - return []; - } - const ids = (account.allowFrom ?? []) - .map((entry: unknown) => String(entry).trim()) - .filter((entry) => Boolean(entry) && entry !== "*") - .map((entry) => normalizeWhatsAppTarget(entry) ?? "") - .filter(Boolean) - .filter((id) => !isWhatsAppGroupJid(id)); - return toDirectoryEntries("user", applyDirectoryQueryAndLimit(ids, params)); -} - -export async function listWhatsAppDirectoryGroupsFromConfig( - params: DirectoryConfigParams, -): Promise { - const account = getChannelPlugin("whatsapp")?.config.resolveAccount( - params.cfg, - params.accountId, - ) as { groups?: Record } | null | undefined; - if (!account || typeof account !== "object") { - return []; - } - const ids = Object.keys(account.groups ?? {}) - .map((id) => id.trim()) - .filter((id) => Boolean(id) && id !== "*"); - return toDirectoryEntries("group", applyDirectoryQueryAndLimit(ids, params)); -} diff --git a/src/channels/plugins/directory-types.ts b/src/channels/plugins/directory-types.ts new file mode 100644 index 00000000000..9adcbcad684 --- /dev/null +++ b/src/channels/plugins/directory-types.ts @@ -0,0 +1,8 @@ +import type { OpenClawConfig } from "../../config/types.js"; + +export type DirectoryConfigParams = { + cfg: OpenClawConfig; + accountId?: string | null; + query?: string | null; + limit?: number | null; +}; diff --git a/src/channels/plugins/index.ts b/src/channels/plugins/index.ts index d7c91248109..1a00438db37 100644 --- a/src/channels/plugins/index.ts +++ b/src/channels/plugins/index.ts @@ -1,14 +1,4 @@ export { getChannelPlugin, listChannelPlugins, normalizeChannelId } from "./registry.js"; -export { - listDiscordDirectoryGroupsFromConfig, - listDiscordDirectoryPeersFromConfig, - listSlackDirectoryGroupsFromConfig, - listSlackDirectoryPeersFromConfig, - listTelegramDirectoryGroupsFromConfig, - listTelegramDirectoryPeersFromConfig, - listWhatsAppDirectoryGroupsFromConfig, - listWhatsAppDirectoryPeersFromConfig, -} from "./directory-config.js"; export { applyChannelMatchMeta, buildChannelKeyCandidates, diff --git a/src/channels/plugins/plugins-core.test.ts b/src/channels/plugins/plugins-core.test.ts index 2c8a7473dd6..b2b4994ff3e 100644 --- a/src/channels/plugins/plugins-core.test.ts +++ b/src/channels/plugins/plugins-core.test.ts @@ -2,13 +2,29 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, expectTypeOf, it } from "vitest"; +import { + listDiscordDirectoryGroupsFromConfig, + listDiscordDirectoryPeersFromConfig, +} from "../../../extensions/discord/src/directory-config.js"; import type { DiscordProbe } from "../../../extensions/discord/src/probe.js"; import type { DiscordTokenResolution } from "../../../extensions/discord/src/token.js"; import type { IMessageProbe } from "../../../extensions/imessage/src/probe.js"; import type { SignalProbe } from "../../../extensions/signal/src/probe.js"; +import { + listSlackDirectoryGroupsFromConfig, + listSlackDirectoryPeersFromConfig, +} from "../../../extensions/slack/src/directory-config.js"; import type { SlackProbe } from "../../../extensions/slack/src/probe.js"; +import { + listTelegramDirectoryGroupsFromConfig, + listTelegramDirectoryPeersFromConfig, +} from "../../../extensions/telegram/src/directory-config.js"; import type { TelegramProbe } from "../../../extensions/telegram/src/probe.js"; import type { TelegramTokenResolution } from "../../../extensions/telegram/src/token.js"; +import { + listWhatsAppDirectoryGroupsFromConfig, + listWhatsAppDirectoryPeersFromConfig, +} from "../../../extensions/whatsapp/src/directory-config.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { LineProbeResult } from "../../line/types.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; @@ -29,16 +45,6 @@ import { resolveChannelConfigWrites, resolveConfigWriteTargetFromPath, } from "./config-writes.js"; -import { - listDiscordDirectoryGroupsFromConfig, - listDiscordDirectoryPeersFromConfig, - listSlackDirectoryGroupsFromConfig, - listSlackDirectoryPeersFromConfig, - listTelegramDirectoryGroupsFromConfig, - listTelegramDirectoryPeersFromConfig, - listWhatsAppDirectoryGroupsFromConfig, - listWhatsAppDirectoryPeersFromConfig, -} from "./directory-config.js"; import { listChannelPlugins } from "./index.js"; import { loadChannelPlugin } from "./load.js"; import { loadChannelOutboundAdapter } from "./outbound/load.js"; diff --git a/src/channels/targets.ts b/src/channels/targets.ts index f9a0b015927..b07f5a45ddf 100644 --- a/src/channels/targets.ts +++ b/src/channels/targets.ts @@ -1,4 +1,4 @@ -export type { DirectoryConfigParams } from "./plugins/directory-config.js"; +export type { DirectoryConfigParams } from "./plugins/directory-types.js"; export type { ChannelDirectoryEntry } from "./plugins/types.js"; export type MessagingTargetKind = "user" | "channel"; diff --git a/src/plugin-sdk/channel-runtime.ts b/src/plugin-sdk/channel-runtime.ts index 0479efd5820..5e90b196c09 100644 --- a/src/plugin-sdk/channel-runtime.ts +++ b/src/plugin-sdk/channel-runtime.ts @@ -32,7 +32,6 @@ export * from "../channels/plugins/actions/reaction-message-id.js"; export * from "../channels/plugins/actions/shared.js"; export type * from "../channels/plugins/types.js"; export * from "../channels/plugins/config-writes.js"; -export * from "../channels/plugins/directory-config.js"; export * from "../channels/plugins/media-payload.js"; export * from "../channels/plugins/message-tool-schema.js"; export * from "../channels/plugins/normalize/signal.js"; @@ -46,6 +45,7 @@ export * from "../polls.js"; export * from "../utils/message-channel.js"; export { createActionGate, jsonResult, readStringParam } from "../agents/tools/common.js"; export * from "./channel-lifecycle.js"; +export * from "./directory-runtime.js"; export type { InteractiveButtonStyle, InteractiveReplyButton, diff --git a/src/plugin-sdk/directory-runtime.ts b/src/plugin-sdk/directory-runtime.ts index afb0ca41822..04f64523f69 100644 --- a/src/plugin-sdk/directory-runtime.ts +++ b/src/plugin-sdk/directory-runtime.ts @@ -1,6 +1,8 @@ /** Shared directory listing helpers for plugins that derive users/groups from config maps. */ +export type { DirectoryConfigParams } from "../channels/plugins/directory-types.js"; export { applyDirectoryQueryAndLimit, + collectNormalizedDirectoryIds, listDirectoryGroupEntriesFromMapKeys, listDirectoryGroupEntriesFromMapKeysAndAllowFrom, listDirectoryUserEntriesFromAllowFrom, diff --git a/src/plugin-sdk/discord.ts b/src/plugin-sdk/discord.ts index 0ca11b82976..ca58ec0c958 100644 --- a/src/plugin-sdk/discord.ts +++ b/src/plugin-sdk/discord.ts @@ -45,15 +45,14 @@ export { projectCredentialSnapshotFields, resolveConfiguredFromCredentialStatuses, } from "../channels/account-snapshot-fields.js"; -export { - listDiscordDirectoryGroupsFromConfig, - listDiscordDirectoryPeersFromConfig, -} from "../channels/plugins/directory-config.js"; - export { resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, } from "../config/runtime-group-policy.js"; +export { + listDiscordDirectoryGroupsFromConfig, + listDiscordDirectoryPeersFromConfig, +} from "../../extensions/discord/src/directory-config.js"; export { resolveDiscordGroupRequireMention, resolveDiscordGroupToolPolicy, diff --git a/src/plugin-sdk/slack.ts b/src/plugin-sdk/slack.ts index 80b49010142..f4720babeb9 100644 --- a/src/plugin-sdk/slack.ts +++ b/src/plugin-sdk/slack.ts @@ -28,14 +28,14 @@ export { resolveConfiguredFromCredentialStatuses, resolveConfiguredFromRequiredCredentialStatuses, } from "../channels/account-snapshot-fields.js"; -export { - listSlackDirectoryGroupsFromConfig, - listSlackDirectoryPeersFromConfig, -} from "../channels/plugins/directory-config.js"; export { looksLikeSlackTargetId, normalizeSlackMessagingTarget, } from "../channels/plugins/normalize/slack.js"; +export { + listSlackDirectoryGroupsFromConfig, + listSlackDirectoryPeersFromConfig, +} from "../../extensions/slack/src/directory-config.js"; export { resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, diff --git a/src/plugin-sdk/telegram.ts b/src/plugin-sdk/telegram.ts index 0b539cf7057..4a180763b38 100644 --- a/src/plugin-sdk/telegram.ts +++ b/src/plugin-sdk/telegram.ts @@ -45,15 +45,14 @@ export { projectCredentialSnapshotFields, resolveConfiguredFromCredentialStatuses, } from "../channels/account-snapshot-fields.js"; -export { - listTelegramDirectoryGroupsFromConfig, - listTelegramDirectoryPeersFromConfig, -} from "../channels/plugins/directory-config.js"; - export { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, } from "../config/runtime-group-policy.js"; +export { + listTelegramDirectoryGroupsFromConfig, + listTelegramDirectoryPeersFromConfig, +} from "../../extensions/telegram/src/directory-config.js"; export { resolveTelegramGroupRequireMention, resolveTelegramGroupToolPolicy, diff --git a/src/plugin-sdk/whatsapp.ts b/src/plugin-sdk/whatsapp.ts index 405118818b5..a3f3293a0fa 100644 --- a/src/plugin-sdk/whatsapp.ts +++ b/src/plugin-sdk/whatsapp.ts @@ -32,11 +32,11 @@ export { resolveWhatsAppConfigAllowFrom, resolveWhatsAppConfigDefaultTo, } from "./channel-config-helpers.js"; +export { normalizeWhatsAppAllowFromEntries } from "../channels/plugins/normalize/whatsapp.js"; export { listWhatsAppDirectoryGroupsFromConfig, listWhatsAppDirectoryPeersFromConfig, -} from "../channels/plugins/directory-config.js"; -export { normalizeWhatsAppAllowFromEntries } from "../channels/plugins/normalize/whatsapp.js"; +} from "../../extensions/whatsapp/src/directory-config.js"; export { collectAllowlistProviderGroupPolicyWarnings, collectOpenGroupPolicyRouteAllowlistWarnings,