diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index 79cbc974a..69285ff95 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -25,6 +25,7 @@ import { probeMatrix } from "./matrix/probe.js"; import { sendMessageMatrix } from "./matrix/send.js"; import { matrixOnboardingAdapter } from "./onboarding.js"; import { matrixOutbound } from "./outbound.js"; +import { resolveMatrixTargets } from "./resolve-targets.js"; import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive, @@ -245,81 +246,8 @@ export const matrixPlugin: ChannelPlugin = { listMatrixDirectoryGroupsLive({ cfg, query, limit }), }, resolver: { - resolveTargets: async ({ cfg, inputs, kind, runtime }) => { - const results = []; - for (const input of inputs) { - const trimmed = input.trim(); - if (!trimmed) { - results.push({ input, resolved: false, note: "empty input" }); - continue; - } - if (kind === "user") { - if (trimmed.startsWith("@") && trimmed.includes(":")) { - results.push({ input, resolved: true, id: trimmed }); - continue; - } - try { - const matches = await listMatrixDirectoryPeersLive({ - cfg, - query: trimmed, - limit: 5, - }); - const best = matches[0]; - results.push({ - input, - resolved: Boolean(best?.id), - id: best?.id, - name: best?.name, - note: matches.length > 1 ? "multiple matches; chose first" : undefined, - }); - } catch (err) { - runtime.error?.(`matrix resolve failed: ${String(err)}`); - results.push({ input, resolved: false, note: "lookup failed" }); - } - continue; - } - if (trimmed.startsWith("!") || trimmed.startsWith("#")) { - try { - const matches = await listMatrixDirectoryGroupsLive({ - cfg, - query: trimmed, - limit: 5, - }); - const best = matches[0]; - results.push({ - input, - resolved: Boolean(best?.id), - id: best?.id, - name: best?.name, - note: matches.length > 1 ? "multiple matches; chose first" : undefined, - }); - } catch (err) { - runtime.error?.(`matrix resolve failed: ${String(err)}`); - results.push({ input, resolved: false, note: "lookup failed" }); - } - continue; - } - try { - const matches = await listMatrixDirectoryGroupsLive({ - cfg, - query: trimmed, - limit: 5, - }); - const best = matches[0]; - results.push({ - input, - resolved: Boolean(best?.id), - id: best?.id, - name: best?.name, - note: matches.length > 1 ? "multiple matches; chose first" : undefined, - }); - } catch (err) { - runtime.error?.(`matrix resolve failed: ${String(err)}`); - results.push({ input, resolved: false, note: "lookup failed" }); - } - } - return results; - }, + resolveTargets: async ({ cfg, inputs, kind, runtime }) => + resolveMatrixTargets({ cfg, inputs, kind, runtime }), }, actions: matrixMessageActions, setup: { diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index 5e9bfa877..d4781ecfe 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -46,6 +46,7 @@ import { resolveMatrixAllowListMatches, normalizeAllowListLower, } from "./allowlist.js"; +import { mergeAllowlist, summarizeMapping } from "../../../../../src/channels/allowlists/resolve-utils.js"; import { registerMatrixAutoJoin } from "./auto-join.js"; import { createDirectRoomTracker } from "./direct.js"; import { downloadMatrixMedia } from "./media.js"; @@ -53,56 +54,7 @@ import { resolveMentions } from "./mentions.js"; import { deliverMatrixReplies } from "./replies.js"; import { resolveMatrixRoomConfig } from "./rooms.js"; import { resolveMatrixThreadRootId, resolveMatrixThreadTarget } from "./threads.js"; -import { - listMatrixDirectoryGroupsLive, - listMatrixDirectoryPeersLive, -} from "../../directory-live.js"; - -function mergeAllowlist(params: { - existing?: Array; - additions: string[]; -}): string[] { - const seen = new Set(); - const merged: string[] = []; - const push = (value: string) => { - const normalized = value.trim(); - if (!normalized) return; - const key = normalized.toLowerCase(); - if (seen.has(key)) return; - seen.add(key); - merged.push(normalized); - }; - for (const entry of params.existing ?? []) { - push(String(entry)); - } - for (const entry of params.additions) { - push(entry); - } - return merged; -} - -function summarizeMapping( - label: string, - mapping: string[], - unresolved: string[], - runtime: RuntimeEnv, -) { - const lines: string[] = []; - if (mapping.length > 0) { - const sample = mapping.slice(0, 6); - const suffix = mapping.length > sample.length ? ` (+${mapping.length - sample.length})` : ""; - lines.push(`${label} resolved: ${sample.join(", ")}${suffix}`); - } - if (unresolved.length > 0) { - const sample = unresolved.slice(0, 6); - const suffix = - unresolved.length > sample.length ? ` (+${unresolved.length - sample.length})` : ""; - lines.push(`${label} unresolved: ${sample.join(", ")}${suffix}`); - } - if (lines.length > 0) { - runtime.log?.(lines.join("\n")); - } -} +import { resolveMatrixTargets } from "../../resolve-targets.js"; export type MonitorMatrixOpts = { runtime?: RuntimeEnv; @@ -146,27 +98,28 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi const mapping: string[] = []; const unresolved: string[] = []; const additions: string[] = []; + const pending: string[] = []; for (const entry of entries) { if (isMatrixUserId(entry)) { additions.push(entry); continue; } - try { - const matches = await listMatrixDirectoryPeersLive({ - cfg, - query: entry, - limit: 5, - }); - const best = matches[0]; - if (best?.id) { - additions.push(best.id); - mapping.push(`${entry}→${best.id}`); + pending.push(entry); + } + if (pending.length > 0) { + const resolved = await resolveMatrixTargets({ + cfg, + inputs: pending, + kind: "user", + runtime, + }); + for (const entry of resolved) { + if (entry.resolved && entry.id) { + additions.push(entry.id); + mapping.push(`${entry.input}→${entry.id}`); } else { - unresolved.push(entry); + unresolved.push(entry.input); } - } catch (err) { - runtime.log?.(`matrix user resolve failed; using config entries. ${String(err)}`); - unresolved.push(entry); } } allowFrom = mergeAllowlist({ existing: allowFrom, additions }); @@ -179,6 +132,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi const mapping: string[] = []; const unresolved: string[] = []; const nextRooms = { ...roomsConfig }; + const pending: Array<{ input: string; query: string }> = []; for (const entry of entries) { const trimmed = entry.trim(); if (!trimmed) continue; @@ -190,28 +144,27 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi mapping.push(`${entry}→${cleaned}`); continue; } - try { - const matches = await listMatrixDirectoryGroupsLive({ - cfg, - query: trimmed, - limit: 10, - }); - const exact = matches.find( - (match) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(), - ); - const best = exact ?? matches[0]; - if (best?.id) { - if (!nextRooms[best.id]) { - nextRooms[best.id] = roomsConfig[entry]; + pending.push({ input: entry, query: trimmed }); + } + if (pending.length > 0) { + const resolved = await resolveMatrixTargets({ + cfg, + inputs: pending.map((entry) => entry.query), + kind: "group", + runtime, + }); + resolved.forEach((entry, index) => { + const source = pending[index]; + if (!source) return; + if (entry.resolved && entry.id) { + if (!nextRooms[entry.id]) { + nextRooms[entry.id] = roomsConfig[source.input]; } - mapping.push(`${entry}→${best.id}`); + mapping.push(`${source.input}→${entry.id}`); } else { - unresolved.push(entry); + unresolved.push(source.input); } - } catch (err) { - runtime.log?.(`matrix room resolve failed; using config entries. ${String(err)}`); - unresolved.push(entry); - } + }); } roomsConfig = nextRooms; summarizeMapping("matrix rooms", mapping, unresolved, runtime); diff --git a/extensions/matrix/src/resolve-targets.ts b/extensions/matrix/src/resolve-targets.ts new file mode 100644 index 000000000..306ae0aa1 --- /dev/null +++ b/extensions/matrix/src/resolve-targets.ts @@ -0,0 +1,89 @@ +import type { + ChannelDirectoryEntry, + ChannelResolveKind, + ChannelResolveResult, +} from "../../../src/channels/plugins/types.js"; +import type { RuntimeEnv } from "../../../src/runtime.js"; + +import { + listMatrixDirectoryGroupsLive, + listMatrixDirectoryPeersLive, +} from "./directory-live.js"; + +function pickBestGroupMatch( + matches: ChannelDirectoryEntry[], + query: string, +): ChannelDirectoryEntry | undefined { + if (matches.length === 0) return undefined; + const normalized = query.trim().toLowerCase(); + if (normalized) { + const exact = matches.find((match) => { + const name = match.name?.trim().toLowerCase(); + const handle = match.handle?.trim().toLowerCase(); + const id = match.id.trim().toLowerCase(); + return name === normalized || handle === normalized || id === normalized; + }); + if (exact) return exact; + } + return matches[0]; +} + +export async function resolveMatrixTargets(params: { + cfg: unknown; + inputs: string[]; + kind: ChannelResolveKind; + runtime?: RuntimeEnv; +}): Promise { + const results: ChannelResolveResult[] = []; + for (const input of params.inputs) { + const trimmed = input.trim(); + if (!trimmed) { + results.push({ input, resolved: false, note: "empty input" }); + continue; + } + if (params.kind === "user") { + if (trimmed.startsWith("@") && trimmed.includes(":")) { + results.push({ input, resolved: true, id: trimmed }); + continue; + } + try { + const matches = await listMatrixDirectoryPeersLive({ + cfg: params.cfg, + query: trimmed, + limit: 5, + }); + const best = matches[0]; + results.push({ + input, + resolved: Boolean(best?.id), + id: best?.id, + name: best?.name, + note: matches.length > 1 ? "multiple matches; chose first" : undefined, + }); + } catch (err) { + params.runtime?.error?.(`matrix resolve failed: ${String(err)}`); + results.push({ input, resolved: false, note: "lookup failed" }); + } + continue; + } + try { + const matches = await listMatrixDirectoryGroupsLive({ + cfg: params.cfg, + query: trimmed, + limit: 5, + }); + const best = pickBestGroupMatch(matches, trimmed); + results.push({ + input, + resolved: Boolean(best?.id), + id: best?.id, + name: best?.name, + note: matches.length > 1 ? "multiple matches; chose first" : undefined, + }); + } catch (err) { + params.runtime?.error?.(`matrix resolve failed: ${String(err)}`); + results.push({ input, resolved: false, note: "lookup failed" }); + } + } + return results; +} diff --git a/extensions/msteams/src/channel.ts b/extensions/msteams/src/channel.ts index 08245c91d..40e22ad79 100644 --- a/extensions/msteams/src/channel.ts +++ b/extensions/msteams/src/channel.ts @@ -9,6 +9,10 @@ import { msteamsOnboardingAdapter } from "./onboarding.js"; import { msteamsOutbound } from "./outbound.js"; import { probeMSTeams } from "./probe.js"; import { + normalizeMSTeamsMessagingTarget, + normalizeMSTeamsUserInput, + parseMSTeamsConversationId, + parseMSTeamsTeamChannelInput, resolveMSTeamsChannelAllowlist, resolveMSTeamsUserAllowlist, } from "./resolve-allowlist.js"; @@ -36,21 +40,6 @@ const meta = { order: 60, } as const; -function normalizeMSTeamsMessagingTarget(raw: string): string | undefined { - let trimmed = raw.trim(); - if (!trimmed) return undefined; - if (/^(msteams|teams):/i.test(trimmed)) { - trimmed = trimmed.replace(/^(msteams|teams):/i, ""); - } - if (/^conversation:/i.test(trimmed)) { - return `conversation:${trimmed.slice("conversation:".length).trim()}`; - } - if (/^user:/i.test(trimmed)) { - return `user:${trimmed.slice("user:".length).trim()}`; - } - return trimmed; -} - export const msteamsPlugin: ChannelPlugin = { id: "msteams", meta: { @@ -214,10 +203,7 @@ export const msteamsPlugin: ChannelPlugin = { })); const stripPrefix = (value: string) => - value - .replace(/^(msteams|teams):/i, "") - .replace(/^(user|conversation):/i, "") - .trim(); + normalizeMSTeamsUserInput(value); if (kind === "user") { const pending: Array<{ input: string; query: string; index: number }> = []; @@ -269,25 +255,20 @@ export const msteamsPlugin: ChannelPlugin = { entry.note = "empty input"; return; } - if (/^conversation:/i.test(trimmed)) { - const id = trimmed.replace(/^conversation:/i, "").trim(); - if (id) { - entry.resolved = true; - entry.id = id; - entry.note = "conversation id"; - } else { - entry.note = "empty conversation id"; - } + const conversationId = parseMSTeamsConversationId(trimmed); + if (conversationId !== null) { + entry.resolved = Boolean(conversationId); + entry.id = conversationId || undefined; + entry.note = conversationId ? "conversation id" : "empty conversation id"; return; } - pending.push({ - input: entry.input, - query: trimmed - .replace(/^(msteams|teams):/i, "") - .replace(/^team:/i, "") - .trim(), - index, - }); + const parsed = parseMSTeamsTeamChannelInput(trimmed); + if (!parsed.team) { + entry.note = "missing team"; + return; + } + const query = parsed.channel ? `${parsed.team}/${parsed.channel}` : parsed.team; + pending.push({ input: entry.input, query, index }); }); if (pending.length > 0) { diff --git a/extensions/msteams/src/monitor.ts b/extensions/msteams/src/monitor.ts index 0e2662fb7..70a47608f 100644 --- a/extensions/msteams/src/monitor.ts +++ b/extensions/msteams/src/monitor.ts @@ -1,5 +1,6 @@ import type { Request, Response } from "express"; import { resolveTextChunkLimit } from "../../../src/auto-reply/chunk.js"; +import { mergeAllowlist, summarizeMapping } from "../../../src/channels/allowlists/resolve-utils.js"; import type { ClawdbotConfig } from "../../../src/config/types.js"; import { getChildLogger } from "../../../src/logging.js"; import type { RuntimeEnv } from "../../../src/runtime.js"; @@ -18,52 +19,6 @@ import { resolveMSTeamsCredentials } from "./token.js"; const log = getChildLogger({ name: "msteams" }); -function mergeAllowlist(params: { - existing?: Array; - additions: string[]; -}): string[] { - const seen = new Set(); - const merged: string[] = []; - const push = (value: string) => { - const normalized = value.trim(); - if (!normalized) return; - const key = normalized.toLowerCase(); - if (seen.has(key)) return; - seen.add(key); - merged.push(normalized); - }; - for (const entry of params.existing ?? []) { - push(String(entry)); - } - for (const entry of params.additions) { - push(entry); - } - return merged; -} - -function summarizeMapping( - label: string, - mapping: string[], - unresolved: string[], - runtime: RuntimeEnv, -) { - const lines: string[] = []; - if (mapping.length > 0) { - const sample = mapping.slice(0, 6); - const suffix = mapping.length > sample.length ? ` (+${mapping.length - sample.length})` : ""; - lines.push(`${label} resolved: ${sample.join(", ")}${suffix}`); - } - if (unresolved.length > 0) { - const sample = unresolved.slice(0, 6); - const suffix = - unresolved.length > sample.length ? ` (+${unresolved.length - sample.length})` : ""; - lines.push(`${label} unresolved: ${sample.join(", ")}${suffix}`); - } - if (lines.length > 0) { - runtime.log?.(lines.join("\n")); - } -} - export type MonitorMSTeamsOpts = { cfg: ClawdbotConfig; runtime?: RuntimeEnv; diff --git a/extensions/msteams/src/onboarding.ts b/extensions/msteams/src/onboarding.ts index f9348397e..539068ddd 100644 --- a/extensions/msteams/src/onboarding.ts +++ b/extensions/msteams/src/onboarding.ts @@ -11,7 +11,10 @@ import { promptChannelAccessConfig } from "../../../src/channels/plugins/onboard import { addWildcardAllowFrom } from "../../../src/channels/plugins/onboarding/helpers.js"; import { resolveMSTeamsCredentials } from "./token.js"; -import { resolveMSTeamsChannelAllowlist } from "./resolve-allowlist.js"; +import { + parseMSTeamsTeamEntry, + resolveMSTeamsChannelAllowlist, +} from "./resolve-allowlist.js"; const channel = "msteams" as const; @@ -94,18 +97,6 @@ function setMSTeamsTeamsAllowlist( }; } -function parseMSTeamsTeamEntry(raw: string): { teamKey: string; channelKey?: string } | null { - const trimmed = raw.trim(); - if (!trimmed) return null; - const parts = trimmed.split("/"); - const teamPart = parts[0]?.trim(); - if (!teamPart) return null; - const channelPart = parts.length > 1 ? parts.slice(1).join("/").trim() : undefined; - const teamKey = teamPart.replace(/^team:/i, "").trim(); - const channelKey = channelPart ? channelPart.replace(/^#/, "").trim() : undefined; - return { teamKey, ...(channelKey ? { channelKey } : {}) }; -} - const dmPolicy: ChannelOnboardingDmPolicy = { label: "MS Teams", channel, diff --git a/extensions/msteams/src/resolve-allowlist.ts b/extensions/msteams/src/resolve-allowlist.ts index 5ba69d9d1..a74c42f61 100644 --- a/extensions/msteams/src/resolve-allowlist.ts +++ b/extensions/msteams/src/resolve-allowlist.ts @@ -49,6 +49,69 @@ function readAccessToken(value: unknown): string | null { return null; } +function stripProviderPrefix(raw: string): string { + return raw.replace(/^(msteams|teams):/i, ""); +} + +export function normalizeMSTeamsMessagingTarget(raw: string): string | undefined { + let trimmed = raw.trim(); + if (!trimmed) return undefined; + trimmed = stripProviderPrefix(trimmed).trim(); + if (/^conversation:/i.test(trimmed)) { + const id = trimmed.slice("conversation:".length).trim(); + return id ? `conversation:${id}` : undefined; + } + if (/^user:/i.test(trimmed)) { + const id = trimmed.slice("user:".length).trim(); + return id ? `user:${id}` : undefined; + } + return trimmed || undefined; +} + +export function normalizeMSTeamsUserInput(raw: string): string { + return stripProviderPrefix(raw).replace(/^(user|conversation):/i, "").trim(); +} + +export function parseMSTeamsConversationId(raw: string): string | null { + const trimmed = stripProviderPrefix(raw).trim(); + if (!/^conversation:/i.test(trimmed)) return null; + const id = trimmed.slice("conversation:".length).trim(); + return id; +} + +function normalizeMSTeamsTeamKey(raw: string): string | undefined { + const trimmed = stripProviderPrefix(raw).replace(/^team:/i, "").trim(); + return trimmed || undefined; +} + +function normalizeMSTeamsChannelKey(raw?: string | null): string | undefined { + const trimmed = raw?.trim().replace(/^#/, "").trim() ?? ""; + return trimmed || undefined; +} + +export function parseMSTeamsTeamChannelInput(raw: string): { team?: string; channel?: string } { + const trimmed = stripProviderPrefix(raw).trim(); + if (!trimmed) return {}; + const parts = trimmed.split("/"); + const team = normalizeMSTeamsTeamKey(parts[0] ?? ""); + const channel = parts.length > 1 ? normalizeMSTeamsChannelKey(parts.slice(1).join("/")) : undefined; + return { + ...(team ? { team } : {}), + ...(channel ? { channel } : {}), + }; +} + +export function parseMSTeamsTeamEntry( + raw: string, +): { teamKey: string; channelKey?: string } | null { + const { team, channel } = parseMSTeamsTeamChannelInput(raw); + if (!team) return null; + return { + teamKey: team, + ...(channel ? { channelKey: channel } : {}), + }; +} + function normalizeQuery(value?: string | null): string { return value?.trim() ?? ""; } @@ -86,15 +149,6 @@ async function resolveGraphToken(cfg: unknown): Promise { return accessToken; } -function parseTeamChannelInput(raw: string): { team?: string; channel?: string } { - const trimmed = raw.trim(); - if (!trimmed) return {}; - const parts = trimmed.split("/"); - const team = parts[0]?.trim(); - const channel = parts.length > 1 ? parts.slice(1).join("/").trim() : undefined; - return { team: team || undefined, channel: channel || undefined }; -} - async function listTeamsByName(token: string, query: string): Promise { const escaped = escapeOData(query); const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`; @@ -117,7 +171,7 @@ export async function resolveMSTeamsChannelAllowlist(params: { const results: MSTeamsChannelResolution[] = []; for (const input of params.entries) { - const { team, channel } = parseTeamChannelInput(input); + const { team, channel } = parseMSTeamsTeamChannelInput(input); if (!team) { results.push({ input, resolved: false }); continue; @@ -180,7 +234,7 @@ export async function resolveMSTeamsUserAllowlist(params: { const results: MSTeamsUserResolution[] = []; for (const input of params.entries) { - const query = normalizeQuery(input); + const query = normalizeQuery(normalizeMSTeamsUserInput(input)); if (!query) { results.push({ input, resolved: false }); continue; diff --git a/extensions/zalouser/src/monitor.ts b/extensions/zalouser/src/monitor.ts index 9df8ad0a1..1e95e0b36 100644 --- a/extensions/zalouser/src/monitor.ts +++ b/extensions/zalouser/src/monitor.ts @@ -5,6 +5,7 @@ import { isControlCommandMessage, shouldComputeCommandAuthorized, } from "../../../src/auto-reply/command-detection.js"; +import { mergeAllowlist, summarizeMapping } from "../../../src/channels/allowlists/resolve-utils.js"; import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js"; import { loadCoreChannelDeps, type CoreChannelDeps } from "./core-bridge.js"; @@ -32,52 +33,6 @@ export type ZalouserMonitorResult = { const ZALOUSER_TEXT_LIMIT = 2000; -function mergeAllowlist(params: { - existing?: Array; - additions: string[]; -}): string[] { - const seen = new Set(); - const merged: string[] = []; - const push = (value: string) => { - const normalized = value.trim(); - if (!normalized) return; - const key = normalized.toLowerCase(); - if (seen.has(key)) return; - seen.add(key); - merged.push(normalized); - }; - for (const entry of params.existing ?? []) { - push(String(entry)); - } - for (const entry of params.additions) { - push(entry); - } - return merged; -} - -function summarizeMapping( - label: string, - mapping: string[], - unresolved: string[], - runtime: RuntimeEnv, -) { - const lines: string[] = []; - if (mapping.length > 0) { - const sample = mapping.slice(0, 6); - const suffix = mapping.length > sample.length ? ` (+${mapping.length - sample.length})` : ""; - lines.push(`${label} resolved: ${sample.join(", ")}${suffix}`); - } - if (unresolved.length > 0) { - const sample = unresolved.slice(0, 6); - const suffix = - unresolved.length > sample.length ? ` (+${unresolved.length - sample.length})` : ""; - lines.push(`${label} unresolved: ${sample.join(", ")}${suffix}`); - } - if (lines.length > 0) { - runtime.log?.(lines.join("\n")); - } -} - function normalizeZalouserEntry(entry: string): string { return entry.replace(/^(zalouser|zlu):/i, "").trim(); } diff --git a/src/channels/allowlists/resolve-utils.ts b/src/channels/allowlists/resolve-utils.ts new file mode 100644 index 000000000..94ed8fb2a --- /dev/null +++ b/src/channels/allowlists/resolve-utils.ts @@ -0,0 +1,47 @@ +import type { RuntimeEnv } from "../../runtime.js"; + +export function mergeAllowlist(params: { + existing?: Array; + additions: string[]; +}): string[] { + const seen = new Set(); + const merged: string[] = []; + const push = (value: string) => { + const normalized = value.trim(); + if (!normalized) return; + const key = normalized.toLowerCase(); + if (seen.has(key)) return; + seen.add(key); + merged.push(normalized); + }; + for (const entry of params.existing ?? []) { + push(String(entry)); + } + for (const entry of params.additions) { + push(entry); + } + return merged; +} + +export function summarizeMapping( + label: string, + mapping: string[], + unresolved: string[], + runtime: RuntimeEnv, +): void { + const lines: string[] = []; + if (mapping.length > 0) { + const sample = mapping.slice(0, 6); + const suffix = mapping.length > sample.length ? ` (+${mapping.length - sample.length})` : ""; + lines.push(`${label} resolved: ${sample.join(", ")}${suffix}`); + } + if (unresolved.length > 0) { + const sample = unresolved.slice(0, 6); + const suffix = + unresolved.length > sample.length ? ` (+${unresolved.length - sample.length})` : ""; + lines.push(`${label} unresolved: ${sample.join(", ")}${suffix}`); + } + if (lines.length > 0) { + runtime.log?.(lines.join("\n")); + } +} diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index 4e4179fa9..e39116d9a 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -5,6 +5,7 @@ import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js"; import { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js"; import type { HistoryEntry } from "../../auto-reply/reply/history.js"; +import { mergeAllowlist, summarizeMapping } from "../../channels/allowlists/resolve-utils.js"; import { isNativeCommandsExplicitlyDisabled, resolveNativeCommandsEnabled, @@ -60,52 +61,6 @@ function summarizeGuilds(entries?: Record) { return `${sample.join(", ")}${suffix}`; } -function mergeAllowlist(params: { - existing?: Array; - additions: string[]; -}): string[] { - const seen = new Set(); - const merged: string[] = []; - const push = (value: string) => { - const normalized = value.trim(); - if (!normalized) return; - const key = normalized.toLowerCase(); - if (seen.has(key)) return; - seen.add(key); - merged.push(normalized); - }; - for (const entry of params.existing ?? []) { - push(String(entry)); - } - for (const entry of params.additions) { - push(entry); - } - return merged; -} - -function summarizeMapping( - label: string, - mapping: string[], - unresolved: string[], - runtime: RuntimeEnv, -) { - const lines: string[] = []; - if (mapping.length > 0) { - const sample = mapping.slice(0, 6); - const suffix = mapping.length > sample.length ? ` (+${mapping.length - sample.length})` : ""; - lines.push(`${label} resolved: ${sample.join(", ")}${suffix}`); - } - if (unresolved.length > 0) { - const sample = unresolved.slice(0, 6); - const suffix = - unresolved.length > sample.length ? ` (+${unresolved.length - sample.length})` : ""; - lines.push(`${label} unresolved: ${sample.join(", ")}${suffix}`); - } - if (lines.length > 0) { - runtime.log?.(lines.join("\n")); - } -} - export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { const cfg = opts.config ?? loadConfig(); const account = resolveDiscordAccount({ diff --git a/src/slack/monitor/provider.ts b/src/slack/monitor/provider.ts index 69e805042..1bbf5c9ae 100644 --- a/src/slack/monitor/provider.ts +++ b/src/slack/monitor/provider.ts @@ -2,6 +2,7 @@ import { App } from "@slack/bolt"; import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import { DEFAULT_GROUP_HISTORY_LIMIT } from "../../auto-reply/reply/history.js"; +import { mergeAllowlist, summarizeMapping } from "../../channels/allowlists/resolve-utils.js"; import { loadConfig } from "../../config/config.js"; import type { SessionScope } from "../../config/sessions.js"; import type { DmPolicy, GroupPolicy } from "../../config/types.js"; @@ -28,52 +29,6 @@ function parseApiAppIdFromAppToken(raw?: string) { return match?.[1]?.toUpperCase(); } -function mergeAllowlist(params: { - existing?: Array; - additions: string[]; -}): string[] { - const seen = new Set(); - const merged: string[] = []; - const push = (value: string) => { - const normalized = value.trim(); - if (!normalized) return; - const key = normalized.toLowerCase(); - if (seen.has(key)) return; - seen.add(key); - merged.push(normalized); - }; - for (const entry of params.existing ?? []) { - push(String(entry)); - } - for (const entry of params.additions) { - push(entry); - } - return merged; -} - -function summarizeMapping( - label: string, - mapping: string[], - unresolved: string[], - runtime: RuntimeEnv, -) { - const lines: string[] = []; - if (mapping.length > 0) { - const sample = mapping.slice(0, 6); - const suffix = mapping.length > sample.length ? ` (+${mapping.length - sample.length})` : ""; - lines.push(`${label} resolved: ${sample.join(", ")}${suffix}`); - } - if (unresolved.length > 0) { - const sample = unresolved.slice(0, 6); - const suffix = - unresolved.length > sample.length ? ` (+${unresolved.length - sample.length})` : ""; - lines.push(`${label} unresolved: ${sample.join(", ")}${suffix}`); - } - if (lines.length > 0) { - runtime.log?.(lines.join("\n")); - } -} - export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const cfg = opts.config ?? loadConfig();