refactor(channels): share allowlist + resolver helpers
This commit is contained in:
@@ -25,6 +25,7 @@ import { probeMatrix } from "./matrix/probe.js";
|
|||||||
import { sendMessageMatrix } from "./matrix/send.js";
|
import { sendMessageMatrix } from "./matrix/send.js";
|
||||||
import { matrixOnboardingAdapter } from "./onboarding.js";
|
import { matrixOnboardingAdapter } from "./onboarding.js";
|
||||||
import { matrixOutbound } from "./outbound.js";
|
import { matrixOutbound } from "./outbound.js";
|
||||||
|
import { resolveMatrixTargets } from "./resolve-targets.js";
|
||||||
import {
|
import {
|
||||||
listMatrixDirectoryGroupsLive,
|
listMatrixDirectoryGroupsLive,
|
||||||
listMatrixDirectoryPeersLive,
|
listMatrixDirectoryPeersLive,
|
||||||
@@ -245,81 +246,8 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|||||||
listMatrixDirectoryGroupsLive({ cfg, query, limit }),
|
listMatrixDirectoryGroupsLive({ cfg, query, limit }),
|
||||||
},
|
},
|
||||||
resolver: {
|
resolver: {
|
||||||
resolveTargets: async ({ cfg, inputs, kind, runtime }) => {
|
resolveTargets: async ({ cfg, inputs, kind, runtime }) =>
|
||||||
const results = [];
|
resolveMatrixTargets({ cfg, inputs, kind, runtime }),
|
||||||
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;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
actions: matrixMessageActions,
|
actions: matrixMessageActions,
|
||||||
setup: {
|
setup: {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
resolveMatrixAllowListMatches,
|
resolveMatrixAllowListMatches,
|
||||||
normalizeAllowListLower,
|
normalizeAllowListLower,
|
||||||
} from "./allowlist.js";
|
} from "./allowlist.js";
|
||||||
|
import { mergeAllowlist, summarizeMapping } from "../../../../../src/channels/allowlists/resolve-utils.js";
|
||||||
import { registerMatrixAutoJoin } from "./auto-join.js";
|
import { registerMatrixAutoJoin } from "./auto-join.js";
|
||||||
import { createDirectRoomTracker } from "./direct.js";
|
import { createDirectRoomTracker } from "./direct.js";
|
||||||
import { downloadMatrixMedia } from "./media.js";
|
import { downloadMatrixMedia } from "./media.js";
|
||||||
@@ -53,56 +54,7 @@ import { resolveMentions } from "./mentions.js";
|
|||||||
import { deliverMatrixReplies } from "./replies.js";
|
import { deliverMatrixReplies } from "./replies.js";
|
||||||
import { resolveMatrixRoomConfig } from "./rooms.js";
|
import { resolveMatrixRoomConfig } from "./rooms.js";
|
||||||
import { resolveMatrixThreadRootId, resolveMatrixThreadTarget } from "./threads.js";
|
import { resolveMatrixThreadRootId, resolveMatrixThreadTarget } from "./threads.js";
|
||||||
import {
|
import { resolveMatrixTargets } from "../../resolve-targets.js";
|
||||||
listMatrixDirectoryGroupsLive,
|
|
||||||
listMatrixDirectoryPeersLive,
|
|
||||||
} from "../../directory-live.js";
|
|
||||||
|
|
||||||
function mergeAllowlist(params: {
|
|
||||||
existing?: Array<string | number>;
|
|
||||||
additions: string[];
|
|
||||||
}): string[] {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
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 MonitorMatrixOpts = {
|
export type MonitorMatrixOpts = {
|
||||||
runtime?: RuntimeEnv;
|
runtime?: RuntimeEnv;
|
||||||
@@ -146,27 +98,28 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|||||||
const mapping: string[] = [];
|
const mapping: string[] = [];
|
||||||
const unresolved: string[] = [];
|
const unresolved: string[] = [];
|
||||||
const additions: string[] = [];
|
const additions: string[] = [];
|
||||||
|
const pending: string[] = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (isMatrixUserId(entry)) {
|
if (isMatrixUserId(entry)) {
|
||||||
additions.push(entry);
|
additions.push(entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
pending.push(entry);
|
||||||
const matches = await listMatrixDirectoryPeersLive({
|
}
|
||||||
cfg,
|
if (pending.length > 0) {
|
||||||
query: entry,
|
const resolved = await resolveMatrixTargets({
|
||||||
limit: 5,
|
cfg,
|
||||||
});
|
inputs: pending,
|
||||||
const best = matches[0];
|
kind: "user",
|
||||||
if (best?.id) {
|
runtime,
|
||||||
additions.push(best.id);
|
});
|
||||||
mapping.push(`${entry}→${best.id}`);
|
for (const entry of resolved) {
|
||||||
|
if (entry.resolved && entry.id) {
|
||||||
|
additions.push(entry.id);
|
||||||
|
mapping.push(`${entry.input}→${entry.id}`);
|
||||||
} else {
|
} 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 });
|
allowFrom = mergeAllowlist({ existing: allowFrom, additions });
|
||||||
@@ -179,6 +132,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|||||||
const mapping: string[] = [];
|
const mapping: string[] = [];
|
||||||
const unresolved: string[] = [];
|
const unresolved: string[] = [];
|
||||||
const nextRooms = { ...roomsConfig };
|
const nextRooms = { ...roomsConfig };
|
||||||
|
const pending: Array<{ input: string; query: string }> = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const trimmed = entry.trim();
|
const trimmed = entry.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
@@ -190,28 +144,27 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|||||||
mapping.push(`${entry}→${cleaned}`);
|
mapping.push(`${entry}→${cleaned}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
pending.push({ input: entry, query: trimmed });
|
||||||
const matches = await listMatrixDirectoryGroupsLive({
|
}
|
||||||
cfg,
|
if (pending.length > 0) {
|
||||||
query: trimmed,
|
const resolved = await resolveMatrixTargets({
|
||||||
limit: 10,
|
cfg,
|
||||||
});
|
inputs: pending.map((entry) => entry.query),
|
||||||
const exact = matches.find(
|
kind: "group",
|
||||||
(match) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(),
|
runtime,
|
||||||
);
|
});
|
||||||
const best = exact ?? matches[0];
|
resolved.forEach((entry, index) => {
|
||||||
if (best?.id) {
|
const source = pending[index];
|
||||||
if (!nextRooms[best.id]) {
|
if (!source) return;
|
||||||
nextRooms[best.id] = roomsConfig[entry];
|
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 {
|
} 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;
|
roomsConfig = nextRooms;
|
||||||
summarizeMapping("matrix rooms", mapping, unresolved, runtime);
|
summarizeMapping("matrix rooms", mapping, unresolved, runtime);
|
||||||
|
|||||||
89
extensions/matrix/src/resolve-targets.ts
Normal file
89
extensions/matrix/src/resolve-targets.ts
Normal file
@@ -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<ChannelResolveResult[]> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -9,6 +9,10 @@ import { msteamsOnboardingAdapter } from "./onboarding.js";
|
|||||||
import { msteamsOutbound } from "./outbound.js";
|
import { msteamsOutbound } from "./outbound.js";
|
||||||
import { probeMSTeams } from "./probe.js";
|
import { probeMSTeams } from "./probe.js";
|
||||||
import {
|
import {
|
||||||
|
normalizeMSTeamsMessagingTarget,
|
||||||
|
normalizeMSTeamsUserInput,
|
||||||
|
parseMSTeamsConversationId,
|
||||||
|
parseMSTeamsTeamChannelInput,
|
||||||
resolveMSTeamsChannelAllowlist,
|
resolveMSTeamsChannelAllowlist,
|
||||||
resolveMSTeamsUserAllowlist,
|
resolveMSTeamsUserAllowlist,
|
||||||
} from "./resolve-allowlist.js";
|
} from "./resolve-allowlist.js";
|
||||||
@@ -36,21 +40,6 @@ const meta = {
|
|||||||
order: 60,
|
order: 60,
|
||||||
} as const;
|
} 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<ResolvedMSTeamsAccount> = {
|
export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||||
id: "msteams",
|
id: "msteams",
|
||||||
meta: {
|
meta: {
|
||||||
@@ -214,10 +203,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const stripPrefix = (value: string) =>
|
const stripPrefix = (value: string) =>
|
||||||
value
|
normalizeMSTeamsUserInput(value);
|
||||||
.replace(/^(msteams|teams):/i, "")
|
|
||||||
.replace(/^(user|conversation):/i, "")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
if (kind === "user") {
|
if (kind === "user") {
|
||||||
const pending: Array<{ input: string; query: string; index: number }> = [];
|
const pending: Array<{ input: string; query: string; index: number }> = [];
|
||||||
@@ -269,25 +255,20 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
|||||||
entry.note = "empty input";
|
entry.note = "empty input";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (/^conversation:/i.test(trimmed)) {
|
const conversationId = parseMSTeamsConversationId(trimmed);
|
||||||
const id = trimmed.replace(/^conversation:/i, "").trim();
|
if (conversationId !== null) {
|
||||||
if (id) {
|
entry.resolved = Boolean(conversationId);
|
||||||
entry.resolved = true;
|
entry.id = conversationId || undefined;
|
||||||
entry.id = id;
|
entry.note = conversationId ? "conversation id" : "empty conversation id";
|
||||||
entry.note = "conversation id";
|
|
||||||
} else {
|
|
||||||
entry.note = "empty conversation id";
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pending.push({
|
const parsed = parseMSTeamsTeamChannelInput(trimmed);
|
||||||
input: entry.input,
|
if (!parsed.team) {
|
||||||
query: trimmed
|
entry.note = "missing team";
|
||||||
.replace(/^(msteams|teams):/i, "")
|
return;
|
||||||
.replace(/^team:/i, "")
|
}
|
||||||
.trim(),
|
const query = parsed.channel ? `${parsed.team}/${parsed.channel}` : parsed.team;
|
||||||
index,
|
pending.push({ input: entry.input, query, index });
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pending.length > 0) {
|
if (pending.length > 0) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { resolveTextChunkLimit } from "../../../src/auto-reply/chunk.js";
|
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 type { ClawdbotConfig } from "../../../src/config/types.js";
|
||||||
import { getChildLogger } from "../../../src/logging.js";
|
import { getChildLogger } from "../../../src/logging.js";
|
||||||
import type { RuntimeEnv } from "../../../src/runtime.js";
|
import type { RuntimeEnv } from "../../../src/runtime.js";
|
||||||
@@ -18,52 +19,6 @@ import { resolveMSTeamsCredentials } from "./token.js";
|
|||||||
|
|
||||||
const log = getChildLogger({ name: "msteams" });
|
const log = getChildLogger({ name: "msteams" });
|
||||||
|
|
||||||
function mergeAllowlist(params: {
|
|
||||||
existing?: Array<string | number>;
|
|
||||||
additions: string[];
|
|
||||||
}): string[] {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
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 = {
|
export type MonitorMSTeamsOpts = {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
runtime?: RuntimeEnv;
|
runtime?: RuntimeEnv;
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import { promptChannelAccessConfig } from "../../../src/channels/plugins/onboard
|
|||||||
import { addWildcardAllowFrom } from "../../../src/channels/plugins/onboarding/helpers.js";
|
import { addWildcardAllowFrom } from "../../../src/channels/plugins/onboarding/helpers.js";
|
||||||
|
|
||||||
import { resolveMSTeamsCredentials } from "./token.js";
|
import { resolveMSTeamsCredentials } from "./token.js";
|
||||||
import { resolveMSTeamsChannelAllowlist } from "./resolve-allowlist.js";
|
import {
|
||||||
|
parseMSTeamsTeamEntry,
|
||||||
|
resolveMSTeamsChannelAllowlist,
|
||||||
|
} from "./resolve-allowlist.js";
|
||||||
|
|
||||||
const channel = "msteams" as const;
|
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 = {
|
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||||
label: "MS Teams",
|
label: "MS Teams",
|
||||||
channel,
|
channel,
|
||||||
|
|||||||
@@ -49,6 +49,69 @@ function readAccessToken(value: unknown): string | null {
|
|||||||
return 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 {
|
function normalizeQuery(value?: string | null): string {
|
||||||
return value?.trim() ?? "";
|
return value?.trim() ?? "";
|
||||||
}
|
}
|
||||||
@@ -86,15 +149,6 @@ async function resolveGraphToken(cfg: unknown): Promise<string> {
|
|||||||
return accessToken;
|
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<GraphGroup[]> {
|
async function listTeamsByName(token: string, query: string): Promise<GraphGroup[]> {
|
||||||
const escaped = escapeOData(query);
|
const escaped = escapeOData(query);
|
||||||
const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`;
|
const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`;
|
||||||
@@ -117,7 +171,7 @@ export async function resolveMSTeamsChannelAllowlist(params: {
|
|||||||
const results: MSTeamsChannelResolution[] = [];
|
const results: MSTeamsChannelResolution[] = [];
|
||||||
|
|
||||||
for (const input of params.entries) {
|
for (const input of params.entries) {
|
||||||
const { team, channel } = parseTeamChannelInput(input);
|
const { team, channel } = parseMSTeamsTeamChannelInput(input);
|
||||||
if (!team) {
|
if (!team) {
|
||||||
results.push({ input, resolved: false });
|
results.push({ input, resolved: false });
|
||||||
continue;
|
continue;
|
||||||
@@ -180,7 +234,7 @@ export async function resolveMSTeamsUserAllowlist(params: {
|
|||||||
const results: MSTeamsUserResolution[] = [];
|
const results: MSTeamsUserResolution[] = [];
|
||||||
|
|
||||||
for (const input of params.entries) {
|
for (const input of params.entries) {
|
||||||
const query = normalizeQuery(input);
|
const query = normalizeQuery(normalizeMSTeamsUserInput(input));
|
||||||
if (!query) {
|
if (!query) {
|
||||||
results.push({ input, resolved: false });
|
results.push({ input, resolved: false });
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
isControlCommandMessage,
|
isControlCommandMessage,
|
||||||
shouldComputeCommandAuthorized,
|
shouldComputeCommandAuthorized,
|
||||||
} from "../../../src/auto-reply/command-detection.js";
|
} 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 { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js";
|
||||||
import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js";
|
import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js";
|
||||||
import { loadCoreChannelDeps, type CoreChannelDeps } from "./core-bridge.js";
|
import { loadCoreChannelDeps, type CoreChannelDeps } from "./core-bridge.js";
|
||||||
@@ -32,52 +33,6 @@ export type ZalouserMonitorResult = {
|
|||||||
|
|
||||||
const ZALOUSER_TEXT_LIMIT = 2000;
|
const ZALOUSER_TEXT_LIMIT = 2000;
|
||||||
|
|
||||||
function mergeAllowlist(params: {
|
|
||||||
existing?: Array<string | number>;
|
|
||||||
additions: string[];
|
|
||||||
}): string[] {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
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 {
|
function normalizeZalouserEntry(entry: string): string {
|
||||||
return entry.replace(/^(zalouser|zlu):/i, "").trim();
|
return entry.replace(/^(zalouser|zlu):/i, "").trim();
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/channels/allowlists/resolve-utils.ts
Normal file
47
src/channels/allowlists/resolve-utils.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
|
|
||||||
|
export function mergeAllowlist(params: {
|
||||||
|
existing?: Array<string | number>;
|
||||||
|
additions: string[];
|
||||||
|
}): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
|||||||
import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js";
|
import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js";
|
||||||
import { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js";
|
import { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js";
|
||||||
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
||||||
|
import { mergeAllowlist, summarizeMapping } from "../../channels/allowlists/resolve-utils.js";
|
||||||
import {
|
import {
|
||||||
isNativeCommandsExplicitlyDisabled,
|
isNativeCommandsExplicitlyDisabled,
|
||||||
resolveNativeCommandsEnabled,
|
resolveNativeCommandsEnabled,
|
||||||
@@ -60,52 +61,6 @@ function summarizeGuilds(entries?: Record<string, unknown>) {
|
|||||||
return `${sample.join(", ")}${suffix}`;
|
return `${sample.join(", ")}${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeAllowlist(params: {
|
|
||||||
existing?: Array<string | number>;
|
|
||||||
additions: string[];
|
|
||||||
}): string[] {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
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 = {}) {
|
export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||||
const cfg = opts.config ?? loadConfig();
|
const cfg = opts.config ?? loadConfig();
|
||||||
const account = resolveDiscordAccount({
|
const account = resolveDiscordAccount({
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { App } from "@slack/bolt";
|
|||||||
|
|
||||||
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
||||||
import { DEFAULT_GROUP_HISTORY_LIMIT } from "../../auto-reply/reply/history.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 { loadConfig } from "../../config/config.js";
|
||||||
import type { SessionScope } from "../../config/sessions.js";
|
import type { SessionScope } from "../../config/sessions.js";
|
||||||
import type { DmPolicy, GroupPolicy } from "../../config/types.js";
|
import type { DmPolicy, GroupPolicy } from "../../config/types.js";
|
||||||
@@ -28,52 +29,6 @@ function parseApiAppIdFromAppToken(raw?: string) {
|
|||||||
return match?.[1]?.toUpperCase();
|
return match?.[1]?.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeAllowlist(params: {
|
|
||||||
existing?: Array<string | number>;
|
|
||||||
additions: string[];
|
|
||||||
}): string[] {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
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 = {}) {
|
export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||||
const cfg = opts.config ?? loadConfig();
|
const cfg = opts.config ?? loadConfig();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user