import type { Bot, Context } from "grammy"; import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline"; import { resolveCommandAuthorization, resolveCommandAuthorizedFromAuthorizers, resolveNativeCommandSessionTargets, } from "openclaw/plugin-sdk/command-auth"; import { buildCommandTextFromArgs, findCommandByNativeName, listNativeCommandSpecs, listNativeCommandSpecsForConfig, parseCommandArgs, resolveCommandArgMenu, type CommandArgs, } from "openclaw/plugin-sdk/command-auth"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { ChannelGroupPolicy } from "openclaw/plugin-sdk/config-runtime"; import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/config-runtime"; import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime"; import { normalizeTelegramCommandName, resolveTelegramCustomCommands, TELEGRAM_COMMAND_NAME_PATTERN, } from "openclaw/plugin-sdk/config-runtime"; import type { ReplyToMode, TelegramAccountConfig, TelegramDirectConfig, TelegramGroupConfig, TelegramTopicConfig, } from "openclaw/plugin-sdk/config-runtime"; import { ensureConfiguredBindingRouteReady, recordInboundSessionMetaSafe, } from "openclaw/plugin-sdk/conversation-runtime"; import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime"; import { executePluginCommand, getPluginCommandSpecs, matchPluginCommand, } from "openclaw/plugin-sdk/plugin-runtime"; import { resolveChunkMode } from "openclaw/plugin-sdk/reply-runtime"; import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-runtime"; import { resolveAgentRoute } from "openclaw/plugin-sdk/routing"; import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing"; import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { getChildLogger } from "openclaw/plugin-sdk/runtime-env"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { resolveTelegramAccount } from "./accounts.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; import { isSenderAllowed, normalizeDmAllowFromWithStore } from "./bot-access.js"; import { defaultTelegramBotDeps, type TelegramBotDeps } from "./bot-deps.js"; import type { TelegramMediaRef } from "./bot-message-context.js"; import type { TelegramMessageContextOptions } from "./bot-message-context.types.js"; import { buildCappedTelegramMenuCommands, buildPluginTelegramMenuCommands, syncTelegramMenuCommands, } from "./bot-native-command-menu.js"; import { TelegramUpdateKeyContext } from "./bot-updates.js"; import { TelegramBotOptions } from "./bot.js"; import { deliverReplies } from "./bot/delivery.js"; import { buildTelegramRoutingTarget, buildTelegramThreadParams, buildSenderName, buildTelegramGroupFrom, extractTelegramForumFlag, resolveTelegramForumFlag, resolveTelegramGroupAllowFromContext, resolveTelegramThreadSpec, } from "./bot/helpers.js"; import type { TelegramContext, TelegramGetChat } from "./bot/types.js"; import { resolveTelegramConversationBaseSessionKey, resolveTelegramConversationRoute, } from "./conversation-route.js"; import { shouldSuppressLocalTelegramExecApprovalPrompt } from "./exec-approvals.js"; import type { TelegramTransport } from "./fetch.js"; import { evaluateTelegramGroupBaseAccess, evaluateTelegramGroupPolicyAccess, } from "./group-access.js"; import { resolveTelegramGroupPromptSettings } from "./group-config-helpers.js"; import { buildInlineKeyboard } from "./send.js"; const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again."; type TelegramNativeCommandContext = Context & { match?: string }; type TelegramCommandAuthResult = { chatId: number; isGroup: boolean; isForum: boolean; resolvedThreadId?: number; senderId: string; senderUsername: string; groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig; commandAuthorized: boolean; }; export type RegisterTelegramHandlerParams = { cfg: OpenClawConfig; accountId: string; bot: Bot; mediaMaxBytes: number; opts: TelegramBotOptions; telegramTransport?: TelegramTransport; runtime: RuntimeEnv; telegramCfg: TelegramAccountConfig; telegramDeps?: TelegramBotDeps; allowFrom?: Array; groupAllowFrom?: Array; resolveGroupPolicy: (chatId: string | number) => ChannelGroupPolicy; resolveTelegramGroupConfig: ( chatId: string | number, messageThreadId?: number, ) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig }; shouldSkipUpdate: (ctx: TelegramUpdateKeyContext) => boolean; processMessage: ( ctx: TelegramContext, allMedia: TelegramMediaRef[], storeAllowFrom: string[], options?: TelegramMessageContextOptions, replyMedia?: TelegramMediaRef[], ) => Promise; logger: ReturnType; }; export type RegisterTelegramNativeCommandsParams = { bot: Bot; cfg: OpenClawConfig; runtime: RuntimeEnv; accountId: string; telegramCfg: TelegramAccountConfig; allowFrom?: Array; groupAllowFrom?: Array; replyToMode: ReplyToMode; textLimit: number; useAccessGroups: boolean; nativeEnabled: boolean; nativeSkillsEnabled: boolean; nativeDisabledExplicit: boolean; resolveGroupPolicy: (chatId: string | number) => ChannelGroupPolicy; resolveTelegramGroupConfig: ( chatId: string | number, messageThreadId?: number, ) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig }; shouldSkipUpdate: (ctx: TelegramUpdateKeyContext) => boolean; telegramDeps?: TelegramBotDeps; opts: { token: string }; }; async function resolveTelegramCommandAuth(params: { msg: NonNullable; bot: Bot; cfg: OpenClawConfig; accountId: string; telegramCfg: TelegramAccountConfig; readChannelAllowFromStore: TelegramBotDeps["readChannelAllowFromStore"]; allowFrom?: Array; groupAllowFrom?: Array; useAccessGroups: boolean; resolveGroupPolicy: (chatId: string | number) => ChannelGroupPolicy; resolveTelegramGroupConfig: ( chatId: string | number, messageThreadId?: number, ) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig }; requireAuth: boolean; }): Promise { const { msg, bot, cfg, accountId, telegramCfg, readChannelAllowFromStore, allowFrom, groupAllowFrom, useAccessGroups, resolveGroupPolicy, resolveTelegramGroupConfig, requireAuth, } = params; const chatId = msg.chat.id; const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup"; const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id; const getChat = typeof bot.api.getChat === "function" ? (bot.api.getChat.bind(bot.api) as TelegramGetChat) : undefined; const isForum = await resolveTelegramForumFlag({ chatId, chatType: msg.chat.type, isGroup, isForum: extractTelegramForumFlag(msg.chat), getChat, }); const threadSpec = resolveTelegramThreadSpec({ isGroup, isForum, messageThreadId, }); const threadParams = buildTelegramThreadParams(threadSpec) ?? {}; const groupAllowContext = await resolveTelegramGroupAllowFromContext({ chatId, accountId, isGroup, isForum, messageThreadId, groupAllowFrom, readChannelAllowFromStore, resolveTelegramGroupConfig, }); const { resolvedThreadId, dmThreadId, storeAllowFrom, groupConfig, topicConfig, groupAllowOverride, 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 requireTopic = (groupConfig as TelegramDirectConfig | undefined)?.requireTopic; 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 senderId = msg.from?.id ? String(msg.from.id) : ""; const senderUsername = msg.from?.username ?? ""; const commandsAllowFrom = cfg.commands?.allowFrom; const commandsAllowFromConfigured = commandsAllowFrom != null && typeof commandsAllowFrom === "object" && (Array.isArray(commandsAllowFrom.telegram) || Array.isArray(commandsAllowFrom["*"])); 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, }, cfg, // commands.allowFrom is the only auth source when configured. commandAuthorized: false, }) : null; const sendAuthMessage = async (text: string) => { await withTelegramApiErrorLogging({ operation: "sendMessage", fn: () => bot.api.sendMessage(chatId, text, threadParams), }); return null; }; const rejectNotAuthorized = async () => { return await sendAuthMessage("You are not authorized to use this command."); }; const baseAccess = evaluateTelegramGroupBaseAccess({ isGroup, groupConfig, topicConfig, hasGroupAllowOverride, effectiveGroupAllow, senderId, senderUsername, enforceAllowOverride: requireAuth, requireSenderForAllowOverride: true, }); if (!baseAccess.allowed) { if (baseAccess.reason === "group-disabled") { return await sendAuthMessage("This group is disabled."); } if (baseAccess.reason === "topic-disabled") { return await sendAuthMessage("This topic is disabled."); } return await rejectNotAuthorized(); } const policyAccess = evaluateTelegramGroupPolicyAccess({ isGroup, chatId, cfg, telegramCfg, topicConfig, groupConfig, effectiveGroupAllow, senderId, senderUsername, resolveGroupPolicy, enforcePolicy: useAccessGroups, useTopicAndGroupOverrides: false, enforceAllowlistAuthorization: requireAuth && !commandsAllowFromConfigured, allowEmptyAllowlistEntries: true, requireSenderForAllowlistAuthorization: true, checkChatAllowlist: useAccessGroups, }); if (!policyAccess.allowed) { if (policyAccess.reason === "group-policy-disabled") { return await sendAuthMessage("Telegram group commands are disabled."); } if ( policyAccess.reason === "group-policy-allowlist-no-sender" || policyAccess.reason === "group-policy-allowlist-unauthorized" ) { return await rejectNotAuthorized(); } if (policyAccess.reason === "group-chat-not-allowed") { return await sendAuthMessage("This group is not allowed."); } } const dmAllow = normalizeDmAllowFromWithStore({ allowFrom: dmAllowFrom, storeAllowFrom: isGroup ? [] : storeAllowFrom, dmPolicy: effectiveDmPolicy, }); const senderAllowed = isSenderAllowed({ allow: dmAllow, senderId, senderUsername, }); const groupSenderAllowed = isGroup ? isSenderAllowed({ allow: effectiveGroupAllow, senderId, senderUsername }) : false; const commandAuthorized = commandsAllowFromConfigured ? Boolean(commandsAllowFromAccess?.isAuthorizedSender) : resolveCommandAuthorizedFromAuthorizers({ useAccessGroups, authorizers: [ { configured: dmAllow.hasEntries, allowed: senderAllowed }, ...(isGroup ? [{ configured: effectiveGroupAllow.hasEntries, allowed: groupSenderAllowed }] : []), ], modeWhenAccessGroupsOff: "configured", }); if (requireAuth && !commandAuthorized) { return await rejectNotAuthorized(); } return { chatId, isGroup, isForum, resolvedThreadId, senderId, senderUsername, groupConfig, topicConfig, commandAuthorized, }; } export const registerTelegramNativeCommands = ({ bot, cfg, runtime, accountId, telegramCfg, allowFrom, groupAllowFrom, replyToMode, textLimit, useAccessGroups, nativeEnabled, nativeSkillsEnabled, nativeDisabledExplicit, resolveGroupPolicy, resolveTelegramGroupConfig, shouldSkipUpdate, telegramDeps = defaultTelegramBotDeps, opts, }: RegisterTelegramNativeCommandsParams) => { const boundRoute = nativeEnabled && nativeSkillsEnabled ? resolveAgentRoute({ cfg, channel: "telegram", accountId }) : null; if (nativeEnabled && nativeSkillsEnabled && !boundRoute) { runtime.log?.( "nativeSkillsEnabled is true but no agent route is bound for this Telegram account; skill commands will not appear in the native menu.", ); } const skillCommands = nativeEnabled && nativeSkillsEnabled && boundRoute ? telegramDeps.listSkillCommandsForAgents({ cfg, agentIds: [boundRoute.agentId], }) : []; const nativeCommands = nativeEnabled ? listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "telegram", }) : []; const reservedCommands = new Set( listNativeCommandSpecs().map((command) => normalizeTelegramCommandName(command.name)), ); for (const command of skillCommands) { reservedCommands.add(command.name.toLowerCase()); } const customResolution = resolveTelegramCustomCommands({ commands: telegramCfg.customCommands, reservedCommands, }); for (const issue of customResolution.issues) { runtime.error?.(danger(issue.message)); } const customCommands = customResolution.commands; const pluginCommandSpecs = getPluginCommandSpecs("telegram"); const existingCommands = new Set( [ ...nativeCommands.map((command) => normalizeTelegramCommandName(command.name)), ...customCommands.map((command) => command.command), ].map((command) => command.toLowerCase()), ); const pluginCatalog = buildPluginTelegramMenuCommands({ specs: pluginCommandSpecs, existingCommands, }); for (const issue of pluginCatalog.issues) { runtime.error?.(danger(issue)); } const loadFreshRuntimeConfig = (): OpenClawConfig => telegramDeps.loadConfig(); const resolveFreshTelegramConfig = (runtimeCfg: OpenClawConfig): TelegramAccountConfig => { try { return resolveTelegramAccount({ cfg: runtimeCfg, accountId, }).config; } catch (error) { logVerbose( `telegram native command: failed to load fresh account config for ${accountId}; using startup snapshot: ${String(error)}`, ); return telegramCfg; } }; const allCommandsFull: Array<{ command: string; description: string }> = [ ...nativeCommands .map((command) => { const normalized = normalizeTelegramCommandName(command.name); if (!TELEGRAM_COMMAND_NAME_PATTERN.test(normalized)) { runtime.error?.( danger( `Native command "${command.name}" is invalid for Telegram (resolved to "${normalized}"). Skipping.`, ), ); return null; } return { command: normalized, description: command.description, }; }) .filter((cmd): cmd is { command: string; description: string } => cmd !== null), ...(nativeEnabled ? pluginCatalog.commands : []), ...customCommands, ]; const { commandsToRegister, totalCommands, maxCommands, overflowCount } = buildCappedTelegramMenuCommands({ allCommands: allCommandsFull, }); if (overflowCount > 0) { runtime.log?.( `Telegram limits bots to ${maxCommands} commands. ` + `${totalCommands} configured; registering first ${maxCommands}. ` + `Use channels.telegram.commands.native: false to disable, or reduce plugin/skill/custom commands.`, ); } // Telegram only limits the setMyCommands payload (menu entries). // Keep hidden commands callable by registering handlers for the full catalog. syncTelegramMenuCommands({ bot, runtime, commandsToRegister, accountId, botIdentity: opts.token, }); const resolveCommandRuntimeContext = async (params: { msg: NonNullable; runtimeCfg: OpenClawConfig; isGroup: boolean; isForum: boolean; resolvedThreadId?: number; senderId?: string; topicAgentId?: string; }): Promise<{ chatId: number; threadSpec: ReturnType; route: ReturnType["route"]; mediaLocalRoots: readonly string[] | undefined; tableMode: ReturnType; chunkMode: ReturnType; } | null> => { const { msg, runtimeCfg, isGroup, isForum, resolvedThreadId, senderId, topicAgentId } = params; const chatId = msg.chat.id; const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id; const threadSpec = resolveTelegramThreadSpec({ isGroup, isForum, messageThreadId: resolvedThreadId ?? messageThreadId, }); let { route, configuredBinding } = resolveTelegramConversationRoute({ cfg: runtimeCfg, accountId, chatId, isGroup, resolvedThreadId, replyThreadId: threadSpec.id, senderId, topicAgentId, }); if (configuredBinding) { const ensured = await ensureConfiguredBindingRouteReady({ cfg: runtimeCfg, bindingResolution: configuredBinding, }); if (!ensured.ok) { logVerbose( `telegram native command: configured ACP binding unavailable for topic ${configuredBinding.record.conversation.conversationId}: ${ensured.error}`, ); await withTelegramApiErrorLogging({ operation: "sendMessage", runtime, fn: () => bot.api.sendMessage( chatId, "Configured ACP binding is unavailable right now. Please try again.", buildTelegramThreadParams(threadSpec) ?? {}, ), }); return null; } } const mediaLocalRoots = getAgentScopedMediaLocalRoots(runtimeCfg, route.agentId); const tableMode = resolveMarkdownTableMode({ cfg: runtimeCfg, channel: "telegram", accountId: route.accountId, }); const chunkMode = resolveChunkMode(runtimeCfg, "telegram", route.accountId); return { chatId, threadSpec, route, mediaLocalRoots, tableMode, chunkMode }; }; const buildCommandDeliveryBaseOptions = (params: { chatId: string | number; accountId: string; sessionKeyForInternalHooks?: string; mirrorIsGroup?: boolean; mirrorGroupId?: string; mediaLocalRoots?: readonly string[]; threadSpec: ReturnType; tableMode: ReturnType; chunkMode: ReturnType; linkPreview?: boolean; }) => ({ chatId: String(params.chatId), accountId: params.accountId, sessionKeyForInternalHooks: params.sessionKeyForInternalHooks, mirrorIsGroup: params.mirrorIsGroup, mirrorGroupId: params.mirrorGroupId, token: opts.token, runtime, bot, mediaLocalRoots: params.mediaLocalRoots, replyToMode, textLimit, thread: params.threadSpec, tableMode: params.tableMode, chunkMode: params.chunkMode, linkPreview: params.linkPreview, }); if (commandsToRegister.length > 0 || pluginCatalog.commands.length > 0) { for (const command of nativeCommands) { const normalizedCommandName = normalizeTelegramCommandName(command.name); bot.command(normalizedCommandName, async (ctx: TelegramNativeCommandContext) => { const msg = ctx.message; if (!msg) { return; } if (shouldSkipUpdate(ctx)) { return; } const runtimeCfg = loadFreshRuntimeConfig(); const runtimeTelegramCfg = resolveFreshTelegramConfig(runtimeCfg); const auth = await resolveTelegramCommandAuth({ msg, bot, cfg: runtimeCfg, accountId, telegramCfg: runtimeTelegramCfg, readChannelAllowFromStore: telegramDeps.readChannelAllowFromStore, allowFrom, groupAllowFrom, useAccessGroups, resolveGroupPolicy, resolveTelegramGroupConfig, requireAuth: true, }); if (!auth) { return; } const { chatId, isGroup, isForum, resolvedThreadId, senderId, senderUsername, groupConfig, topicConfig, commandAuthorized, } = auth; const runtimeContext = await resolveCommandRuntimeContext({ msg, runtimeCfg, isGroup, isForum, resolvedThreadId, senderId, topicAgentId: topicConfig?.agentId, }); if (!runtimeContext) { return; } const { threadSpec, route, mediaLocalRoots, tableMode, chunkMode } = runtimeContext; const threadParams = buildTelegramThreadParams(threadSpec) ?? {}; const originatingTo = buildTelegramRoutingTarget(chatId, threadSpec); const executionCfg = getRuntimeConfigSnapshot() ?? cfg; const commandDefinition = findCommandByNativeName(command.name, "telegram"); const rawText = ctx.match?.trim() ?? ""; const commandArgs = commandDefinition ? parseCommandArgs(commandDefinition, rawText) : rawText ? ({ raw: rawText } satisfies CommandArgs) : undefined; const prompt = commandDefinition ? buildCommandTextFromArgs(commandDefinition, commandArgs) : rawText ? `/${command.name} ${rawText}` : `/${command.name}`; const menu = commandDefinition ? resolveCommandArgMenu({ command: commandDefinition, args: commandArgs, cfg: runtimeCfg, }) : null; if (menu && commandDefinition) { const title = menu.title ?? `Choose ${menu.arg.description || menu.arg.name} for /${commandDefinition.nativeName ?? commandDefinition.key}.`; const rows: Array> = []; for (let i = 0; i < menu.choices.length; i += 2) { const slice = menu.choices.slice(i, i + 2); rows.push( slice.map((choice) => { const args: CommandArgs = { values: { [menu.arg.name]: choice.value }, }; return { text: choice.label, callback_data: buildCommandTextFromArgs(commandDefinition, args), }; }), ); } const replyMarkup = buildInlineKeyboard(rows); await withTelegramApiErrorLogging({ operation: "sendMessage", runtime, fn: () => bot.api.sendMessage(chatId, title, { ...(replyMarkup ? { reply_markup: replyMarkup } : {}), ...threadParams, }), }); return; } const baseSessionKey = resolveTelegramConversationBaseSessionKey({ cfg: runtimeCfg, route, chatId, isGroup, senderId, }); // DMs: use raw messageThreadId for thread sessions (not resolvedThreadId which is for forums) const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined; const threadKeys = dmThreadId != null ? resolveThreadSessionKeys({ baseSessionKey, threadId: `${chatId}:${dmThreadId}`, }) : null; const sessionKey = threadKeys?.sessionKey ?? baseSessionKey; const { skillFilter, groupSystemPrompt } = resolveTelegramGroupPromptSettings({ groupConfig, topicConfig, }); const { sessionKey: commandSessionKey, commandTargetSessionKey } = resolveNativeCommandSessionTargets({ agentId: route.agentId, sessionPrefix: "telegram:slash", userId: String(senderId || chatId), targetSessionKey: sessionKey, }); const deliveryBaseOptions = buildCommandDeliveryBaseOptions({ chatId, accountId: route.accountId, sessionKeyForInternalHooks: commandSessionKey, mirrorIsGroup: isGroup, mirrorGroupId: isGroup ? String(chatId) : undefined, mediaLocalRoots, threadSpec, tableMode, chunkMode, linkPreview: runtimeTelegramCfg.linkPreview, }); const conversationLabel = isGroup ? msg.chat.title ? `${msg.chat.title} id:${chatId}` : `group:${chatId}` : (buildSenderName(msg) ?? String(senderId || chatId)); const ctxPayload = finalizeInboundContext({ Body: prompt, BodyForAgent: prompt, RawBody: prompt, CommandBody: prompt, CommandArgs: commandArgs, From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`, To: `slash:${senderId || chatId}`, ChatType: isGroup ? "group" : "direct", ConversationLabel: conversationLabel, GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined, GroupSystemPrompt: isGroup || (!isGroup && groupConfig) ? groupSystemPrompt : undefined, SenderName: buildSenderName(msg), SenderId: senderId || undefined, SenderUsername: senderUsername || undefined, Surface: "telegram", Provider: "telegram", MessageSid: String(msg.message_id), Timestamp: msg.date ? msg.date * 1000 : undefined, WasMentioned: true, CommandAuthorized: commandAuthorized, CommandSource: "native" as const, SessionKey: commandSessionKey, AccountId: route.accountId, CommandTargetSessionKey: commandTargetSessionKey, MessageThreadId: threadSpec.id, IsForum: isForum, // Originating context for sub-agent announce routing OriginatingChannel: "telegram" as const, OriginatingTo: originatingTo, }); await recordInboundSessionMetaSafe({ cfg: executionCfg, agentId: route.agentId, sessionKey: ctxPayload.SessionKey ?? route.sessionKey, ctx: ctxPayload, onError: (err) => runtime.error?.(danger(`telegram slash: failed updating session meta: ${String(err)}`)), }); const disableBlockStreaming = typeof runtimeTelegramCfg.blockStreaming === "boolean" ? !runtimeTelegramCfg.blockStreaming : undefined; const deliveryState = { delivered: false, skippedNonSilent: 0, }; const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({ cfg: executionCfg, agentId: route.agentId, channel: "telegram", accountId: route.accountId, }); await telegramDeps.dispatchReplyWithBufferedBlockDispatcher({ ctx: ctxPayload, cfg: executionCfg, dispatcherOptions: { ...replyPipeline, deliver: async (payload, _info) => { if ( shouldSuppressLocalTelegramExecApprovalPrompt({ cfg: executionCfg, accountId: route.accountId, payload, }) ) { deliveryState.delivered = true; return; } const result = await deliverReplies({ replies: [payload], ...deliveryBaseOptions, silent: runtimeTelegramCfg.silentErrorReplies === true && payload.isError === true, }); if (result.delivered) { deliveryState.delivered = true; } }, onSkip: (_payload, info) => { if (info.reason !== "silent") { deliveryState.skippedNonSilent += 1; } }, onError: (err, info) => { runtime.error?.(danger(`telegram slash ${info.kind} reply failed: ${String(err)}`)); }, }, replyOptions: { skillFilter, disableBlockStreaming, onModelSelected, }, }); if (!deliveryState.delivered && deliveryState.skippedNonSilent > 0) { await deliverReplies({ replies: [{ text: EMPTY_RESPONSE_FALLBACK }], ...deliveryBaseOptions, }); } }); } for (const pluginCommand of pluginCatalog.commands) { bot.command(pluginCommand.command, async (ctx: TelegramNativeCommandContext) => { const msg = ctx.message; if (!msg) { return; } if (shouldSkipUpdate(ctx)) { return; } const chatId = msg.chat.id; const runtimeCfg = loadFreshRuntimeConfig(); const runtimeTelegramCfg = resolveFreshTelegramConfig(runtimeCfg); const rawText = ctx.match?.trim() ?? ""; const commandBody = `/${pluginCommand.command}${rawText ? ` ${rawText}` : ""}`; const match = matchPluginCommand(commandBody); if (!match) { await withTelegramApiErrorLogging({ operation: "sendMessage", runtime, fn: () => bot.api.sendMessage(chatId, "Command not found."), }); return; } const auth = await resolveTelegramCommandAuth({ msg, bot, cfg: runtimeCfg, accountId, telegramCfg: runtimeTelegramCfg, readChannelAllowFromStore: telegramDeps.readChannelAllowFromStore, allowFrom, groupAllowFrom, useAccessGroups, resolveGroupPolicy, resolveTelegramGroupConfig, requireAuth: match.command.requireAuth !== false, }); if (!auth) { return; } const { senderId, commandAuthorized, isGroup, isForum, resolvedThreadId } = auth; const runtimeContext = await resolveCommandRuntimeContext({ msg, runtimeCfg, isGroup, isForum, resolvedThreadId, senderId, topicAgentId: auth.topicConfig?.agentId, }); if (!runtimeContext) { return; } const { threadSpec, route, mediaLocalRoots, tableMode, chunkMode } = runtimeContext; const deliveryBaseOptions = buildCommandDeliveryBaseOptions({ chatId, accountId: route.accountId, sessionKeyForInternalHooks: route.sessionKey, mirrorIsGroup: isGroup, mirrorGroupId: isGroup ? String(chatId) : undefined, mediaLocalRoots, threadSpec, tableMode, chunkMode, linkPreview: runtimeTelegramCfg.linkPreview, }); const from = isGroup ? buildTelegramGroupFrom(chatId, threadSpec.id) : `telegram:${chatId}`; const to = `telegram:${chatId}`; const result = await executePluginCommand({ command: match.command, args: match.args, senderId, channel: "telegram", isAuthorizedSender: commandAuthorized, commandBody, config: runtimeCfg, from, to, accountId, messageThreadId: threadSpec.id, }); if ( !shouldSuppressLocalTelegramExecApprovalPrompt({ cfg: runtimeCfg, accountId: route.accountId, payload: result, }) ) { await deliverReplies({ replies: [result], ...deliveryBaseOptions, silent: runtimeTelegramCfg.silentErrorReplies === true && result.isError === true, }); } }); } } else if (nativeDisabledExplicit) { withTelegramApiErrorLogging({ operation: "setMyCommands", runtime, fn: () => bot.api.setMyCommands([]), }).catch(() => {}); } };