From cf0ea6c75652d5cfce4670c0c80e2c9f8f9e6b54 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 07:14:06 +0000 Subject: [PATCH] refactor: unify target resolver metadata --- src/channels/plugins/directory-config.test.ts | 42 ++-- src/channels/plugins/directory-config.ts | 214 ++++++++++++++++++ src/channels/plugins/discord.ts | 80 +------ src/channels/plugins/imessage.ts | 16 +- src/channels/plugins/signal.ts | 6 +- src/channels/plugins/slack.ts | 63 +----- src/channels/plugins/telegram.ts | 51 +---- src/channels/plugins/types.core.ts | 6 +- src/channels/plugins/whatsapp.ts | 37 +-- src/infra/outbound/target-normalization.ts | 5 +- src/infra/outbound/target-resolver.ts | 4 +- src/infra/outbound/targets.ts | 2 +- 12 files changed, 295 insertions(+), 231 deletions(-) create mode 100644 src/channels/plugins/directory-config.ts diff --git a/src/channels/plugins/directory-config.test.ts b/src/channels/plugins/directory-config.test.ts index d9df6caa5..8e96e8f98 100644 --- a/src/channels/plugins/directory-config.test.ts +++ b/src/channels/plugins/directory-config.test.ts @@ -1,9 +1,17 @@ import { describe, expect, it } from "vitest"; +import { + listDiscordDirectoryGroupsFromConfig, + listDiscordDirectoryPeersFromConfig, + listSlackDirectoryGroupsFromConfig, + listSlackDirectoryPeersFromConfig, + listTelegramDirectoryGroupsFromConfig, + listTelegramDirectoryPeersFromConfig, + listWhatsAppDirectoryGroupsFromConfig, + listWhatsAppDirectoryPeersFromConfig, +} from "./directory-config.js"; describe("directory (config-backed)", () => { it("lists Slack peers/groups from config", async () => { - const { slackPlugin } = await import("./slack.js"); - const runtime = { log: () => {}, error: () => {}, exit: () => {} } as any; const cfg = { channels: { slack: { @@ -16,12 +24,11 @@ describe("directory (config-backed)", () => { }, } as any; - const peers = await slackPlugin.directory?.listPeers?.({ + const peers = await listSlackDirectoryPeersFromConfig({ cfg, accountId: "default", query: null, limit: null, - runtime, }); expect(peers?.map((e) => e.id).sort()).toEqual([ "user:u123", @@ -30,19 +37,16 @@ describe("directory (config-backed)", () => { "user:u999", ]); - const groups = await slackPlugin.directory?.listGroups?.({ + const groups = await listSlackDirectoryGroupsFromConfig({ cfg, accountId: "default", query: null, limit: null, - runtime, }); expect(groups?.map((e) => e.id)).toEqual(["channel:c111"]); }); it("lists Discord peers/groups from config (numeric ids only)", async () => { - const { discordPlugin } = await import("./discord.js"); - const runtime = { log: () => {}, error: () => {}, exit: () => {} } as any; const cfg = { channels: { discord: { @@ -63,28 +67,24 @@ describe("directory (config-backed)", () => { }, } as any; - const peers = await discordPlugin.directory?.listPeers?.({ + const peers = await listDiscordDirectoryPeersFromConfig({ cfg, accountId: "default", query: null, limit: null, - runtime, }); expect(peers?.map((e) => e.id).sort()).toEqual(["user:111", "user:12345", "user:222"]); - const groups = await discordPlugin.directory?.listGroups?.({ + const groups = await listDiscordDirectoryGroupsFromConfig({ cfg, accountId: "default", query: null, limit: null, - runtime, }); expect(groups?.map((e) => e.id).sort()).toEqual(["channel:555", "channel:666"]); }); it("lists Telegram peers/groups from config", async () => { - const { telegramPlugin } = await import("./telegram.js"); - const runtime = { log: () => {}, error: () => {}, exit: () => {} } as any; const cfg = { channels: { telegram: { @@ -96,28 +96,24 @@ describe("directory (config-backed)", () => { }, } as any; - const peers = await telegramPlugin.directory?.listPeers?.({ + const peers = await listTelegramDirectoryPeersFromConfig({ cfg, accountId: "default", query: null, limit: null, - runtime, }); expect(peers?.map((e) => e.id).sort()).toEqual(["123", "456", "@alice", "@bob"]); - const groups = await telegramPlugin.directory?.listGroups?.({ + const groups = await listTelegramDirectoryGroupsFromConfig({ cfg, accountId: "default", query: null, limit: null, - runtime, }); expect(groups?.map((e) => e.id)).toEqual(["-1001"]); }); it("lists WhatsApp peers/groups from config", async () => { - const { whatsappPlugin } = await import("./whatsapp.js"); - const runtime = { log: () => {}, error: () => {}, exit: () => {} } as any; const cfg = { channels: { whatsapp: { @@ -127,21 +123,19 @@ describe("directory (config-backed)", () => { }, } as any; - const peers = await whatsappPlugin.directory?.listPeers?.({ + const peers = await listWhatsAppDirectoryPeersFromConfig({ cfg, accountId: "default", query: null, limit: null, - runtime, }); expect(peers?.map((e) => e.id)).toEqual(["+15550000000"]); - const groups = await whatsappPlugin.directory?.listGroups?.({ + const groups = await listWhatsAppDirectoryGroupsFromConfig({ cfg, accountId: "default", query: null, limit: null, - runtime, }); expect(groups?.map((e) => e.id)).toEqual(["999@g.us"]); }); diff --git a/src/channels/plugins/directory-config.ts b/src/channels/plugins/directory-config.ts new file mode 100644 index 000000000..ba7ac32e5 --- /dev/null +++ b/src/channels/plugins/directory-config.ts @@ -0,0 +1,214 @@ +import type { ClawdbotConfig } from "../../config/types.js"; +import type { ChannelDirectoryEntry } from "./types.js"; +import { resolveSlackAccount } from "../../slack/accounts.js"; +import { resolveDiscordAccount } from "../../discord/accounts.js"; +import { resolveTelegramAccount } from "../../telegram/accounts.js"; +import { resolveWhatsAppAccount } from "../../web/accounts.js"; +import { normalizeSlackMessagingTarget } from "./normalize-target.js"; +import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js"; + +export type DirectoryConfigParams = { + cfg: ClawdbotConfig; + accountId?: string | null; + query?: string | null; + limit?: number | null; +}; + +export async function listSlackDirectoryPeersFromConfig( + params: DirectoryConfigParams, +): Promise { + const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId }); + const q = params.query?.trim().toLowerCase() || ""; + const ids = new Set(); + + for (const entry of account.dm?.allowFrom ?? []) { + const raw = String(entry).trim(); + if (!raw || raw === "*") continue; + ids.add(raw); + } + for (const id of Object.keys(account.config.dms ?? {})) { + const trimmed = id.trim(); + if (trimmed) ids.add(trimmed); + } + for (const channel of Object.values(account.config.channels ?? {})) { + for (const user of channel.users ?? []) { + const raw = String(user).trim(); + if (raw) ids.add(raw); + } + } + + return Array.from(ids) + .map((raw) => raw.trim()) + .filter(Boolean) + .map((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 is string => Boolean(id)) + .filter((id) => id.startsWith("user:")) + .filter((id) => (q ? id.toLowerCase().includes(q) : true)) + .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) + .map((id) => ({ kind: "user", id }) as const); +} + +export async function listSlackDirectoryGroupsFromConfig( + params: DirectoryConfigParams, +): Promise { + const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId }); + const q = params.query?.trim().toLowerCase() || ""; + return Object.keys(account.config.channels ?? {}) + .map((raw) => raw.trim()) + .filter(Boolean) + .map((raw) => normalizeSlackMessagingTarget(raw) ?? raw.toLowerCase()) + .filter((id) => id.startsWith("channel:")) + .filter((id) => (q ? id.toLowerCase().includes(q) : true)) + .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) + .map((id) => ({ kind: "group", id }) as const); +} + +export async function listDiscordDirectoryPeersFromConfig( + params: DirectoryConfigParams, +): Promise { + const account = resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId }); + const q = params.query?.trim().toLowerCase() || ""; + const ids = new Set(); + + for (const entry of account.config.dm?.allowFrom ?? []) { + const raw = String(entry).trim(); + if (!raw || raw === "*") continue; + ids.add(raw); + } + for (const id of Object.keys(account.config.dms ?? {})) { + const trimmed = id.trim(); + if (trimmed) ids.add(trimmed); + } + for (const guild of Object.values(account.config.guilds ?? {})) { + for (const entry of guild.users ?? []) { + const raw = String(entry).trim(); + if (raw) ids.add(raw); + } + for (const channel of Object.values(guild.channels ?? {})) { + for (const user of channel.users ?? []) { + const raw = String(user).trim(); + if (raw) ids.add(raw); + } + } + } + + return Array.from(ids) + .map((raw) => raw.trim()) + .filter(Boolean) + .map((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}`; + }) + .filter((id): id is string => Boolean(id)) + .filter((id) => (q ? id.toLowerCase().includes(q) : true)) + .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) + .map((id) => ({ kind: "user", id }) as const); +} + +export async function listDiscordDirectoryGroupsFromConfig( + params: DirectoryConfigParams, +): Promise { + const account = resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId }); + const q = params.query?.trim().toLowerCase() || ""; + const ids = new Set(); + for (const guild of Object.values(account.config.guilds ?? {})) { + for (const channelId of Object.keys(guild.channels ?? {})) { + const trimmed = channelId.trim(); + if (trimmed) ids.add(trimmed); + } + } + + return Array.from(ids) + .map((raw) => raw.trim()) + .filter(Boolean) + .map((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}`; + }) + .filter((id): id is string => Boolean(id)) + .filter((id) => (q ? id.toLowerCase().includes(q) : true)) + .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) + .map((id) => ({ kind: "group", id }) as const); +} + +export async function listTelegramDirectoryPeersFromConfig( + params: DirectoryConfigParams, +): Promise { + const account = resolveTelegramAccount({ cfg: params.cfg, accountId: params.accountId }); + const q = params.query?.trim().toLowerCase() || ""; + const raw = [ + ...(account.config.allowFrom ?? []).map((entry) => String(entry)), + ...Object.keys(account.config.dms ?? {}), + ]; + return 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)) + .filter((id) => (q ? id.toLowerCase().includes(q) : true)) + .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) + .map((id) => ({ kind: "user", id }) as const); +} + +export async function listTelegramDirectoryGroupsFromConfig( + params: DirectoryConfigParams, +): Promise { + const account = resolveTelegramAccount({ cfg: params.cfg, accountId: params.accountId }); + const q = params.query?.trim().toLowerCase() || ""; + return Object.keys(account.config.groups ?? {}) + .map((id) => id.trim()) + .filter((id) => Boolean(id) && id !== "*") + .filter((id) => (q ? id.toLowerCase().includes(q) : true)) + .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) + .map((id) => ({ kind: "group", id }) as const); +} + +export async function listWhatsAppDirectoryPeersFromConfig( + params: DirectoryConfigParams, +): Promise { + const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId }); + const q = params.query?.trim().toLowerCase() || ""; + return (account.allowFrom ?? []) + .map((entry) => String(entry).trim()) + .filter((entry) => Boolean(entry) && entry !== "*") + .map((entry) => normalizeWhatsAppTarget(entry) ?? "") + .filter(Boolean) + .filter((id) => !isWhatsAppGroupJid(id)) + .filter((id) => (q ? id.toLowerCase().includes(q) : true)) + .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) + .map((id) => ({ kind: "user", id }) as const); +} + +export async function listWhatsAppDirectoryGroupsFromConfig( + params: DirectoryConfigParams, +): Promise { + const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId }); + const q = params.query?.trim().toLowerCase() || ""; + return Object.keys(account.groups ?? {}) + .map((id) => id.trim()) + .filter((id) => Boolean(id) && id !== "*") + .filter((id) => (q ? id.toLowerCase().includes(q) : true)) + .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) + .map((id) => ({ kind: "group", id }) as const); +} diff --git a/src/channels/plugins/discord.ts b/src/channels/plugins/discord.ts index 8ed09e9e0..a3eddd5cc 100644 --- a/src/channels/plugins/discord.ts +++ b/src/channels/plugins/discord.ts @@ -38,6 +38,10 @@ import { } from "./setup-helpers.js"; import { collectDiscordStatusIssues } from "./status-issues/discord.js"; import type { ChannelPlugin } from "./types.js"; +import { + listDiscordDirectoryGroupsFromConfig, + listDiscordDirectoryPeersFromConfig, +} from "./directory-config.js"; const meta = getChatChannelMeta("discord"); @@ -152,79 +156,15 @@ export const discordPlugin: ChannelPlugin = { }, messaging: { normalizeTarget: normalizeDiscordMessagingTarget, - looksLikeTargetId: looksLikeDiscordTargetId, - targetHint: "", + targetResolver: { + looksLikeId: looksLikeDiscordTargetId, + hint: "", + }, }, directory: { self: async () => null, - listPeers: async ({ cfg, accountId, query, limit }) => { - const account = resolveDiscordAccount({ cfg, accountId }); - const q = query?.trim().toLowerCase() || ""; - const ids = new Set(); - - for (const entry of account.config.dm?.allowFrom ?? []) { - const raw = String(entry).trim(); - if (!raw || raw === "*") continue; - ids.add(raw); - } - for (const id of Object.keys(account.config.dms ?? {})) { - const trimmed = id.trim(); - if (trimmed) ids.add(trimmed); - } - for (const guild of Object.values(account.config.guilds ?? {})) { - for (const entry of guild.users ?? []) { - const raw = String(entry).trim(); - if (raw) ids.add(raw); - } - for (const channel of Object.values(guild.channels ?? {})) { - for (const user of channel.users ?? []) { - const raw = String(user).trim(); - if (raw) ids.add(raw); - } - } - } - - const peers = Array.from(ids) - .map((raw) => raw.trim()) - .filter(Boolean) - .map((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}`; - }) - .filter((id): id is string => Boolean(id)) - .filter((id) => (q ? id.toLowerCase().includes(q) : true)) - .slice(0, limit && limit > 0 ? limit : undefined) - .map((id) => ({ kind: "user", id }) as const); - return peers; - }, - listGroups: async ({ cfg, accountId, query, limit }) => { - const account = resolveDiscordAccount({ cfg, accountId }); - const q = query?.trim().toLowerCase() || ""; - const ids = new Set(); - for (const guild of Object.values(account.config.guilds ?? {})) { - for (const channelId of Object.keys(guild.channels ?? {})) { - const trimmed = channelId.trim(); - if (trimmed) ids.add(trimmed); - } - } - - const groups = Array.from(ids) - .map((raw) => raw.trim()) - .filter(Boolean) - .map((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}`; - }) - .filter((id): id is string => Boolean(id)) - .filter((id) => (q ? id.toLowerCase().includes(q) : true)) - .slice(0, limit && limit > 0 ? limit : undefined) - .map((id) => ({ kind: "group", id }) as const); - return groups; - }, + listPeers: async (params) => listDiscordDirectoryPeersFromConfig(params), + listGroups: async (params) => listDiscordDirectoryGroupsFromConfig(params), listGroupsLive: async ({ cfg, accountId, query, limit }) => { const account = resolveDiscordAccount({ cfg, accountId }); const q = query?.trim().toLowerCase() || ""; diff --git a/src/channels/plugins/imessage.ts b/src/channels/plugins/imessage.ts index a8ed5e80c..9b88d7538 100644 --- a/src/channels/plugins/imessage.ts +++ b/src/channels/plugins/imessage.ts @@ -107,14 +107,16 @@ export const imessagePlugin: ChannelPlugin = { resolveRequireMention: resolveIMessageGroupRequireMention, }, messaging: { - looksLikeTargetId: (raw) => { - const trimmed = raw.trim(); - if (!trimmed) return false; - if (/^(imessage:|chat_id:)/i.test(trimmed)) return true; - if (trimmed.includes("@")) return true; - return /^\+?\d{3,}$/.test(trimmed); + targetResolver: { + looksLikeId: (raw) => { + const trimmed = raw.trim(); + if (!trimmed) return false; + if (/^(imessage:|chat_id:)/i.test(trimmed)) return true; + if (trimmed.includes("@")) return true; + return /^\+?\d{3,}$/.test(trimmed); + }, + hint: "", }, - targetHint: "", }, setup: { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), diff --git a/src/channels/plugins/signal.ts b/src/channels/plugins/signal.ts index 2e48684cc..8b8d44719 100644 --- a/src/channels/plugins/signal.ts +++ b/src/channels/plugins/signal.ts @@ -118,8 +118,10 @@ export const signalPlugin: ChannelPlugin = { }, messaging: { normalizeTarget: normalizeSignalMessagingTarget, - looksLikeTargetId: looksLikeSignalTargetId, - targetHint: "", + targetResolver: { + looksLikeId: looksLikeSignalTargetId, + hint: "", + }, }, setup: { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), diff --git a/src/channels/plugins/slack.ts b/src/channels/plugins/slack.ts index 6efdf6429..d3d8a60ee 100644 --- a/src/channels/plugins/slack.ts +++ b/src/channels/plugins/slack.ts @@ -28,6 +28,10 @@ import { migrateBaseNameToDefaultAccount, } from "./setup-helpers.js"; import type { ChannelMessageActionName, ChannelPlugin } from "./types.js"; +import { + listSlackDirectoryGroupsFromConfig, + listSlackDirectoryPeersFromConfig, +} from "./directory-config.js"; const meta = getChatChannelMeta("slack"); @@ -177,62 +181,15 @@ export const slackPlugin: ChannelPlugin = { }, messaging: { normalizeTarget: normalizeSlackMessagingTarget, - looksLikeTargetId: looksLikeSlackTargetId, - targetHint: "", + targetResolver: { + looksLikeId: looksLikeSlackTargetId, + hint: "", + }, }, directory: { self: async () => null, - listPeers: async ({ cfg, accountId, query, limit }) => { - const account = resolveSlackAccount({ cfg, accountId }); - const q = query?.trim().toLowerCase() || ""; - const ids = new Set(); - - for (const entry of account.dm?.allowFrom ?? []) { - const raw = String(entry).trim(); - if (!raw || raw === "*") continue; - ids.add(raw); - } - for (const id of Object.keys(account.config.dms ?? {})) { - const trimmed = id.trim(); - if (trimmed) ids.add(trimmed); - } - for (const channel of Object.values(account.config.channels ?? {})) { - for (const user of channel.users ?? []) { - const raw = String(user).trim(); - if (raw) ids.add(raw); - } - } - - const peers = Array.from(ids) - .map((raw) => raw.trim()) - .filter(Boolean) - .map((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 is string => Boolean(id)) - .filter((id) => id.startsWith("user:")) - .filter((id) => (q ? id.toLowerCase().includes(q) : true)) - .slice(0, limit && limit > 0 ? limit : undefined) - .map((id) => ({ kind: "user", id }) as const); - return peers; - }, - listGroups: async ({ cfg, accountId, query, limit }) => { - const account = resolveSlackAccount({ cfg, accountId }); - const q = query?.trim().toLowerCase() || ""; - const groups = Object.keys(account.config.channels ?? {}) - .map((raw) => raw.trim()) - .filter(Boolean) - .map((raw) => normalizeSlackMessagingTarget(raw) ?? raw.toLowerCase()) - .filter((id) => id.startsWith("channel:")) - .filter((id) => (q ? id.toLowerCase().includes(q) : true)) - .slice(0, limit && limit > 0 ? limit : undefined) - .map((id) => ({ kind: "group", id }) as const); - return groups; - }, + listPeers: async (params) => listSlackDirectoryPeersFromConfig(params), + listGroups: async (params) => listSlackDirectoryGroupsFromConfig(params), }, actions: { listActions: ({ cfg }) => { diff --git a/src/channels/plugins/telegram.ts b/src/channels/plugins/telegram.ts index 2fd417b6a..eae94475a 100644 --- a/src/channels/plugins/telegram.ts +++ b/src/channels/plugins/telegram.ts @@ -38,6 +38,10 @@ import { } from "./setup-helpers.js"; import { collectTelegramStatusIssues } from "./status-issues/telegram.js"; import type { ChannelPlugin } from "./types.js"; +import { + listTelegramDirectoryGroupsFromConfig, + listTelegramDirectoryPeersFromConfig, +} from "./directory-config.js"; const meta = getChatChannelMeta("telegram"); @@ -160,50 +164,15 @@ export const telegramPlugin: ChannelPlugin = { }, messaging: { normalizeTarget: normalizeTelegramMessagingTarget, - looksLikeTargetId: looksLikeTelegramTargetId, - targetHint: "", + targetResolver: { + looksLikeId: looksLikeTelegramTargetId, + hint: "", + }, }, directory: { self: async () => null, - listPeers: async ({ cfg, accountId, query, limit }) => { - const account = resolveTelegramAccount({ cfg, accountId }); - const q = query?.trim().toLowerCase() || ""; - const raw = [ - ...(account.config.allowFrom ?? []).map((entry) => String(entry)), - ...Object.keys(account.config.dms ?? {}), - ]; - const peers = 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)) - .filter((id) => (q ? id.toLowerCase().includes(q) : true)) - .slice(0, limit && limit > 0 ? limit : undefined) - .map((id) => ({ kind: "user", id }) as const); - return peers; - }, - listGroups: async ({ cfg, accountId, query, limit }) => { - const account = resolveTelegramAccount({ cfg, accountId }); - const q = query?.trim().toLowerCase() || ""; - const groups = Object.keys(account.config.groups ?? {}) - .map((id) => id.trim()) - .filter((id) => Boolean(id) && id !== "*") - .filter((id) => (q ? id.toLowerCase().includes(q) : true)) - .slice(0, limit && limit > 0 ? limit : undefined) - .map((id) => ({ kind: "group", id }) as const); - return groups; - }, + listPeers: async (params) => listTelegramDirectoryPeersFromConfig(params), + listGroups: async (params) => listTelegramDirectoryGroupsFromConfig(params), }, actions: telegramMessageActions, setup: { diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index bd0036b71..dad18d3cf 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -216,13 +216,15 @@ export type ChannelThreadingToolContext = { export type ChannelMessagingAdapter = { normalizeTarget?: (raw: string) => string | undefined; - looksLikeTargetId?: (raw: string, normalized?: string) => boolean; + targetResolver?: { + looksLikeId?: (raw: string, normalized?: string) => boolean; + hint?: string; + }; formatTargetDisplay?: (params: { target: string; display?: string; kind?: ChannelDirectoryEntryKind; }) => string; - targetHint?: string; }; export type ChannelDirectoryEntryKind = "user" | "group" | "channel"; diff --git a/src/channels/plugins/whatsapp.ts b/src/channels/plugins/whatsapp.ts index fa8777325..125c74af6 100644 --- a/src/channels/plugins/whatsapp.ts +++ b/src/channels/plugins/whatsapp.ts @@ -39,6 +39,10 @@ import { collectWhatsAppStatusIssues } from "./status-issues/whatsapp.js"; import type { ChannelMessageActionName, ChannelPlugin } from "./types.js"; import { resolveWhatsAppHeartbeatRecipients } from "./whatsapp-heartbeat.js"; import { missingTargetError } from "../../infra/outbound/target-errors.js"; +import { + listWhatsAppDirectoryGroupsFromConfig, + listWhatsAppDirectoryPeersFromConfig, +} from "./directory-config.js"; const meta = getChatChannelMeta("whatsapp"); @@ -222,8 +226,10 @@ export const whatsappPlugin: ChannelPlugin = { }, messaging: { normalizeTarget: normalizeWhatsAppMessagingTarget, - looksLikeTargetId: looksLikeWhatsAppTargetId, - targetHint: "", + targetResolver: { + looksLikeId: looksLikeWhatsAppTargetId, + hint: "", + }, }, directory: { self: async ({ cfg, accountId }) => { @@ -238,31 +244,8 @@ export const whatsappPlugin: ChannelPlugin = { raw: { e164, jid }, }; }, - listPeers: async ({ cfg, accountId, query, limit }) => { - const account = resolveWhatsAppAccount({ cfg, accountId }); - const q = query?.trim().toLowerCase() || ""; - const peers = (account.allowFrom ?? []) - .map((entry) => String(entry).trim()) - .filter((entry) => Boolean(entry) && entry !== "*") - .map((entry) => normalizeWhatsAppTarget(entry) ?? "") - .filter(Boolean) - .filter((id) => !isWhatsAppGroupJid(id)) - .filter((id) => (q ? id.toLowerCase().includes(q) : true)) - .slice(0, limit && limit > 0 ? limit : undefined) - .map((id) => ({ kind: "user", id }) as const); - return peers; - }, - listGroups: async ({ cfg, accountId, query, limit }) => { - const account = resolveWhatsAppAccount({ cfg, accountId }); - const q = query?.trim().toLowerCase() || ""; - const groups = Object.keys(account.groups ?? {}) - .map((id) => id.trim()) - .filter((id) => Boolean(id) && id !== "*") - .filter((id) => (q ? id.toLowerCase().includes(q) : true)) - .slice(0, limit && limit > 0 ? limit : undefined) - .map((id) => ({ kind: "group", id }) as const); - return groups; - }, + listPeers: async (params) => listWhatsAppDirectoryPeersFromConfig(params), + listGroups: async (params) => listWhatsAppDirectoryGroupsFromConfig(params), }, actions: { listActions: ({ cfg }) => { diff --git a/src/infra/outbound/target-normalization.ts b/src/infra/outbound/target-normalization.ts index 93bd79dba..dbab5e4e4 100644 --- a/src/infra/outbound/target-normalization.ts +++ b/src/infra/outbound/target-normalization.ts @@ -16,8 +16,9 @@ export function normalizeTargetForProvider(provider: string, raw?: string): stri export function buildTargetResolverSignature(channel: ChannelId): string { const plugin = getChannelPlugin(channel); - const hint = plugin?.messaging?.targetHint ?? ""; - const looksLike = plugin?.messaging?.looksLikeTargetId; + const resolver = plugin?.messaging?.targetResolver; + const hint = resolver?.hint ?? ""; + const looksLike = resolver?.looksLikeId; const source = looksLike ? looksLike.toString() : ""; return hashSignature(`${hint}|${source}`); } diff --git a/src/infra/outbound/target-resolver.ts b/src/infra/outbound/target-resolver.ts index 14df9dd4c..e27db0eaa 100644 --- a/src/infra/outbound/target-resolver.ts +++ b/src/infra/outbound/target-resolver.ts @@ -172,7 +172,7 @@ function looksLikeTargetId(params: { const raw = params.raw.trim(); if (!raw) return false; const plugin = getChannelPlugin(params.channel); - const lookup = plugin?.messaging?.looksLikeTargetId; + const lookup = plugin?.messaging?.targetResolver?.looksLikeId; if (lookup) return lookup(raw, params.normalized); if (/^(channel|group|user):/i.test(raw)) return true; if (/^[@#]/.test(raw)) return true; @@ -285,7 +285,7 @@ export async function resolveMessagingTarget(params: { } const plugin = getChannelPlugin(params.channel); const providerLabel = plugin?.meta?.label ?? params.channel; - const hint = plugin?.messaging?.targetHint; + const hint = plugin?.messaging?.targetResolver?.hint; const kind = detectTargetKind(raw, params.preferredKind); const normalized = normalizeTargetForProvider(params.channel, raw) ?? raw; if (looksLikeTargetId({ channel: params.channel, raw, normalized })) { diff --git a/src/infra/outbound/targets.ts b/src/infra/outbound/targets.ts index 93ea835ac..619b0d039 100644 --- a/src/infra/outbound/targets.ts +++ b/src/infra/outbound/targets.ts @@ -146,7 +146,7 @@ export function resolveOutboundTarget(params: { if (trimmed) { return { ok: true, to: trimmed }; } - const hint = plugin.messaging?.targetHint; + const hint = plugin.messaging?.targetResolver?.hint; return { ok: false, error: missingTargetError(plugin.meta.label ?? params.channel, hint),