From 53efb6747d3e68e157b590d90283f062955cc997 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Thu, 7 May 2026 08:55:36 +0530 Subject: [PATCH] refactor(telegram): centralize access authorization --- extensions/telegram/src/access-groups.ts | 40 ++++++- extensions/telegram/src/bot-access.ts | 16 +++ .../telegram/src/bot-handlers.runtime.ts | 90 +++++----------- .../telegram/src/bot-message-context.ts | 51 ++++----- .../telegram/src/bot-native-commands.ts | 101 +++++++----------- extensions/telegram/src/bot/helpers.ts | 40 +++++++ 6 files changed, 180 insertions(+), 158 deletions(-) diff --git a/extensions/telegram/src/access-groups.ts b/extensions/telegram/src/access-groups.ts index 870ad1a81fd..3bb26c925a8 100644 --- a/extensions/telegram/src/access-groups.ts +++ b/extensions/telegram/src/access-groups.ts @@ -1,9 +1,14 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import type { DmPolicy, OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { expandAllowFromWithAccessGroups, parseAccessGroupAllowFromEntry, } from "openclaw/plugin-sdk/security-runtime"; -import { isSenderAllowed, normalizeAllowFrom } from "./bot-access.js"; +import { + isSenderAllowed, + normalizeAllowFrom, + normalizeDmAllowFromWithStore, + type NormalizedAllowFrom, +} from "./bot-access.js"; export async function expandTelegramAllowFromWithAccessGroups(params: { cfg?: OpenClawConfig; @@ -34,3 +39,34 @@ export async function expandTelegramAllowFromWithAccessGroups(params: { ? expanded.filter((entry) => parseAccessGroupAllowFromEntry(entry) == null) : expanded; } + +export async function resolveTelegramDmAllow(params: { + cfg?: OpenClawConfig; + allowFrom?: Array; + groupAllowOverride?: Array; + storeAllowFrom?: string[]; + dmPolicy?: DmPolicy; + accountId?: string; + senderId?: string; +}): Promise<{ + allowFrom?: Array; + expandedAllowFrom: string[]; + effectiveAllow: NormalizedAllowFrom; +}> { + const allowFrom = params.groupAllowOverride ?? params.allowFrom; + const expandedAllowFrom = await expandTelegramAllowFromWithAccessGroups({ + cfg: params.cfg, + allowFrom, + accountId: params.accountId, + senderId: params.senderId, + }); + return { + allowFrom, + expandedAllowFrom, + effectiveAllow: normalizeDmAllowFromWithStore({ + allowFrom: expandedAllowFrom, + storeAllowFrom: params.storeAllowFrom, + dmPolicy: params.dmPolicy, + }), + }; +} diff --git a/extensions/telegram/src/bot-access.ts b/extensions/telegram/src/bot-access.ts index c00236f054b..99ca43502cd 100644 --- a/extensions/telegram/src/bot-access.ts +++ b/extensions/telegram/src/bot-access.ts @@ -4,6 +4,11 @@ import { mergeDmAllowFromSources, type AllowlistMatch, } from "openclaw/plugin-sdk/allow-from"; +import type { + DmPolicy, + TelegramDirectConfig, + TelegramGroupConfig, +} from "openclaw/plugin-sdk/config-types"; import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; @@ -67,6 +72,17 @@ export const normalizeDmAllowFromWithStore = (params: { dmPolicy?: string; }): NormalizedAllowFrom => normalizeAllowFrom(mergeDmAllowFromSources(params)); +export function resolveTelegramEffectiveDmPolicy(params: { + isGroup: boolean; + groupConfig?: TelegramDirectConfig | TelegramGroupConfig; + dmPolicy?: DmPolicy; +}): DmPolicy { + if (!params.isGroup && params.groupConfig && "dmPolicy" in params.groupConfig) { + return params.groupConfig.dmPolicy ?? params.dmPolicy ?? "pairing"; + } + return params.dmPolicy ?? "pairing"; +} + export const isSenderAllowed = (params: { allow: NormalizedAllowFrom; senderId?: string; diff --git a/extensions/telegram/src/bot-handlers.runtime.ts b/extensions/telegram/src/bot-handlers.runtime.ts index b660d8755f2..1e692936389 100644 --- a/extensions/telegram/src/bot-handlers.runtime.ts +++ b/extensions/telegram/src/bot-handlers.runtime.ts @@ -7,10 +7,7 @@ import { resolveInboundDebounceMs, } from "openclaw/plugin-sdk/channel-inbound-debounce"; import { resolveStoredModelOverride } from "openclaw/plugin-sdk/command-auth"; -import { - resolveCommandAuthorization, - resolveCommandAuthorizedFromAuthorizers, -} from "openclaw/plugin-sdk/command-auth-native"; +import { resolveCommandAuthorizedFromAuthorizers } from "openclaw/plugin-sdk/command-auth-native"; import { buildCommandsMessagePaginated } from "openclaw/plugin-sdk/command-status"; import { replaceConfigFile } from "openclaw/plugin-sdk/config-mutation"; import type { DmPolicy, OpenClawConfig } from "openclaw/plugin-sdk/config-types"; @@ -34,12 +31,16 @@ import { resolveSessionStoreEntry, updateSessionStore, } from "openclaw/plugin-sdk/session-store-runtime"; -import { expandTelegramAllowFromWithAccessGroups } from "./access-groups.js"; +import { + expandTelegramAllowFromWithAccessGroups, + resolveTelegramDmAllow, +} from "./access-groups.js"; import { resolveTelegramAccount, resolveTelegramMediaRuntimeOptions } from "./accounts.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; import { isSenderAllowed, normalizeDmAllowFromWithStore, + resolveTelegramEffectiveDmPolicy, type NormalizedAllowFrom, } from "./bot-access.js"; import { @@ -71,9 +72,10 @@ import { import { resolveMedia } from "./bot/delivery.resolve-media.js"; import { getTelegramTextParts, - buildTelegramGroupFrom, buildTelegramGroupPeerId, buildTelegramParentPeer, + isTelegramCommandsAllowFromConfigured, + resolveTelegramCommandAuthorization, resolveTelegramForumFlag, resolveTelegramForumThreadId, resolveTelegramGroupAllowFromContext, @@ -728,13 +730,11 @@ export const registerTelegramHandlers = ({ readChannelAllowFromStore: telegramDeps.readChannelAllowFromStore, resolveTelegramGroupConfig, })); - // Use direct config dmPolicy override if available for DMs - const effectiveDmPolicy = - !params.isGroup && - groupAllowContext.groupConfig && - "dmPolicy" in groupAllowContext.groupConfig - ? (groupAllowContext.groupConfig.dmPolicy ?? telegramCfg.dmPolicy ?? "pairing") - : (telegramCfg.dmPolicy ?? "pairing"); + const effectiveDmPolicy = resolveTelegramEffectiveDmPolicy({ + isGroup: params.isGroup, + groupConfig: groupAllowContext.groupConfig, + dmPolicy: telegramCfg.dmPolicy, + }); return { dmPolicy: effectiveDmPolicy, ...groupAllowContext }; }; @@ -831,27 +831,15 @@ export const registerTelegramHandlers = ({ const { chatId, isGroup, senderId, senderUsername, context, cfg } = params; const useAccessGroups = cfg.commands?.useAccessGroups !== false; const dmAllowFrom = context.groupAllowOverride ?? allowFrom; - const commandsAllowFrom = cfg.commands?.allowFrom; - const commandsAllowFromConfigured = - commandsAllowFrom != null && - typeof commandsAllowFrom === "object" && - (Array.isArray(commandsAllowFrom.telegram) || Array.isArray(commandsAllowFrom["*"])); - if (commandsAllowFromConfigured) { - return resolveCommandAuthorization({ - ctx: { - Provider: "telegram", - Surface: "telegram", - OriginatingChannel: "telegram", - AccountId: accountId, - ChatType: isGroup ? "group" : "direct", - From: isGroup - ? buildTelegramGroupFrom(chatId, context.resolvedThreadId) - : `telegram:${chatId}`, - SenderId: senderId || undefined, - SenderUsername: senderUsername || undefined, - }, + if (isTelegramCommandsAllowFromConfigured(cfg)) { + return resolveTelegramCommandAuthorization({ cfg, - commandAuthorized: false, + accountId, + chatId, + isGroup, + resolvedThreadId: context.resolvedThreadId, + senderId, + senderUsername, }).isAuthorizedSender; } @@ -896,7 +884,6 @@ export const registerTelegramHandlers = ({ }); }; - // Handle emoji reactions to messages. bot.on("message_reaction", async (ctx) => { try { const reaction = ctx.messageReaction; @@ -915,7 +902,6 @@ export const registerTelegramHandlers = ({ const isGroup = reaction.chat.type === "group" || reaction.chat.type === "supergroup"; const isForum = reaction.chat.is_forum === true; - // Resolve reaction notification mode (default: "own"). const reactionMode = telegramCfg.reactionNotifications ?? "own"; if (reactionMode === "off") { return; @@ -963,7 +949,6 @@ export const registerTelegramHandlers = ({ } } - // Detect added reactions. const oldEmojis = new Set( reaction.old_reaction .filter((r): r is ReactionTypeEmoji => r.type === "emoji") @@ -977,7 +962,6 @@ export const registerTelegramHandlers = ({ return; } - // Build sender label. const senderName = user ? [user.first_name, user.last_name].filter(Boolean).join(" ").trim() || user.username : undefined; @@ -1001,7 +985,6 @@ export const registerTelegramHandlers = ({ : undefined; const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId); const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId }); - // Fresh config for bindings lookup; other routing inputs are payload-derived. const route = resolveAgentRoute({ cfg: telegramDeps.getRuntimeConfig(), channel: "telegram", @@ -1011,7 +994,6 @@ export const registerTelegramHandlers = ({ }); const sessionKey = route.sessionKey; - // Enqueue system event for each added reaction. for (const r of addedReactions) { const emoji = r.emoji; const text = `Telegram reaction added: ${emoji} by ${senderLabel} on msg ${messageId}`; @@ -1047,14 +1029,11 @@ export const registerTelegramHandlers = ({ oversizeLogMessage, } = params; - // Text fragment handling - Telegram splits long pastes into multiple inbound messages (~4096 chars). - // We buffer “near-limit” messages and append immediately-following parts. const text = typeof msg.text === "string" ? msg.text : undefined; const isCommandLike = (text ?? "").trim().startsWith("/"); if (text && !isCommandLike) { const nowMs = Date.now(); const senderId = msg.from?.id != null ? String(msg.from.id) : "unknown"; - // Use resolvedThreadId for forum groups, dmThreadId for DM topics const threadId = resolvedThreadId ?? dmThreadId; const key = `text:${chatId}:${threadId ?? "main"}:${senderId}`; const existing = textFragmentBuffer.get(key); @@ -1087,7 +1066,6 @@ export const registerTelegramHandlers = ({ } } - // Not appendable (or limits exceeded): flush buffered entry first, then continue normally. clearTimeout(existing.timer); textFragmentBuffer.delete(key); textFragmentProcessing = textFragmentProcessing @@ -1111,7 +1089,6 @@ export const registerTelegramHandlers = ({ } } - // Media group handling - buffer multi-image messages const mediaGroupId = msg.media_group_id; if (mediaGroupId) { const existing = mediaGroupBuffer.get(mediaGroupId); @@ -1186,8 +1163,6 @@ export const registerTelegramHandlers = ({ return; } - // Skip sticker-only messages where the sticker was skipped (animated/video) - // These have no media and no text content to process. const hasText = Boolean(getTelegramTextParts(msg).text.trim()); if (msg.sticker && !media && !hasText) { logVerbose("telegram: skipping sticker-only message (unsupported sticker type)"); @@ -1240,7 +1215,6 @@ export const registerTelegramHandlers = ({ typeof (ctx as { answerCallbackQuery?: unknown }).answerCallbackQuery === "function" ? () => ctx.answerCallbackQuery() : () => bot.api.answerCallbackQuery(callback.id); - // Answer immediately to prevent Telegram from retrying while we process await withTelegramApiErrorLogging({ operation: "answerCallbackQuery", runtime, @@ -1573,7 +1547,6 @@ export const registerTelegramHandlers = ({ return; } - // Model selection callback handler (mdl_prov, mdl_list_*, mdl_sel_*, mdl_back) const modelCallback = parseModelCallbackData(data); if (modelCallback) { if ( @@ -1660,7 +1633,6 @@ export const registerTelegramHandlers = ({ const { provider, page } = modelCallback; const modelSet = byProvider.get(provider); if (!modelSet || modelSet.size === 0) { - // Provider not found or no models - show providers list const providerInfos: ProviderInfo[] = providers.map((p) => ({ id: p, count: byProvider.get(p)?.size ?? 0, @@ -1681,7 +1653,6 @@ export const registerTelegramHandlers = ({ const totalPages = calculateTotalPages(models.length, pageSize); const safePage = Math.max(1, Math.min(page, totalPages)); - // Resolve current model from session (prefer overrides) const currentModel = sessionState.model; const buttons = buildModelsKeyboard({ @@ -1744,7 +1715,6 @@ export const registerTelegramHandlers = ({ return; } - // Directly set model override in session try { // Use the fresh runtimeCfg (loaded at callback entry) so store path // and default-model resolution stay consistent with the next @@ -1782,7 +1752,6 @@ export const registerTelegramHandlers = ({ throw new TelegramRetryableCallbackError(err); } - // Update message to show success with visual feedback const escapeHtml = (text: string) => text.replace(/&/g, "&").replace(//g, ">"); const actionText = isDefaultSelection @@ -1832,7 +1801,6 @@ export const registerTelegramHandlers = ({ } }); - // Handle group migration to supergroup (chat ID changes) bot.on("message:migrate_to_chat_id", async (ctx) => { try { const msg = ctx.message; @@ -1854,7 +1822,6 @@ export const registerTelegramHandlers = ({ return; } - // Check if old chat ID has config and migrate it const currentConfig = telegramDeps.getRuntimeConfig(); const migration = migrateTelegramGroupConfig({ cfg: currentConfig, @@ -1927,16 +1894,12 @@ export const registerTelegramHandlers = ({ effectiveGroupAllow, hasGroupAllowOverride, } = eventAuthContext; - // For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom - const dmAllowFrom = groupAllowOverride ?? allowFrom; - const expandedDmAllowFrom = await expandTelegramAllowFromWithAccessGroups({ + const dmAllow = await resolveTelegramDmAllow({ cfg, - allowFrom: dmAllowFrom, + groupAllowOverride, + allowFrom, accountId, senderId: event.senderId, - }); - const effectiveDmAllow = normalizeDmAllowFromWithStore({ - allowFrom: expandedDmAllowFrom, storeAllowFrom, dmPolicy, }); @@ -1969,7 +1932,7 @@ export const registerTelegramHandlers = ({ dmPolicy, msg: event.msg, chatId: event.chatId, - effectiveDmAllow, + effectiveDmAllow: dmAllow.effectiveAllow, accountId, bot, logger, @@ -2031,9 +1994,6 @@ export const registerTelegramHandlers = ({ }); }); - // Handle channel posts — enables bot-to-bot communication via Telegram channels. - // Telegram bots cannot see other bot messages in groups, but CAN in channels. - // This handler normalizes channel_post updates into the standard message pipeline. bot.on("channel_post", async (ctx) => { const post = ctx.channelPost; if (!post) { diff --git a/extensions/telegram/src/bot-message-context.ts b/extensions/telegram/src/bot-message-context.ts index 93226af5e63..cff81c65481 100644 --- a/extensions/telegram/src/bot-message-context.ts +++ b/extensions/telegram/src/bot-message-context.ts @@ -8,10 +8,17 @@ import type { TelegramDirectConfig, TelegramGroupConfig } from "openclaw/plugin- import { deriveLastRoutePolicy } from "openclaw/plugin-sdk/routing"; import { normalizeAccountId, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; -import { expandTelegramAllowFromWithAccessGroups } from "./access-groups.js"; +import { + expandTelegramAllowFromWithAccessGroups, + resolveTelegramDmAllow, +} from "./access-groups.js"; import { mergeTelegramAccountConfig, resolveDefaultTelegramAccountId } from "./accounts.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; -import { firstDefined, normalizeAllowFrom, normalizeDmAllowFromWithStore } from "./bot-access.js"; +import { + firstDefined, + normalizeAllowFrom, + resolveTelegramEffectiveDmPolicy, +} from "./bot-access.js"; import { resolveTelegramInboundBody } from "./bot-message-context.body.js"; import { buildTelegramInboundContextPayload, @@ -217,12 +224,11 @@ export const buildTelegramMessageContext = async ({ const telegramGroupConfig = isGroup ? (groupConfig as TelegramGroupConfig | undefined) : undefined; - // Use direct config dmPolicy override if available for DMs - const effectiveDmPolicy = - !isGroup && groupConfig && "dmPolicy" in groupConfig - ? (groupConfig.dmPolicy ?? dmPolicy) - : dmPolicy; - // Fresh config for bindings lookup; other routing inputs are payload-derived. + const effectiveDmPolicy = resolveTelegramEffectiveDmPolicy({ + isGroup, + groupConfig, + dmPolicy, + }); const freshCfg = loadFreshConfig?.() ?? (runtime?.getRuntimeConfig ?? (await loadTelegramMessageContextRuntime()).getRuntimeConfig)(); @@ -255,16 +261,16 @@ export const buildTelegramMessageContext = async ({ }); return null; } - // Calculate groupAllowOverride first - it's needed for both DM and group allowlist checks const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); - // For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom - const dmAllowFrom = groupAllowOverride ?? allowFrom; - const [expandedDmAllowFrom, expandedGroupAllowFrom] = await Promise.all([ - expandTelegramAllowFromWithAccessGroups({ + const [dmAllow, expandedGroupAllowFrom] = await Promise.all([ + resolveTelegramDmAllow({ cfg: freshCfg, - allowFrom: dmAllowFrom, + groupAllowOverride, + allowFrom, accountId: account.accountId, senderId, + storeAllowFrom, + dmPolicy: effectiveDmPolicy, }), expandTelegramAllowFromWithAccessGroups({ cfg: freshCfg, @@ -273,12 +279,6 @@ export const buildTelegramMessageContext = async ({ senderId, }), ]); - const effectiveDmAllow = normalizeDmAllowFromWithStore({ - allowFrom: expandedDmAllowFrom, - storeAllowFrom, - dmPolicy: effectiveDmPolicy, - }); - // Group sender checks are explicit and must not inherit DM pairing-store entries. const effectiveGroupAllow = normalizeAllowFrom(expandedGroupAllowFrom); const hasGroupAllowOverride = groupAllowOverride !== undefined; const senderUsername = msg.from?.username ?? ""; @@ -353,7 +353,7 @@ export const buildTelegramMessageContext = async ({ dmPolicy: effectiveDmPolicy, msg, chatId, - effectiveDmAllow, + effectiveDmAllow: dmAllow.effectiveAllow, accountId: account.accountId, bot, logger, @@ -417,7 +417,6 @@ export const buildTelegramMessageContext = async ({ mainSessionKey: route.mainSessionKey, }), }; - // Compute requireMention after access checks and final route selection. const activationOverride = resolveGroupActivation({ chatId, messageThreadId: resolvedThreadId, @@ -456,7 +455,7 @@ export const buildTelegramMessageContext = async ({ routeAgentId: route.agentId, sessionKey, effectiveGroupAllow, - effectiveDmAllow, + effectiveDmAllow: dmAllow.effectiveAllow, groupConfig, topicConfig, requireMention, @@ -473,7 +472,6 @@ export const buildTelegramMessageContext = async ({ return null; } - // ACK reactions const ackReaction = resolveAckReaction(cfg, route.agentId, { channel: "telegram", accountId: account.accountId, @@ -494,7 +492,6 @@ export const buildTelegramMessageContext = async ({ shouldBypassMention: bodyResult.shouldBypassMention, }), ); - // Status Reactions controller (lifecycle reactions) const statusReactionsConfig = cfg.messages?.statusReactions; const statusReactionsEnabled = statusReactionsConfig?.enabled === true && Boolean(reactionApi) && shouldSendAckReaction; @@ -546,7 +543,6 @@ export const buildTelegramMessageContext = async ({ ]); } }, - // Telegram replaces atomically — no removeReaction needed }, initialEmoji: ackReaction, emojis: resolvedStatusReactionEmojis ?? undefined, @@ -557,7 +553,6 @@ export const buildTelegramMessageContext = async ({ }) : null; - // When status reactions are enabled, setQueued() replaces the simple ack reaction const ackReactionPromise: Promise | null = statusReactionController ? shouldSendAckReaction ? Promise.resolve(statusReactionController.setQueued()).then( @@ -608,7 +603,7 @@ export const buildTelegramMessageContext = async ({ : {}), locationData: bodyResult.locationData, options, - dmAllowFrom, + dmAllowFrom: dmAllow.allowFrom, effectiveGroupAllow, commandAuthorized: bodyResult.commandAuthorized, topicName, diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index 42d7a0c42c2..f35d4747a7e 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -9,7 +9,6 @@ import { } from "openclaw/plugin-sdk/agent-runtime"; import { resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-streaming"; import { - resolveCommandAuthorization, resolveCommandAuthorizedFromAuthorizers, resolveNativeCommandSessionTargets, } from "openclaw/plugin-sdk/command-auth-native"; @@ -51,10 +50,10 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "openclaw/plugin-sdk/text-runtime"; -import { expandTelegramAllowFromWithAccessGroups } from "./access-groups.js"; +import { resolveTelegramDmAllow } from "./access-groups.js"; import { resolveTelegramAccount } from "./accounts.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; -import { isSenderAllowed, normalizeDmAllowFromWithStore } from "./bot-access.js"; +import { isSenderAllowed, resolveTelegramEffectiveDmPolicy } from "./bot-access.js"; import type { TelegramBotDeps } from "./bot-deps.js"; import type { TelegramMediaRef } from "./bot-message-context.js"; import type { TelegramMessageContextOptions } from "./bot-message-context.types.js"; @@ -75,6 +74,8 @@ import { buildSenderName, buildTelegramGroupFrom, extractTelegramForumFlag, + isTelegramCommandsAllowFromConfigured, + resolveTelegramCommandAuthorization, resolveTelegramForumFlag, resolveTelegramGroupAllowFromContext, resolveTelegramThreadSpec, @@ -343,9 +344,7 @@ async function cleanupTelegramProgressPlaceholder(params: { runtime: params.runtime, fn: () => params.bot.api.deleteMessage(params.chatId, progressMessageId), }); - } catch { - // Best-effort cleanup before fallback or suppression exits. - } + } catch {} } async function resolveTelegramNativeCommandThreadContext(params: { @@ -514,54 +513,46 @@ async function resolveTelegramCommandAuth(params: { effectiveGroupAllow, hasGroupAllowOverride, } = groupAllowContext; - // Use direct config dmPolicy override if available for DMs - const effectiveDmPolicy = - !isGroup && groupConfig && "dmPolicy" in groupConfig - ? (groupConfig.dmPolicy ?? telegramCfg.dmPolicy ?? "pairing") - : (telegramCfg.dmPolicy ?? "pairing"); + const effectiveDmPolicy = resolveTelegramEffectiveDmPolicy({ + isGroup, + groupConfig, + dmPolicy: telegramCfg.dmPolicy, + }); const requireTopic = !isGroup && groupConfig && "requireTopic" in groupConfig ? groupConfig.requireTopic : undefined; if (!isGroup && requireTopic === true && dmThreadId == null) { logVerbose(`Blocked telegram command in DM ${chatId}: requireTopic=true but no topic present`); return null; } - // For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom - const dmAllowFrom = groupAllowOverride ?? allowFrom; - const commandsAllowFrom = cfg.commands?.allowFrom; - const commandsAllowFromConfigured = - commandsAllowFrom != null && - typeof commandsAllowFrom === "object" && - (Array.isArray(commandsAllowFrom.telegram) || Array.isArray(commandsAllowFrom["*"])); + const dmAllow = await resolveTelegramDmAllow({ + cfg, + groupAllowOverride, + allowFrom, + accountId, + senderId, + storeAllowFrom: isGroup ? [] : storeAllowFrom, + dmPolicy: effectiveDmPolicy, + }); + const commandsAllowFromConfigured = isTelegramCommandsAllowFromConfigured(cfg); const commandsAllowFromAccess = commandsAllowFromConfigured - ? resolveCommandAuthorization({ - ctx: { - Provider: "telegram", - Surface: "telegram", - OriginatingChannel: "telegram", - AccountId: accountId, - ChatType: isGroup ? "group" : "direct", - From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`, - SenderId: senderId || undefined, - SenderUsername: senderUsername || undefined, - }, + ? resolveTelegramCommandAuthorization({ cfg, - // commands.allowFrom is the only auth source when configured. - commandAuthorized: false, + accountId, + chatId, + isGroup, + resolvedThreadId, + senderId, + senderUsername, }) : null; - const ownerAccess = resolveCommandAuthorization({ - ctx: { - Provider: "telegram", - Surface: "telegram", - OriginatingChannel: "telegram", - AccountId: accountId, - ChatType: isGroup ? "group" : "direct", - From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`, - SenderId: senderId || undefined, - SenderUsername: senderUsername || undefined, - }, + const ownerAccess = resolveTelegramCommandAuthorization({ cfg, - commandAuthorized: false, + accountId, + chatId, + isGroup, + resolvedThreadId, + senderId, + senderUsername, }); const sendAuthMessage = async (text: string) => { @@ -629,19 +620,8 @@ async function resolveTelegramCommandAuth(params: { } } - const expandedDmAllowFrom = await expandTelegramAllowFromWithAccessGroups({ - cfg, - allowFrom: dmAllowFrom, - accountId, - senderId, - }); - const dmAllow = normalizeDmAllowFromWithStore({ - allowFrom: expandedDmAllowFrom, - storeAllowFrom: isGroup ? [] : storeAllowFrom, - dmPolicy: effectiveDmPolicy, - }); const senderAllowed = isSenderAllowed({ - allow: dmAllow, + allow: dmAllow.effectiveAllow, senderId, senderUsername, }); @@ -654,7 +634,7 @@ async function resolveTelegramCommandAuth(params: { : resolveCommandAuthorizedFromAuthorizers({ useAccessGroups, authorizers: [ - { configured: dmAllow.hasEntries, allowed: senderAllowed }, + { configured: dmAllow.effectiveAllow.hasEntries, allowed: senderAllowed }, ...(isGroup ? [{ configured: effectiveGroupAllow.hasEntries, allowed: groupSenderAllowed }] : []), @@ -1168,7 +1148,6 @@ export const registerTelegramNativeCommands = ({ CommandTargetSessionKey: commandTargetSessionKey, MessageThreadId: threadSpec.id, IsForum: isForum, - // Originating context for sub-agent announce routing OriginatingChannel: "telegram" as const, OriginatingTo: originatingTo, }); @@ -1348,9 +1327,7 @@ export const registerTelegramNativeCommands = ({ if (typeof maybeMessageId === "number") { progressMessageId = maybeMessageId; } - } catch { - // Fall back to the normal final reply path if the placeholder send fails. - } + } catch {} } const sessionFileContext = await resolveTelegramCommandSessionFile({ @@ -1430,9 +1407,7 @@ export const registerTelegramNativeCommands = ({ groupId: isGroup ? String(chatId) : undefined, }); return; - } catch { - // Fall through to cleanup + normal delivered reply if editing fails. - } + } catch {} } await cleanupTelegramProgressPlaceholder({ bot, diff --git a/extensions/telegram/src/bot/helpers.ts b/extensions/telegram/src/bot/helpers.ts index 53afcc9d86c..d79ff4d5cf1 100644 --- a/extensions/telegram/src/bot/helpers.ts +++ b/extensions/telegram/src/bot/helpers.ts @@ -1,5 +1,9 @@ import type { Chat, Message } from "@grammyjs/types"; import { formatLocationText } from "openclaw/plugin-sdk/channel-inbound"; +import { + resolveCommandAuthorization, + type CommandAuthorization, +} from "openclaw/plugin-sdk/command-auth-native"; import type { OpenClawConfig, TelegramAccountConfig, @@ -380,6 +384,42 @@ export function buildTelegramGroupFrom(chatId: number | string, messageThreadId? return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`; } +export function isTelegramCommandsAllowFromConfigured(cfg: OpenClawConfig): boolean { + const commandsAllowFrom = cfg.commands?.allowFrom; + return ( + commandsAllowFrom != null && + typeof commandsAllowFrom === "object" && + (Array.isArray(commandsAllowFrom.telegram) || Array.isArray(commandsAllowFrom["*"])) + ); +} + +export function resolveTelegramCommandAuthorization(params: { + cfg: OpenClawConfig; + accountId: string; + chatId: number; + isGroup: boolean; + resolvedThreadId?: number; + senderId?: string; + senderUsername?: string; +}): CommandAuthorization { + return resolveCommandAuthorization({ + ctx: { + Provider: "telegram", + Surface: "telegram", + OriginatingChannel: "telegram", + AccountId: params.accountId, + ChatType: params.isGroup ? "group" : "direct", + From: params.isGroup + ? buildTelegramGroupFrom(params.chatId, params.resolvedThreadId) + : `telegram:${params.chatId}`, + SenderId: params.senderId || undefined, + SenderUsername: params.senderUsername || undefined, + }, + cfg: params.cfg, + commandAuthorized: false, + }); +} + /** * Build parentPeer for forum topic binding inheritance. * When a message comes from a forum topic, the peer ID includes the topic suffix