diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index 333f5c74ac5..c7527652f55 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -6,6 +6,8 @@ import { deleteAccountFromConfigSection, formatPairingApproveHint, getChatChannelMeta, + listDirectoryGroupEntriesFromMapKeys, + listDirectoryUserEntriesFromAllowFrom, migrateBaseNameToDefaultAccount, missingTargetError, normalizeAccountId, @@ -243,34 +245,23 @@ export const googlechatPlugin: ChannelPlugin = { cfg: cfg, accountId, }); - const q = query?.trim().toLowerCase() || ""; - const allowFrom = account.config.dm?.allowFrom ?? []; - const peers = Array.from( - new Set( - allowFrom - .map((entry) => String(entry).trim()) - .filter((entry) => Boolean(entry) && entry !== "*") - .map((entry) => normalizeGoogleChatTarget(entry) ?? entry), - ), - ) - .filter((id) => (q ? id.toLowerCase().includes(q) : true)) - .slice(0, limit && limit > 0 ? limit : undefined) - .map((id) => ({ kind: "user", id }) as const); - return peers; + return listDirectoryUserEntriesFromAllowFrom({ + allowFrom: account.config.dm?.allowFrom, + query, + limit, + normalizeId: (entry) => normalizeGoogleChatTarget(entry) ?? entry, + }); }, listGroups: async ({ cfg, accountId, query, limit }) => { const account = resolveGoogleChatAccount({ cfg: cfg, accountId, }); - const groups = account.config.groups ?? {}; - const q = query?.trim().toLowerCase() || ""; - const entries = Object.keys(groups) - .filter((key) => key && key !== "*") - .filter((key) => (q ? key.toLowerCase().includes(q) : true)) - .slice(0, limit && limit > 0 ? limit : undefined) - .map((id) => ({ kind: "group", id }) as const); - return entries; + return listDirectoryGroupEntriesFromMapKeys({ + groups: account.config.groups, + query, + limit, + }); }, }, resolver: { diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index 54dce454693..9de72910ab7 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -17,6 +17,7 @@ import { formatAllowFromLowercase, formatPairingApproveHint, migrateBaseNameToDefaultAccount, + listDirectoryUserEntriesFromAllowFrom, normalizeAccountId, isNumericTargetId, PAIRING_APPROVED_MESSAGE, @@ -196,19 +197,12 @@ export const zaloPlugin: ChannelPlugin = { self: async () => null, listPeers: async ({ cfg, accountId, query, limit }) => { const account = resolveZaloAccount({ cfg: cfg, accountId }); - const q = query?.trim().toLowerCase() || ""; - const peers = Array.from( - new Set( - (account.config.allowFrom ?? []) - .map((entry) => String(entry).trim()) - .filter((entry) => Boolean(entry) && entry !== "*") - .map((entry) => entry.replace(/^(zalo|zl):/i, "")), - ), - ) - .filter((id) => (q ? id.toLowerCase().includes(q) : true)) - .slice(0, limit && limit > 0 ? limit : undefined) - .map((id) => ({ kind: "user", id }) as const); - return peers; + return listDirectoryUserEntriesFromAllowFrom({ + allowFrom: account.config.allowFrom, + query, + limit, + normalizeId: (entry) => entry.replace(/^(zalo|zl):/i, ""), + }); }, listGroups: async () => [], }, diff --git a/src/channels/plugins/directory-config-helpers.test.ts b/src/channels/plugins/directory-config-helpers.test.ts new file mode 100644 index 00000000000..c4f5370cc4a --- /dev/null +++ b/src/channels/plugins/directory-config-helpers.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { + listDirectoryGroupEntriesFromMapKeys, + listDirectoryUserEntriesFromAllowFrom, +} from "./directory-config-helpers.js"; + +describe("listDirectoryUserEntriesFromAllowFrom", () => { + it("normalizes, deduplicates, filters, and limits user ids", () => { + const entries = listDirectoryUserEntriesFromAllowFrom({ + allowFrom: ["", "*", " user:Alice ", "user:alice", "user:Bob", "user:Carla"], + normalizeId: (entry) => entry.replace(/^user:/i, "").toLowerCase(), + query: "a", + limit: 2, + }); + + expect(entries).toEqual([ + { kind: "user", id: "alice" }, + { kind: "user", id: "carla" }, + ]); + }); +}); + +describe("listDirectoryGroupEntriesFromMapKeys", () => { + it("extracts normalized group ids from map keys", () => { + const entries = listDirectoryGroupEntriesFromMapKeys({ + groups: { + "*": {}, + " Space/A ": {}, + "space/b": {}, + }, + normalizeId: (entry) => entry.toLowerCase().replace(/\s+/g, ""), + }); + + expect(entries).toEqual([ + { kind: "group", id: "space/a" }, + { kind: "group", id: "space/b" }, + ]); + }); +}); diff --git a/src/channels/plugins/directory-config-helpers.ts b/src/channels/plugins/directory-config-helpers.ts new file mode 100644 index 00000000000..b431bc14a8b --- /dev/null +++ b/src/channels/plugins/directory-config-helpers.ts @@ -0,0 +1,65 @@ +import type { ChannelDirectoryEntry } from "./types.js"; + +function resolveDirectoryQuery(query?: string | null): string { + return query?.trim().toLowerCase() || ""; +} + +function resolveDirectoryLimit(limit?: number | null): number | undefined { + return typeof limit === "number" && limit > 0 ? limit : undefined; +} + +function applyDirectoryQueryAndLimit( + ids: string[], + params: { query?: string | null; limit?: number | null }, +): string[] { + const q = resolveDirectoryQuery(params.query); + const limit = resolveDirectoryLimit(params.limit); + const filtered = ids.filter((id) => (q ? id.toLowerCase().includes(q) : true)); + return typeof limit === "number" ? filtered.slice(0, limit) : filtered; +} + +function toDirectoryEntries(kind: "user" | "group", ids: string[]): ChannelDirectoryEntry[] { + return ids.map((id) => ({ kind, id }) as const); +} + +export function listDirectoryUserEntriesFromAllowFrom(params: { + allowFrom?: readonly unknown[]; + query?: string | null; + limit?: number | null; + normalizeId?: (entry: string) => string | null | undefined; +}): ChannelDirectoryEntry[] { + const ids = Array.from( + new Set( + (params.allowFrom ?? []) + .map((entry) => String(entry).trim()) + .filter((entry) => Boolean(entry) && entry !== "*") + .map((entry) => { + const normalized = params.normalizeId ? params.normalizeId(entry) : entry; + return typeof normalized === "string" ? normalized.trim() : ""; + }) + .filter(Boolean), + ), + ); + return toDirectoryEntries("user", applyDirectoryQueryAndLimit(ids, params)); +} + +export function listDirectoryGroupEntriesFromMapKeys(params: { + groups?: Record; + query?: string | null; + limit?: number | null; + normalizeId?: (entry: string) => string | null | undefined; +}): ChannelDirectoryEntry[] { + const ids = Array.from( + new Set( + Object.keys(params.groups ?? {}) + .map((entry) => entry.trim()) + .filter((entry) => Boolean(entry) && entry !== "*") + .map((entry) => { + const normalized = params.normalizeId ? params.normalizeId(entry) : entry; + return typeof normalized === "string" ? normalized.trim() : ""; + }) + .filter(Boolean), + ), + ); + return toDirectoryEntries("group", applyDirectoryQueryAndLimit(ids, params)); +} diff --git a/src/plugin-sdk/googlechat.ts b/src/plugin-sdk/googlechat.ts index 0b84d803d5f..59bc1849119 100644 --- a/src/plugin-sdk/googlechat.ts +++ b/src/plugin-sdk/googlechat.ts @@ -14,6 +14,10 @@ export { deleteAccountFromConfigSection, setAccountEnabledInConfigSection, } from "../channels/plugins/config-helpers.js"; +export { + listDirectoryGroupEntriesFromMapKeys, + listDirectoryUserEntriesFromAllowFrom, +} from "../channels/plugins/directory-config-helpers.js"; export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; export { resolveGoogleChatGroupRequireMention } from "../channels/plugins/group-mentions.js"; export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; diff --git a/src/plugin-sdk/zalo.ts b/src/plugin-sdk/zalo.ts index 011b95e43cb..5732a4e138c 100644 --- a/src/plugin-sdk/zalo.ts +++ b/src/plugin-sdk/zalo.ts @@ -8,6 +8,7 @@ export { deleteAccountFromConfigSection, setAccountEnabledInConfigSection, } from "../channels/plugins/config-helpers.js"; +export { listDirectoryUserEntriesFromAllowFrom } from "../channels/plugins/directory-config-helpers.js"; export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; export type {