feat: resolve allowlists in channel plugins
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
type WizardPrompter,
|
type WizardPrompter,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
|
import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
|
||||||
|
import { listMatrixDirectoryPeersLive } from "./directory-live.js";
|
||||||
import { resolveMatrixAccount } from "./matrix/accounts.js";
|
import { resolveMatrixAccount } from "./matrix/accounts.js";
|
||||||
import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
|
import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
|
||||||
import type { CoreConfig, DmPolicy } from "./types.js";
|
import type { CoreConfig, DmPolicy } from "./types.js";
|
||||||
@@ -49,40 +50,86 @@ async function promptMatrixAllowFrom(params: {
|
|||||||
}): Promise<CoreConfig> {
|
}): Promise<CoreConfig> {
|
||||||
const { cfg, prompter } = params;
|
const { cfg, prompter } = params;
|
||||||
const existingAllowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? [];
|
const existingAllowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? [];
|
||||||
const entry = await prompter.text({
|
const account = resolveMatrixAccount({ cfg });
|
||||||
message: "Matrix allowFrom (user id)",
|
const canResolve = Boolean(account.configured);
|
||||||
placeholder: "@user:server",
|
|
||||||
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
|
||||||
validate: (value) => {
|
|
||||||
const raw = String(value ?? "").trim();
|
|
||||||
if (!raw) return "Required";
|
|
||||||
if (!raw.startsWith("@")) return "Matrix user IDs should start with @";
|
|
||||||
if (!raw.includes(":")) return "Matrix user IDs should include a server (:@server)";
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const normalized = String(entry).trim();
|
|
||||||
const merged = [
|
|
||||||
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
|
||||||
normalized,
|
|
||||||
];
|
|
||||||
const unique = [...new Set(merged)];
|
|
||||||
|
|
||||||
return {
|
const parseInput = (raw: string) =>
|
||||||
...cfg,
|
raw
|
||||||
channels: {
|
.split(/[\n,;]+/g)
|
||||||
...cfg.channels,
|
.map((entry) => entry.trim())
|
||||||
matrix: {
|
.filter(Boolean);
|
||||||
...cfg.channels?.matrix,
|
|
||||||
enabled: true,
|
const isFullUserId = (value: string) => value.startsWith("@") && value.includes(":");
|
||||||
dm: {
|
|
||||||
...cfg.channels?.matrix?.dm,
|
while (true) {
|
||||||
policy: "allowlist",
|
const entry = await prompter.text({
|
||||||
allowFrom: unique,
|
message: "Matrix allowFrom (username or user id)",
|
||||||
|
placeholder: "@user:server",
|
||||||
|
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
||||||
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||||
|
});
|
||||||
|
const parts = parseInput(String(entry));
|
||||||
|
const resolvedIds: string[] = [];
|
||||||
|
let unresolved: string[] = [];
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (isFullUserId(part)) {
|
||||||
|
resolvedIds.push(part);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!canResolve) {
|
||||||
|
unresolved.push(part);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const results = await listMatrixDirectoryPeersLive({
|
||||||
|
cfg,
|
||||||
|
query: part,
|
||||||
|
limit: 5,
|
||||||
|
}).catch(() => []);
|
||||||
|
const match = results.find((result) => result.id);
|
||||||
|
if (match?.id) {
|
||||||
|
resolvedIds.push(match.id);
|
||||||
|
if (results.length > 1) {
|
||||||
|
await prompter.note(
|
||||||
|
`Multiple matches for "${part}", using ${match.id}.`,
|
||||||
|
"Matrix allowlist",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unresolved.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unresolved.length > 0) {
|
||||||
|
await prompter.note(
|
||||||
|
`Could not resolve: ${unresolved.join(", ")}. Use full @user:server IDs.`,
|
||||||
|
"Matrix allowlist",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unique = [
|
||||||
|
...new Set([
|
||||||
|
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
||||||
|
...resolvedIds,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
matrix: {
|
||||||
|
...cfg.channels?.matrix,
|
||||||
|
enabled: true,
|
||||||
|
dm: {
|
||||||
|
...cfg.channels?.matrix?.dm,
|
||||||
|
policy: "allowlist",
|
||||||
|
allowFrom: unique,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMatrixGroupPolicy(cfg: CoreConfig, groupPolicy: "open" | "allowlist" | "disabled") {
|
function setMatrixGroupPolicy(cfg: CoreConfig, groupPolicy: "open" | "allowlist" | "disabled") {
|
||||||
@@ -121,6 +168,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|||||||
allowFromKey: "channels.matrix.dm.allowFrom",
|
allowFromKey: "channels.matrix.dm.allowFrom",
|
||||||
getCurrent: (cfg) => (cfg as CoreConfig).channels?.matrix?.dm?.policy ?? "pairing",
|
getCurrent: (cfg) => (cfg as CoreConfig).channels?.matrix?.dm?.policy ?? "pairing",
|
||||||
setPolicy: (cfg, policy) => setMatrixDmPolicy(cfg as CoreConfig, policy),
|
setPolicy: (cfg, policy) => setMatrixDmPolicy(cfg as CoreConfig, policy),
|
||||||
|
promptAllowFrom: promptMatrixAllowFrom,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { resolveMSTeamsCredentials } from "./token.js";
|
|||||||
import {
|
import {
|
||||||
parseMSTeamsTeamEntry,
|
parseMSTeamsTeamEntry,
|
||||||
resolveMSTeamsChannelAllowlist,
|
resolveMSTeamsChannelAllowlist,
|
||||||
|
resolveMSTeamsUserAllowlist,
|
||||||
} from "./resolve-allowlist.js";
|
} from "./resolve-allowlist.js";
|
||||||
|
|
||||||
const channel = "msteams" as const;
|
const channel = "msteams" as const;
|
||||||
@@ -38,6 +39,97 @@ function setMSTeamsDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setMSTeamsAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig {
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
msteams: {
|
||||||
|
...cfg.channels?.msteams,
|
||||||
|
allowFrom,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAllowFromInput(raw: string): string[] {
|
||||||
|
return raw
|
||||||
|
.split(/[\n,;]+/g)
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeGuid(value: string): boolean {
|
||||||
|
return /^[0-9a-fA-F-]{16,}$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptMSTeamsAllowFrom(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
}): Promise<ClawdbotConfig> {
|
||||||
|
const existing = params.cfg.channels?.msteams?.allowFrom ?? [];
|
||||||
|
await params.prompter.note(
|
||||||
|
[
|
||||||
|
"Allowlist MS Teams DMs by display name, UPN/email, or user id.",
|
||||||
|
"We resolve names to user IDs via Microsoft Graph when credentials allow.",
|
||||||
|
"Examples:",
|
||||||
|
"- alex@example.com",
|
||||||
|
"- Alex Johnson",
|
||||||
|
"- 00000000-0000-0000-0000-000000000000",
|
||||||
|
].join("\n"),
|
||||||
|
"MS Teams allowlist",
|
||||||
|
);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const entry = await params.prompter.text({
|
||||||
|
message: "MS Teams allowFrom (usernames or ids)",
|
||||||
|
placeholder: "alex@example.com, Alex Johnson",
|
||||||
|
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||||
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||||
|
});
|
||||||
|
const parts = parseAllowFromInput(String(entry));
|
||||||
|
if (parts.length === 0) {
|
||||||
|
await params.prompter.note("Enter at least one user.", "MS Teams allowlist");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = await resolveMSTeamsUserAllowlist({
|
||||||
|
cfg: params.cfg,
|
||||||
|
entries: parts,
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
|
const ids = parts.filter((part) => looksLikeGuid(part));
|
||||||
|
if (ids.length !== parts.length) {
|
||||||
|
await params.prompter.note(
|
||||||
|
"Graph lookup unavailable. Use user IDs only.",
|
||||||
|
"MS Teams allowlist",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const unique = [
|
||||||
|
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]),
|
||||||
|
];
|
||||||
|
return setMSTeamsAllowFrom(params.cfg, unique);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unresolved = resolved.filter((item) => !item.resolved || !item.id);
|
||||||
|
if (unresolved.length > 0) {
|
||||||
|
await params.prompter.note(
|
||||||
|
`Could not resolve: ${unresolved.map((item) => item.input).join(", ")}`,
|
||||||
|
"MS Teams allowlist",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = resolved.map((item) => item.id as string);
|
||||||
|
const unique = [
|
||||||
|
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]),
|
||||||
|
];
|
||||||
|
return setMSTeamsAllowFrom(params.cfg, unique);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function noteMSTeamsCredentialHelp(prompter: WizardPrompter): Promise<void> {
|
async function noteMSTeamsCredentialHelp(prompter: WizardPrompter): Promise<void> {
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
[
|
[
|
||||||
@@ -106,6 +198,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|||||||
allowFromKey: "channels.msteams.allowFrom",
|
allowFromKey: "channels.msteams.allowFrom",
|
||||||
getCurrent: (cfg) => cfg.channels?.msteams?.dmPolicy ?? "pairing",
|
getCurrent: (cfg) => cfg.channels?.msteams?.dmPolicy ?? "pairing",
|
||||||
setPolicy: (cfg, policy) => setMSTeamsDmPolicy(cfg, policy),
|
setPolicy: (cfg, policy) => setMSTeamsDmPolicy(cfg, policy),
|
||||||
|
promptAllowFrom: promptMSTeamsAllowFrom,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
|
export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||||
|
|||||||
@@ -207,6 +207,17 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|||||||
allowFromKey: "channels.zalo.allowFrom",
|
allowFromKey: "channels.zalo.allowFrom",
|
||||||
getCurrent: (cfg) => (cfg.channels?.zalo?.dmPolicy ?? "pairing") as "pairing",
|
getCurrent: (cfg) => (cfg.channels?.zalo?.dmPolicy ?? "pairing") as "pairing",
|
||||||
setPolicy: (cfg, policy) => setZaloDmPolicy(cfg as ClawdbotConfig, policy),
|
setPolicy: (cfg, policy) => setZaloDmPolicy(cfg as ClawdbotConfig, policy),
|
||||||
|
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
||||||
|
const id =
|
||||||
|
accountId && normalizeAccountId(accountId)
|
||||||
|
? normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID
|
||||||
|
: resolveDefaultZaloAccountId(cfg as ClawdbotConfig);
|
||||||
|
return promptZaloAllowFrom({
|
||||||
|
cfg: cfg as ClawdbotConfig,
|
||||||
|
prompter,
|
||||||
|
accountId: id,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
checkZcaAuthenticated,
|
checkZcaAuthenticated,
|
||||||
} from "./accounts.js";
|
} from "./accounts.js";
|
||||||
import { runZca, runZcaInteractive, checkZcaInstalled, parseJsonOutput } from "./zca.js";
|
import { runZca, runZcaInteractive, checkZcaInstalled, parseJsonOutput } from "./zca.js";
|
||||||
import type { ZcaGroup } from "./types.js";
|
import type { ZcaFriend, ZcaGroup } from "./types.js";
|
||||||
|
|
||||||
const channel = "zalouser" as const;
|
const channel = "zalouser" as const;
|
||||||
|
|
||||||
@@ -67,25 +67,73 @@ async function promptZalouserAllowFrom(params: {
|
|||||||
const { cfg, prompter, accountId } = params;
|
const { cfg, prompter, accountId } = params;
|
||||||
const resolved = resolveZalouserAccountSync({ cfg, accountId });
|
const resolved = resolveZalouserAccountSync({ cfg, accountId });
|
||||||
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
||||||
const entry = await prompter.text({
|
const parseInput = (raw: string) =>
|
||||||
message: "Zalouser allowFrom (user id)",
|
raw
|
||||||
placeholder: "123456789",
|
.split(/[\n,;]+/g)
|
||||||
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
.map((entry) => entry.trim())
|
||||||
validate: (value) => {
|
.filter(Boolean);
|
||||||
const raw = String(value ?? "").trim();
|
|
||||||
if (!raw) return "Required";
|
const resolveUserId = async (input: string): Promise<string | null> => {
|
||||||
if (!/^\d+$/.test(raw)) return "Use a numeric Zalo user id";
|
const trimmed = input.trim();
|
||||||
return undefined;
|
if (!trimmed) return null;
|
||||||
},
|
if (/^\d+$/.test(trimmed)) return trimmed;
|
||||||
});
|
const ok = await checkZcaInstalled();
|
||||||
const normalized = String(entry).trim();
|
if (!ok) return null;
|
||||||
const merged = [
|
const result = await runZca(["friend", "find", trimmed], {
|
||||||
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
profile: resolved.profile,
|
||||||
normalized,
|
timeout: 15000,
|
||||||
];
|
});
|
||||||
const unique = [...new Set(merged)];
|
if (!result.ok) return null;
|
||||||
|
const parsed = parseJsonOutput<ZcaFriend[]>(result.stdout);
|
||||||
|
const rows = Array.isArray(parsed) ? parsed : [];
|
||||||
|
const match = rows[0];
|
||||||
|
if (!match?.userId) return null;
|
||||||
|
if (rows.length > 1) {
|
||||||
|
await prompter.note(
|
||||||
|
`Multiple matches for "${trimmed}", using ${match.displayName ?? match.userId}.`,
|
||||||
|
"Zalo Personal allowlist",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return String(match.userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const entry = await prompter.text({
|
||||||
|
message: "Zalouser allowFrom (username or user id)",
|
||||||
|
placeholder: "Alice, 123456789",
|
||||||
|
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
||||||
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||||
|
});
|
||||||
|
const parts = parseInput(String(entry));
|
||||||
|
const results = await Promise.all(parts.map((part) => resolveUserId(part)));
|
||||||
|
const unresolved = parts.filter((_, idx) => !results[idx]);
|
||||||
|
if (unresolved.length > 0) {
|
||||||
|
await prompter.note(
|
||||||
|
`Could not resolve: ${unresolved.join(", ")}. Use numeric user ids or ensure zca is available.`,
|
||||||
|
"Zalo Personal allowlist",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const merged = [
|
||||||
|
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
||||||
|
...(results.filter(Boolean) as string[]),
|
||||||
|
];
|
||||||
|
const unique = [...new Set(merged)];
|
||||||
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
zalouser: {
|
||||||
|
...cfg.channels?.zalouser,
|
||||||
|
enabled: true,
|
||||||
|
dmPolicy: "allowlist",
|
||||||
|
allowFrom: unique,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
}
|
||||||
|
|
||||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
||||||
return {
|
return {
|
||||||
...cfg,
|
...cfg,
|
||||||
channels: {
|
channels: {
|
||||||
@@ -93,32 +141,19 @@ async function promptZalouserAllowFrom(params: {
|
|||||||
zalouser: {
|
zalouser: {
|
||||||
...cfg.channels?.zalouser,
|
...cfg.channels?.zalouser,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
dmPolicy: "allowlist",
|
accounts: {
|
||||||
allowFrom: unique,
|
...(cfg.channels?.zalouser?.accounts ?? {}),
|
||||||
|
[accountId]: {
|
||||||
|
...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}),
|
||||||
|
enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true,
|
||||||
|
dmPolicy: "allowlist",
|
||||||
|
allowFrom: unique,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as ClawdbotConfig;
|
} as ClawdbotConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
...cfg,
|
|
||||||
channels: {
|
|
||||||
...cfg.channels,
|
|
||||||
zalouser: {
|
|
||||||
...cfg.channels?.zalouser,
|
|
||||||
enabled: true,
|
|
||||||
accounts: {
|
|
||||||
...(cfg.channels?.zalouser?.accounts ?? {}),
|
|
||||||
[accountId]: {
|
|
||||||
...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}),
|
|
||||||
enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true,
|
|
||||||
dmPolicy: "allowlist",
|
|
||||||
allowFrom: unique,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as ClawdbotConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setZalouserGroupPolicy(
|
function setZalouserGroupPolicy(
|
||||||
@@ -237,6 +272,17 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|||||||
allowFromKey: "channels.zalouser.allowFrom",
|
allowFromKey: "channels.zalouser.allowFrom",
|
||||||
getCurrent: (cfg) => ((cfg as ClawdbotConfig).channels?.zalouser?.dmPolicy ?? "pairing") as "pairing",
|
getCurrent: (cfg) => ((cfg as ClawdbotConfig).channels?.zalouser?.dmPolicy ?? "pairing") as "pairing",
|
||||||
setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg as ClawdbotConfig, policy),
|
setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg as ClawdbotConfig, policy),
|
||||||
|
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
||||||
|
const id =
|
||||||
|
accountId && normalizeAccountId(accountId)
|
||||||
|
? normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID
|
||||||
|
: resolveDefaultZalouserAccountId(cfg as ClawdbotConfig);
|
||||||
|
return promptZalouserAllowFrom({
|
||||||
|
cfg: cfg as ClawdbotConfig,
|
||||||
|
prompter,
|
||||||
|
accountId: id,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||||
|
|||||||
Reference in New Issue
Block a user