diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index 1c9aafff1ea..c3b61cbecb8 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -2,13 +2,20 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { telegramCommandTestPlugin } from "../../../extensions/telegram/test-support.js"; import type { ChannelPlugin } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; import { updateSessionStore, type SessionEntry } from "../../config/sessions.js"; +import { formatAllowFromLowercase } from "../../plugin-sdk/allow-from.js"; +import { buildDmGroupAccountAllowlistAdapter } from "../../plugin-sdk/allowlist-config-edit.js"; +import { createApproverRestrictedNativeApprovalAdapter } from "../../plugin-sdk/approval-runtime.js"; +import { createScopedChannelConfigAdapter } from "../../plugin-sdk/channel-config-helpers.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; import { loadBundledPluginPublicSurfaceSync } from "../../test-utils/bundled-plugin-public-surface.js"; -import { createTestRegistry } from "../../test-utils/channel-plugins.js"; +import { + createChannelTestPluginBase, + createTestRegistry, +} from "../../test-utils/channel-plugins.js"; import { typedCases } from "../../test-utils/typed-cases.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js"; import type { MsgContext } from "../templating.js"; @@ -157,6 +164,197 @@ const { createTaskRecord, resetTaskRegistryForTests } = let testWorkspaceDir = os.tmpdir(); +type TelegramTestAccountConfig = { + enabled?: boolean; + allowFrom?: Array; + groupAllowFrom?: Array; + dmPolicy?: string; + groupPolicy?: string; + configWrites?: boolean; + execApprovals?: { + enabled?: boolean; + approvers?: string[]; + target?: "dm" | "channel" | "both"; + }; +}; + +type TelegramTestSectionConfig = TelegramTestAccountConfig & { + accounts?: Record; +}; + +function listConfiguredTelegramAccountIds(cfg: OpenClawConfig): string[] { + const channel = cfg.channels?.telegram as TelegramTestSectionConfig | undefined; + const accountIds = Object.keys(channel?.accounts ?? {}); + if (accountIds.length > 0) { + return accountIds; + } + if (!channel) { + return []; + } + const { accounts: _accounts, ...base } = channel; + return Object.values(base).some((value) => value !== undefined) ? [DEFAULT_ACCOUNT_ID] : []; +} + +function resolveTelegramTestAccount( + cfg: OpenClawConfig, + accountId?: string | null, +): TelegramTestAccountConfig { + const resolvedAccountId = normalizeAccountId(accountId); + const channel = cfg.channels?.telegram as TelegramTestSectionConfig | undefined; + const scoped = channel?.accounts?.[resolvedAccountId]; + const base = resolvedAccountId === DEFAULT_ACCOUNT_ID ? channel : undefined; + return { + ...base, + ...scoped, + enabled: + typeof scoped?.enabled === "boolean" + ? scoped.enabled + : typeof channel?.enabled === "boolean" + ? channel.enabled + : true, + }; +} + +function normalizeTelegramAllowFromEntries(values: Array): string[] { + return formatAllowFromLowercase({ allowFrom: values }); +} + +function normalizeTelegramDirectApproverId(value: string | number): string | undefined { + const normalized = String(value).trim(); + if (!normalized || normalized.startsWith("-")) { + return undefined; + } + return normalized.replace(/^(?:tg|telegram):/i, ""); +} + +function getTelegramExecApprovalApprovers(params: { + cfg: OpenClawConfig; + accountId?: string | null; +}): string[] { + const account = resolveTelegramTestAccount(params.cfg, params.accountId); + const explicit = account.execApprovals?.approvers; + const allowFrom = account.allowFrom; + const source = Array.isArray(explicit) ? explicit : Array.isArray(allowFrom) ? allowFrom : []; + return source + .map((entry) => normalizeTelegramDirectApproverId(entry)) + .filter((entry): entry is string => Boolean(entry)); +} + +function isTelegramExecApprovalTargetRecipient(params: { + cfg: OpenClawConfig; + senderId?: string | null; + accountId?: string | null; +}): boolean { + const senderId = params.senderId?.trim(); + const execApprovals = params.cfg.approvals?.exec; + if ( + !senderId || + execApprovals?.enabled !== true || + (execApprovals.mode !== "targets" && execApprovals.mode !== "both") + ) { + return false; + } + const accountId = params.accountId ? normalizeAccountId(params.accountId) : undefined; + return (execApprovals.targets ?? []).some((target) => { + if (target.channel?.trim().toLowerCase() !== "telegram") { + return false; + } + if (accountId && target.accountId && normalizeAccountId(target.accountId) !== accountId) { + return false; + } + const to = target.to ? normalizeTelegramDirectApproverId(target.to) : undefined; + return Boolean(to && to === senderId); + }); +} + +function isTelegramExecApprovalAuthorizedSender(params: { + cfg: OpenClawConfig; + accountId?: string | null; + senderId?: string | null; +}): boolean { + const senderId = params.senderId?.trim(); + if (!senderId) { + return false; + } + return ( + getTelegramExecApprovalApprovers(params).includes(senderId) || + isTelegramExecApprovalTargetRecipient(params) + ); +} + +function isTelegramExecApprovalClientEnabled(params: { + cfg: OpenClawConfig; + accountId?: string | null; +}): boolean { + const config = resolveTelegramTestAccount(params.cfg, params.accountId).execApprovals; + return Boolean(config?.enabled && getTelegramExecApprovalApprovers(params).length > 0); +} + +function resolveTelegramExecApprovalTarget(params: { + cfg: OpenClawConfig; + accountId?: string | null; +}): "dm" | "channel" | "both" { + return resolveTelegramTestAccount(params.cfg, params.accountId).execApprovals?.target ?? "dm"; +} + +const telegramNativeApprovalAdapter = createApproverRestrictedNativeApprovalAdapter({ + channel: "telegram", + channelLabel: "Telegram", + listAccountIds: listConfiguredTelegramAccountIds, + hasApprovers: ({ cfg, accountId }) => + getTelegramExecApprovalApprovers({ cfg, accountId }).length > 0, + isExecAuthorizedSender: isTelegramExecApprovalAuthorizedSender, + isPluginAuthorizedSender: ({ cfg, accountId, senderId }) => { + const normalizedSenderId = senderId?.trim(); + return Boolean( + normalizedSenderId && + getTelegramExecApprovalApprovers({ cfg, accountId }).includes(normalizedSenderId), + ); + }, + isNativeDeliveryEnabled: isTelegramExecApprovalClientEnabled, + resolveNativeDeliveryMode: resolveTelegramExecApprovalTarget, + requireMatchingTurnSourceChannel: true, +}); + +const telegramCommandTestPlugin: ChannelPlugin = { + ...createChannelTestPluginBase({ + id: "telegram", + label: "Telegram", + docsPath: "/channels/telegram", + capabilities: { + chatTypes: ["direct", "group", "channel", "thread"], + reactions: true, + threads: true, + media: true, + polls: true, + nativeCommands: true, + blockStreaming: true, + }, + }), + config: createScopedChannelConfigAdapter({ + sectionKey: "telegram", + listAccountIds: listConfiguredTelegramAccountIds, + resolveAccount: (cfg, accountId) => resolveTelegramTestAccount(cfg, accountId), + defaultAccountId: () => DEFAULT_ACCOUNT_ID, + clearBaseFields: [], + resolveAllowFrom: (account) => account.allowFrom, + formatAllowFrom: normalizeTelegramAllowFromEntries, + }), + auth: telegramNativeApprovalAdapter.auth, + pairing: { + idLabel: "telegramUserId", + }, + allowlist: buildDmGroupAccountAllowlistAdapter({ + channelId: "telegram", + resolveAccount: ({ cfg, accountId }) => resolveTelegramTestAccount(cfg, accountId), + normalize: ({ values }) => normalizeTelegramAllowFromEntries(values), + resolveDmAllowFrom: (account) => account.allowFrom, + resolveGroupAllowFrom: (account) => account.groupAllowFrom, + resolveDmPolicy: (account) => account.dmPolicy, + resolveGroupPolicy: (account) => account.groupPolicy, + }), +}; + function setMinimalChannelPluginRegistryForTests(): void { setActivePluginRegistry( createTestRegistry([