Telegram: move group policy behind plugin boundary

This commit is contained in:
Gustavo Madeira Santana 2026-03-18 03:32:26 +00:00
parent a34944c918
commit 7ba8dd112f
No known key found for this signature in database
9 changed files with 178 additions and 118 deletions

View File

@ -3,6 +3,7 @@ export * from "./src/accounts.js";
export * from "./src/allow-from.js";
export * from "./src/api-fetch.js";
export * from "./src/exec-approvals.js";
export * from "./src/group-policy.js";
export * from "./src/inline-buttons.js";
export * from "./src/model-buttons.js";
export * from "./src/normalize.js";

View File

@ -154,6 +154,42 @@ afterEach(() => {
vi.restoreAllMocks();
});
describe("telegramPlugin groups", () => {
it("uses plugin-owned group policy resolvers", () => {
const cfg = {
channels: {
telegram: {
botToken: "telegram-test",
groups: {
"-1001": {
requireMention: true,
tools: { allow: ["message.send"] },
topics: {
"77": {
requireMention: false,
},
},
},
},
},
},
} as OpenClawConfig;
expect(
telegramPlugin.groups?.resolveRequireMention?.({
cfg,
groupId: "-1001:topic:77",
}),
).toBe(false);
expect(
telegramPlugin.groups?.resolveToolPolicy?.({
cfg,
groupId: "-1001:topic:77",
}),
).toEqual({ allow: ["message.send"] });
});
});
describe("telegramPlugin duplicate token guard", () => {
it("marks secondary account as not configured when token is shared", async () => {
const cfg = createCfg();

View File

@ -20,8 +20,6 @@ import {
PAIRING_APPROVED_MESSAGE,
projectCredentialSnapshotFields,
resolveConfiguredFromCredentialStatuses,
resolveTelegramGroupRequireMention,
resolveTelegramGroupToolPolicy,
type ChannelPlugin,
type ChannelMessageActionAdapter,
type OpenClawConfig,
@ -38,6 +36,10 @@ import {
isTelegramExecApprovalClientEnabled,
resolveTelegramExecApprovalTarget,
} from "./exec-approvals.js";
import {
resolveTelegramGroupRequireMention,
resolveTelegramGroupToolPolicy,
} from "./group-policy.js";
import { monitorTelegramProvider } from "./monitor.js";
import { looksLikeTelegramTargetId, normalizeTelegramMessagingTarget } from "./normalize.js";
import { sendTelegramPayloadMessages } from "./outbound-adapter.js";

View File

@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import {
resolveTelegramGroupRequireMention,
resolveTelegramGroupToolPolicy,
} from "./group-policy.js";
describe("telegram group policy", () => {
it("resolves topic-level requireMention and chat-level tools for topic ids", () => {
const telegramCfg = {
channels: {
telegram: {
botToken: "telegram-test",
groups: {
"-1001": {
requireMention: true,
tools: { allow: ["message.send"] },
topics: {
"77": {
requireMention: false,
},
},
},
"*": {
requireMention: true,
},
},
},
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any;
expect(
resolveTelegramGroupRequireMention({ cfg: telegramCfg, groupId: "-1001:topic:77" }),
).toBe(false);
expect(resolveTelegramGroupToolPolicy({ cfg: telegramCfg, groupId: "-1001:topic:77" })).toEqual(
{
allow: ["message.send"],
},
);
});
});

View File

@ -0,0 +1,91 @@
import {
resolveChannelGroupRequireMention,
resolveChannelGroupToolsPolicy,
type GroupToolPolicyConfig,
} from "openclaw/plugin-sdk/channel-policy";
import { type ChannelGroupContext } from "openclaw/plugin-sdk/channel-runtime";
function parseTelegramGroupId(value?: string | null) {
const raw = value?.trim() ?? "";
if (!raw) {
return { chatId: undefined, topicId: undefined };
}
const parts = raw.split(":").filter(Boolean);
if (
parts.length >= 3 &&
parts[1] === "topic" &&
/^-?\d+$/.test(parts[0]) &&
/^\d+$/.test(parts[2])
) {
return { chatId: parts[0], topicId: parts[2] };
}
if (parts.length >= 2 && /^-?\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
return { chatId: parts[0], topicId: parts[1] };
}
return { chatId: raw, topicId: undefined };
}
function resolveTelegramRequireMention(params: {
cfg: ChannelGroupContext["cfg"];
chatId?: string;
topicId?: string;
}): boolean | undefined {
const { cfg, chatId, topicId } = params;
if (!chatId) {
return undefined;
}
const groupConfig = cfg.channels?.telegram?.groups?.[chatId];
const groupDefault = cfg.channels?.telegram?.groups?.["*"];
const topicConfig = topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined;
const defaultTopicConfig =
topicId && groupDefault?.topics ? groupDefault.topics[topicId] : undefined;
if (typeof topicConfig?.requireMention === "boolean") {
return topicConfig.requireMention;
}
if (typeof defaultTopicConfig?.requireMention === "boolean") {
return defaultTopicConfig.requireMention;
}
if (typeof groupConfig?.requireMention === "boolean") {
return groupConfig.requireMention;
}
if (typeof groupDefault?.requireMention === "boolean") {
return groupDefault.requireMention;
}
return undefined;
}
export function resolveTelegramGroupRequireMention(
params: ChannelGroupContext,
): boolean | undefined {
const { chatId, topicId } = parseTelegramGroupId(params.groupId);
const requireMention = resolveTelegramRequireMention({
cfg: params.cfg,
chatId,
topicId,
});
if (typeof requireMention === "boolean") {
return requireMention;
}
return resolveChannelGroupRequireMention({
cfg: params.cfg,
channel: "telegram",
groupId: chatId ?? params.groupId,
accountId: params.accountId,
});
}
export function resolveTelegramGroupToolPolicy(
params: ChannelGroupContext,
): GroupToolPolicyConfig | undefined {
const { chatId } = parseTelegramGroupId(params.groupId);
return resolveChannelGroupToolsPolicy({
cfg: params.cfg,
channel: "telegram",
groupId: chatId ?? params.groupId,
accountId: params.accountId,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
}

View File

@ -4,45 +4,8 @@ import {
resolveBlueBubblesGroupToolPolicy,
resolveLineGroupRequireMention,
resolveLineGroupToolPolicy,
resolveTelegramGroupRequireMention,
resolveTelegramGroupToolPolicy,
} from "./group-mentions.js";
describe("group mentions (telegram)", () => {
it("resolves topic-level requireMention and chat-level tools for topic ids", () => {
const telegramCfg = {
channels: {
telegram: {
botToken: "telegram-test",
groups: {
"-1001": {
requireMention: true,
tools: { allow: ["message.send"] },
topics: {
"77": {
requireMention: false,
},
},
},
"*": {
requireMention: true,
},
},
},
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any;
expect(
resolveTelegramGroupRequireMention({ cfg: telegramCfg, groupId: "-1001:topic:77" }),
).toBe(false);
expect(resolveTelegramGroupToolPolicy({ cfg: telegramCfg, groupId: "-1001:topic:77" })).toEqual(
{
allow: ["message.send"],
},
);
});
});
describe("group mentions (bluebubbles)", () => {
it("uses generic channel group policy helpers", () => {
const blueBubblesCfg = {

View File

@ -1,4 +1,3 @@
import type { OpenClawConfig } from "../../config/config.js";
import {
resolveChannelGroupRequireMention,
resolveChannelGroupToolsPolicy,
@ -9,55 +8,6 @@ import type { ChannelGroupContext } from "./types.js";
type GroupMentionParams = ChannelGroupContext;
function parseTelegramGroupId(value?: string | null) {
const raw = value?.trim() ?? "";
if (!raw) {
return { chatId: undefined, topicId: undefined };
}
const parts = raw.split(":").filter(Boolean);
if (
parts.length >= 3 &&
parts[1] === "topic" &&
/^-?\d+$/.test(parts[0]) &&
/^\d+$/.test(parts[2])
) {
return { chatId: parts[0], topicId: parts[2] };
}
if (parts.length >= 2 && /^-?\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
return { chatId: parts[0], topicId: parts[1] };
}
return { chatId: raw, topicId: undefined };
}
function resolveTelegramRequireMention(params: {
cfg: OpenClawConfig;
chatId?: string;
topicId?: string;
}): boolean | undefined {
const { cfg, chatId, topicId } = params;
if (!chatId) {
return undefined;
}
const groupConfig = cfg.channels?.telegram?.groups?.[chatId];
const groupDefault = cfg.channels?.telegram?.groups?.["*"];
const topicConfig = topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined;
const defaultTopicConfig =
topicId && groupDefault?.topics ? groupDefault.topics[topicId] : undefined;
if (typeof topicConfig?.requireMention === "boolean") {
return topicConfig.requireMention;
}
if (typeof defaultTopicConfig?.requireMention === "boolean") {
return defaultTopicConfig.requireMention;
}
if (typeof groupConfig?.requireMention === "boolean") {
return groupConfig.requireMention;
}
if (typeof groupDefault?.requireMention === "boolean") {
return groupDefault.requireMention;
}
return undefined;
}
type ChannelGroupPolicyChannel =
| "telegram"
| "whatsapp"
@ -96,26 +46,6 @@ function resolveChannelToolPolicyForSender(
});
}
export function resolveTelegramGroupRequireMention(
params: GroupMentionParams,
): boolean | undefined {
const { chatId, topicId } = parseTelegramGroupId(params.groupId);
const requireMention = resolveTelegramRequireMention({
cfg: params.cfg,
chatId,
topicId,
});
if (typeof requireMention === "boolean") {
return requireMention;
}
return resolveChannelGroupRequireMention({
cfg: params.cfg,
channel: "telegram",
groupId: chatId ?? params.groupId,
accountId: params.accountId,
});
}
export function resolveWhatsAppGroupRequireMention(params: GroupMentionParams): boolean {
return resolveChannelRequireMention(params, "whatsapp");
}
@ -138,13 +68,6 @@ export function resolveBlueBubblesGroupRequireMention(params: GroupMentionParams
return resolveChannelRequireMention(params, "bluebubbles");
}
export function resolveTelegramGroupToolPolicy(
params: GroupMentionParams,
): GroupToolPolicyConfig | undefined {
const { chatId } = parseTelegramGroupId(params.groupId);
return resolveChannelToolPolicyForSender(params, "telegram", chatId ?? params.groupId);
}
export function resolveWhatsAppGroupToolPolicy(
params: GroupMentionParams,
): GroupToolPolicyConfig | undefined {

View File

@ -14,7 +14,11 @@ export {
collectOpenProviderGroupPolicyWarnings,
} from "../channels/plugins/group-policy-warnings.js";
export { buildAccountScopedDmSecurityPolicy } from "../channels/plugins/helpers.js";
export { resolveChannelGroupRequireMention, resolveToolsBySender } from "../config/group-policy.js";
export {
resolveChannelGroupRequireMention,
resolveChannelGroupToolsPolicy,
resolveToolsBySender,
} from "../config/group-policy.js";
export {
DM_GROUP_ACCESS_REASON,
readStoreAllowFromForDmPolicy,

View File

@ -57,7 +57,7 @@ export {
export {
resolveTelegramGroupRequireMention,
resolveTelegramGroupToolPolicy,
} from "../channels/plugins/group-mentions.js";
} from "../../extensions/telegram/src/group-policy.js";
export { TelegramConfigSchema } from "../config/zod-schema.providers-core.js";
export { buildTokenChannelStatusSummary } from "./status-helpers.js";