UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

994 lines (993 loc) 39.4 kB
import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { a as root } from "./secure-temp-dir-XAWcZnE2.js"; import { r as logVerbose, s as warn, t as danger } from "./globals-GTrXU4s9.js"; import { t as getGlobalHookRunner, u as fireAndForgetHook } from "./hook-runner-global-BnyQREr6.js"; import { m as triggerInternalHook, n as createInternalHookEvent } from "./internal-hooks-Bq6_9FFu.js"; import { c as kindFromMime, s as isGifMedia } from "./mime-C8mVE2Bw.js"; import { u as saveMediaBuffer } from "./store-C9M_0A1p.js"; import { a as toInternalMessageSentContext, d as toPluginMessageSentEvent, l as toPluginMessageContext, t as buildCanonicalSentMessageHookContext } from "./message-hook-mappers-BqsqHKq-.js"; import { l as normalizeMessagePresentation } from "./payload-CZlThETZ.js"; import { n as probeVideoDimensions } from "./media-services-DE3J1qM-.js"; import { n as retryAsync } from "./retry-BSIArBBz.js"; import { a as saveRemoteMedia, n as MediaFetchError } from "./fetch-2npQWHQh.js"; import { n as loadWebMedia } from "./web-media-D8G2d_q7.js"; import { i as chunkMarkdownTextWithMode } from "./chunk-Cpb8JO1x.js"; import { a as projectOutboundPayloadPlanForDelivery, t as createOutboundPayloadPlan } from "./payloads-B4DTFTLz.js"; import { t as buildOutboundMediaLoadOptions } from "./load-options-VzbF4ozo.js"; import "./media-runtime-fgpmX6eL.js"; import "./runtime-env-D_2q8-VK.js"; import { i as shouldRetryTelegramTransportFallback, t as resolveTelegramApiBase } from "./fetch-DVxN6WLs.js"; import { o as isSafeToRetrySendError, u as isTelegramRateLimitError } from "./request-timeouts-DYFbgXTJ.js"; import "./reply-chunking-C9pgDFVG.js"; import "./channel-outbound-B3_Zy-kG.js"; import "./web-media-Nj3kZBh5.js"; import "./file-access-runtime-D4fGIm4U.js"; import { n as createChannelApiRetryRunner } from "./retry-policy-COMGOvKi.js"; import "./ssrf-runtime-BOGN5pUi.js"; import "./plugin-runtime-Dw_Zc32A.js"; import "./hook-runtime-CA9YrfdF.js"; import "./retry-runtime-DdapUPJ5.js"; import { I as resolveTelegramReplyId, Y as resolveTelegramMediaPlaceholder } from "./sent-message-cache-IpiNMjf9.js"; import { t as resolveTelegramInlineButtons } from "./button-types-Di5CvasV.js"; import { a as renderTelegramHtmlText, c as wrapFileReferencesInHtml, n as markdownToTelegramChunks, r as markdownToTelegramHtml } from "./format-RoDvU0eA.js"; import { t as resolveTelegramInteractiveTextFallback } from "./interactive-fallback-BQgyaNXo.js"; import { o as getCachedSticker, r as cacheSticker } from "./sticker-cache-store-mh6jiBwt.js"; import "./sticker-cache-BFNVd7JL.js"; import { D as withTelegramApiErrorLogging, E as splitTelegramCaption, _ as getTelegramNativeQuoteReplyMessageId, b as buildInlineKeyboard, g as buildTelegramSendParams, h as resolveTelegramVoiceSend, v as removeTelegramNativeQuoteParam } from "./send-f0zZCDGg.js"; import path from "node:path"; import { GrammyError, InputFile } from "grammy"; //#region extensions/telegram/src/bot/delivery.send.ts const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i; const EMPTY_TEXT_ERR_RE = /message text is empty/i; const QUOTE_PARAM_RE = /\bquote not found\b|\bQUOTE_TEXT_INVALID\b|\bquote text invalid\b/i; const GrammyErrorCtor$2 = typeof GrammyError === "function" ? GrammyError : void 0; function isTelegramQuoteParamError(err) { if (GrammyErrorCtor$2 && err instanceof GrammyErrorCtor$2) return QUOTE_PARAM_RE.test(err.description); return QUOTE_PARAM_RE.test(formatErrorMessage(err)); } function createTelegramDeliverySendRetry() { return createChannelApiRetryRunner({ shouldRetry: (err) => isSafeToRetrySendError(err) || isTelegramRateLimitError(err), strictShouldRetry: true }); } async function sendTelegramWithThreadFallback(params) { const hasNativeQuote = getTelegramNativeQuoteReplyMessageId(params.requestParams) != null; const shouldSuppressFirstErrorLog = (err) => hasNativeQuote && isTelegramQuoteParamError(err); const mergedShouldLog = params.shouldLog ? (err) => params.shouldLog(err) && !shouldSuppressFirstErrorLog(err) : (err) => !shouldSuppressFirstErrorLog(err); const requestWithRetry = createTelegramDeliverySendRetry(); const runLoggedSend = (operation, requestParams, shouldLog) => withTelegramApiErrorLogging({ operation, runtime: params.runtime, ...shouldLog ? { shouldLog } : {}, fn: () => requestWithRetry(() => params.send(requestParams), operation) }); try { return await runLoggedSend(params.operation, params.requestParams, mergedShouldLog); } catch (err) { if (hasNativeQuote && isTelegramQuoteParamError(err)) { params.runtime.log?.(`telegram ${params.operation}: native quote rejected; retrying with legacy reply_to_message_id`); return await sendTelegramWithThreadFallback({ ...params, operation: `${params.operation} (legacy reply retry)`, requestParams: removeTelegramNativeQuoteParam(params.requestParams) }); } throw err; } } async function sendTelegramText(bot, chatId, text, runtime, opts) { const baseParams = buildTelegramSendParams({ replyToMessageId: opts?.replyToMessageId, replyQuoteMessageId: opts?.replyQuoteMessageId, replyQuoteText: opts?.replyQuoteText, replyQuotePosition: opts?.replyQuotePosition, replyQuoteEntities: opts?.replyQuoteEntities, thread: opts?.thread, silent: opts?.silent }); const linkPreviewOptions = opts?.linkPreview ?? true ? void 0 : { is_disabled: true }; const htmlText = (opts?.textMode ?? "markdown") === "html" ? text : markdownToTelegramHtml(text); const fallbackText = opts?.plainText ?? text; const hasFallbackText = fallbackText.trim().length > 0; const sendPlainFallback = async () => { const res = await sendTelegramWithThreadFallback({ operation: "sendMessage", runtime, thread: opts?.thread, requestParams: baseParams, send: (effectiveParams) => bot.api.sendMessage(chatId, fallbackText, { ...linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}, ...opts?.replyMarkup ? { reply_markup: opts.replyMarkup } : {}, ...effectiveParams }) }); runtime.log?.(`telegram sendMessage ok chat=${chatId} message=${res.message_id} (plain)`); return res.message_id; }; if (!htmlText.trim()) { if (!hasFallbackText) throw new Error("telegram sendMessage failed: empty formatted text and empty plain fallback"); return await sendPlainFallback(); } try { const res = await sendTelegramWithThreadFallback({ operation: "sendMessage", runtime, thread: opts?.thread, requestParams: baseParams, shouldLog: (err) => { const errText = formatErrorMessage(err); return !PARSE_ERR_RE.test(errText) && !EMPTY_TEXT_ERR_RE.test(errText); }, send: (effectiveParams) => bot.api.sendMessage(chatId, htmlText, { parse_mode: "HTML", ...linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}, ...opts?.replyMarkup ? { reply_markup: opts.replyMarkup } : {}, ...effectiveParams }) }); runtime.log?.(`telegram sendMessage ok chat=${chatId} message=${res.message_id}`); return res.message_id; } catch (err) { const errText = formatErrorMessage(err); if (PARSE_ERR_RE.test(errText) || EMPTY_TEXT_ERR_RE.test(errText)) { if (!hasFallbackText) throw err; runtime.log?.(`telegram formatted send failed; retrying without formatting: ${errText}`); return await sendPlainFallback(); } throw err; } } //#endregion //#region extensions/telegram/src/bot/reply-threading.ts function resolveReplyToForSend(params) { return params.replyToId && (params.replyToMode === "all" || !params.progress.hasReplied) ? params.replyToId : void 0; } function markReplyApplied(progress, replyToId) { if (replyToId && !progress.hasReplied) progress.hasReplied = true; } function markDelivered$1(progress) { progress.hasDelivered = true; } async function sendChunkedTelegramReplyText(params) { const applyDelivered = params.markDelivered ?? markDelivered$1; for (let i = 0; i < params.chunks.length; i += 1) { const chunk = params.chunks[i]; if (!chunk) continue; const isFirstChunk = i === 0; const replyToMessageId = resolveReplyToForSend({ replyToId: params.replyToId, replyToMode: params.replyToMode, progress: params.progress }); const shouldAttachQuote = Boolean(replyToMessageId) && Boolean(params.replyQuoteText) && (params.quoteOnlyOnFirstChunk !== true || isFirstChunk); await params.sendChunk({ chunk, isFirstChunk, replyToMessageId, replyMarkup: isFirstChunk ? params.replyMarkup : void 0, replyQuoteText: shouldAttachQuote ? params.replyQuoteText : void 0 }); markReplyApplied(params.progress, replyToMessageId); applyDelivered(params.progress); } } //#endregion //#region extensions/telegram/src/bot/delivery.replies.ts const VOICE_FORBIDDEN_MARKER = "VOICE_MESSAGES_FORBIDDEN"; const CAPTION_TOO_LONG_RE = /caption is too long/i; const GrammyErrorCtor$1 = typeof GrammyError === "function" ? GrammyError : void 0; function buildChunkTextResolver(params) { return (markdown) => { const markdownChunks = params.chunkMode === "newline" ? chunkMarkdownTextWithMode(markdown, params.textLimit, params.chunkMode) : [markdown]; const chunks = []; for (const chunk of markdownChunks) { const nested = markdownToTelegramChunks(chunk, params.textLimit, { tableMode: params.tableMode }); if (!nested.length && chunk) { chunks.push({ html: wrapFileReferencesInHtml(markdownToTelegramHtml(chunk, { tableMode: params.tableMode, wrapFileRefs: false })), text: chunk }); continue; } chunks.push(...nested); } return chunks; }; } function markDelivered(progress) { progress.hasDelivered = true; progress.deliveredCount += 1; } function filterEmptyTelegramTextChunks(chunks) { return chunks.filter((chunk) => chunk.text.trim().length > 0); } function resolveReplyQuoteForSend(params) { if (params.replyToId != null) { const mapped = params.replyQuoteByMessageId?.[String(params.replyToId)]; if (mapped?.text) { const quote = { messageId: params.replyToId, text: mapped.text }; if (typeof mapped.position === "number") quote.position = mapped.position; if (mapped.entities) quote.entities = mapped.entities; return quote; } } const quote = {}; if (params.replyQuoteMessageId != null) quote.messageId = params.replyQuoteMessageId; if (params.replyQuoteText != null) quote.text = params.replyQuoteText; if (params.replyQuotePosition != null) quote.position = params.replyQuotePosition; if (params.replyQuoteEntities != null) quote.entities = params.replyQuoteEntities; return quote; } async function deliverTextReply(params) { let firstDeliveredMessageId; await sendChunkedTelegramReplyText({ chunks: filterEmptyTelegramTextChunks(params.chunkText(params.replyText)), progress: params.progress, replyToId: params.replyToId, replyToMode: params.replyToMode, replyMarkup: params.replyMarkup, replyQuoteText: params.replyQuoteText, markDelivered, sendChunk: async ({ chunk, replyToMessageId, replyMarkup, replyQuoteText }) => { const messageId = await sendTelegramText(params.bot, params.chatId, chunk.html, params.runtime, { replyToMessageId, replyQuoteMessageId: params.replyQuoteMessageId, replyQuoteText, replyQuotePosition: params.replyQuotePosition, replyQuoteEntities: params.replyQuoteEntities, thread: params.thread, textMode: "html", plainText: chunk.text, linkPreview: params.linkPreview, silent: params.silent, replyMarkup }); if (firstDeliveredMessageId == null) firstDeliveredMessageId = messageId; } }); return firstDeliveredMessageId; } async function sendPendingFollowUpText(params) { await sendChunkedTelegramReplyText({ chunks: filterEmptyTelegramTextChunks(params.chunkText(params.text)), progress: params.progress, replyToId: params.replyToId, replyToMode: params.replyToMode, replyMarkup: params.replyMarkup, markDelivered, sendChunk: async ({ chunk, replyToMessageId, replyMarkup }) => { await sendTelegramText(params.bot, params.chatId, chunk.html, params.runtime, { replyToMessageId, thread: params.thread, textMode: "html", plainText: chunk.text, linkPreview: params.linkPreview, silent: params.silent, replyMarkup }); } }); } function isVoiceMessagesForbidden(err) { if (GrammyErrorCtor$1 && err instanceof GrammyErrorCtor$1) return err.description.includes(VOICE_FORBIDDEN_MARKER); return formatErrorMessage(err).includes(VOICE_FORBIDDEN_MARKER); } function isCaptionTooLong(err) { if (GrammyErrorCtor$1 && err instanceof GrammyErrorCtor$1) return CAPTION_TOO_LONG_RE.test(err.description); return CAPTION_TOO_LONG_RE.test(formatErrorMessage(err)); } function resolveVoiceFallbackText(reply) { if (reply.text?.trim()) return reply.text; if (reply.spokenText?.trim()) return reply.spokenText; } async function sendTelegramVoiceFallbackText(opts) { let firstDeliveredMessageId; const chunks = filterEmptyTelegramTextChunks(opts.chunkText(opts.text)); let appliedReplyTo = false; for (const chunk of chunks) { const replyToForChunk = !appliedReplyTo ? opts.replyToId : void 0; const applyQuoteForChunk = !appliedReplyTo; const messageId = await sendTelegramText(opts.bot, opts.chatId, chunk.html, opts.runtime, { replyToMessageId: replyToForChunk, replyQuoteMessageId: applyQuoteForChunk ? opts.replyQuoteMessageId : void 0, replyQuoteText: applyQuoteForChunk ? opts.replyQuoteText : void 0, replyQuotePosition: applyQuoteForChunk ? opts.replyQuotePosition : void 0, replyQuoteEntities: applyQuoteForChunk ? opts.replyQuoteEntities : void 0, thread: opts.thread, textMode: "html", plainText: chunk.text, linkPreview: opts.linkPreview, silent: opts.silent, replyMarkup: !appliedReplyTo ? opts.replyMarkup : void 0 }); if (firstDeliveredMessageId == null) firstDeliveredMessageId = messageId; if (replyToForChunk) appliedReplyTo = true; } return firstDeliveredMessageId; } async function deliverMediaReply(params) { let firstDeliveredMessageId; let visibleFallbackText; let first = true; let pendingFollowUpText; for (const mediaUrl of params.mediaList) { const isFirstMedia = first; const media = await params.mediaLoader(mediaUrl, buildOutboundMediaLoadOptions({ mediaLocalRoots: params.mediaLocalRoots, maxBytes: params.mediaMaxBytes })); const kind = kindFromMime(media.contentType ?? void 0); const isGif = isGifMedia({ contentType: media.contentType, fileName: media.fileName }); const fileName = media.fileName ?? (isGif ? "animation.gif" : "file"); const file = new InputFile(media.buffer, fileName); const { caption, followUpText } = splitTelegramCaption(isFirstMedia ? params.reply.text ?? void 0 : void 0); const htmlCaption = caption ? renderTelegramHtmlText(caption, { tableMode: params.tableMode }) : void 0; if (followUpText) pendingFollowUpText = followUpText; first = false; const replyToMessageId = resolveReplyToForSend({ replyToId: params.replyToId, replyToMode: params.replyToMode, progress: params.progress }); const shouldAttachButtonsToMedia = isFirstMedia && params.replyMarkup && !followUpText; const videoDimensions = kind === "video" ? await probeVideoDimensions(media.buffer) : void 0; const mediaParams = { caption: htmlCaption, ...htmlCaption ? { parse_mode: "HTML" } : {}, ...shouldAttachButtonsToMedia ? { reply_markup: params.replyMarkup } : {}, ...videoDimensions ? { width: videoDimensions.width, height: videoDimensions.height } : {}, ...buildTelegramSendParams({ replyToMessageId, replyQuoteMessageId: params.replyQuoteMessageId, replyQuoteText: params.replyQuoteText, replyQuotePosition: params.replyQuotePosition, replyQuoteEntities: params.replyQuoteEntities, thread: params.thread, silent: params.silent }) }; if (isGif) { const result = await sendTelegramWithThreadFallback({ operation: "sendAnimation", runtime: params.runtime, thread: params.thread, requestParams: mediaParams, send: (effectiveParams) => params.bot.api.sendAnimation(params.chatId, file, { ...effectiveParams }) }); if (firstDeliveredMessageId == null) firstDeliveredMessageId = result.message_id; markDelivered(params.progress); } else if (kind === "image") { const result = await sendTelegramWithThreadFallback({ operation: "sendPhoto", runtime: params.runtime, thread: params.thread, requestParams: mediaParams, send: (effectiveParams) => params.bot.api.sendPhoto(params.chatId, file, { ...effectiveParams }) }); if (firstDeliveredMessageId == null) firstDeliveredMessageId = result.message_id; markDelivered(params.progress); } else if (kind === "video") { const result = await sendTelegramWithThreadFallback({ operation: "sendVideo", runtime: params.runtime, thread: params.thread, requestParams: mediaParams, send: (effectiveParams) => params.bot.api.sendVideo(params.chatId, file, { ...effectiveParams }) }); if (firstDeliveredMessageId == null) firstDeliveredMessageId = result.message_id; markDelivered(params.progress); } else if (kind === "audio") { const { useVoice } = resolveTelegramVoiceSend({ wantsVoice: params.reply.audioAsVoice === true, contentType: media.contentType, fileName, logFallback: logVerbose }); if (useVoice) { const sendVoiceMedia = async (requestParams, shouldLog) => { const result = await sendTelegramWithThreadFallback({ operation: "sendVoice", runtime: params.runtime, thread: params.thread, requestParams, shouldLog, send: (effectiveParams) => params.bot.api.sendVoice(params.chatId, file, { ...effectiveParams }) }); if (firstDeliveredMessageId == null) firstDeliveredMessageId = result.message_id; markDelivered(params.progress); }; await params.onVoiceRecording?.(); try { await sendVoiceMedia(mediaParams, (err) => !isVoiceMessagesForbidden(err)); } catch (voiceErr) { if (isVoiceMessagesForbidden(voiceErr)) { const fallbackText = resolveVoiceFallbackText(params.reply); if (!fallbackText || !fallbackText.trim()) throw voiceErr; logVerbose("telegram sendVoice forbidden (recipient has voice messages blocked in privacy settings); falling back to text"); const voiceFallbackReplyTo = resolveReplyToForSend({ replyToId: params.replyToId, replyToMode: params.replyToMode, progress: params.progress }); const fallbackMessageId = await sendTelegramVoiceFallbackText({ bot: params.bot, chatId: params.chatId, runtime: params.runtime, text: fallbackText, chunkText: params.chunkText, replyToId: voiceFallbackReplyTo, replyQuoteMessageId: params.replyQuoteMessageId, replyQuotePosition: params.replyQuotePosition, replyQuoteEntities: params.replyQuoteEntities, thread: params.thread, linkPreview: params.linkPreview, silent: params.silent, replyMarkup: params.replyMarkup, replyQuoteText: params.replyQuoteText }); if (firstDeliveredMessageId == null) firstDeliveredMessageId = fallbackMessageId; visibleFallbackText = fallbackText; markReplyApplied(params.progress, voiceFallbackReplyTo); markDelivered(params.progress); continue; } if (isCaptionTooLong(voiceErr)) { logVerbose("telegram sendVoice caption too long; resending voice without caption + text separately"); const noCaptionParams = { ...mediaParams }; delete noCaptionParams.caption; delete noCaptionParams.parse_mode; await sendVoiceMedia(noCaptionParams); const fallbackText = resolveVoiceFallbackText(params.reply); if (fallbackText?.trim()) { await sendTelegramVoiceFallbackText({ bot: params.bot, chatId: params.chatId, runtime: params.runtime, text: fallbackText, chunkText: params.chunkText, replyToId: void 0, thread: params.thread, linkPreview: params.linkPreview, silent: params.silent, replyMarkup: params.replyMarkup }); visibleFallbackText = fallbackText; } markReplyApplied(params.progress, replyToMessageId); continue; } throw voiceErr; } } else { const result = await sendTelegramWithThreadFallback({ operation: "sendAudio", runtime: params.runtime, thread: params.thread, requestParams: mediaParams, send: (effectiveParams) => params.bot.api.sendAudio(params.chatId, file, { ...effectiveParams }) }); if (firstDeliveredMessageId == null) firstDeliveredMessageId = result.message_id; markDelivered(params.progress); } } else { const result = await sendTelegramWithThreadFallback({ operation: "sendDocument", runtime: params.runtime, thread: params.thread, requestParams: mediaParams, send: (effectiveParams) => params.bot.api.sendDocument(params.chatId, file, { ...effectiveParams }) }); if (firstDeliveredMessageId == null) firstDeliveredMessageId = result.message_id; markDelivered(params.progress); } markReplyApplied(params.progress, replyToMessageId); if (pendingFollowUpText && isFirstMedia) { await sendPendingFollowUpText({ bot: params.bot, chatId: params.chatId, runtime: params.runtime, thread: params.thread, chunkText: params.chunkText, text: pendingFollowUpText, replyMarkup: params.replyMarkup, linkPreview: params.linkPreview, silent: params.silent, replyToId: params.replyToId, replyToMode: params.replyToMode, progress: params.progress }); pendingFollowUpText = void 0; } } return { firstDeliveredMessageId, visibleFallbackText }; } async function maybePinFirstDeliveredMessage(params) { if (!(params.pin === true || typeof params.pin === "object" && params.pin.enabled) || typeof params.firstDeliveredMessageId !== "number") return; const notify = typeof params.pin === "object" && params.pin.notify === true; try { await params.bot.api.pinChatMessage(params.chatId, params.firstDeliveredMessageId, { disable_notification: !notify }); } catch (err) { logVerbose(`telegram pinChatMessage failed chat=${params.chatId} message=${params.firstDeliveredMessageId}: ${formatErrorMessage(err)}`); } } function buildTelegramSentHookContext(params) { return buildCanonicalSentMessageHookContext({ to: params.chatId, content: params.content, success: params.success, error: params.error, channelId: "telegram", accountId: params.accountId, conversationId: params.chatId, messageId: typeof params.messageId === "number" ? String(params.messageId) : void 0, isGroup: params.isGroup, groupId: params.groupId }); } function emitInternalMessageSentHook(params) { if (!params.sessionKeyForInternalHooks) return; const canonical = buildTelegramSentHookContext(params); fireAndForgetHook(triggerInternalHook(createInternalHookEvent("message", "sent", params.sessionKeyForInternalHooks, toInternalMessageSentContext(canonical))), "telegram: message:sent internal hook failed"); } function emitMessageSentHooks(params) { if (!params.enabled && !params.sessionKeyForInternalHooks) return; const canonical = buildTelegramSentHookContext(params); if (params.enabled) fireAndForgetHook(Promise.resolve(params.hookRunner.runMessageSent(toPluginMessageSentEvent(canonical), toPluginMessageContext(canonical))), "telegram: message_sent plugin hook failed"); emitInternalMessageSentHook(params); } function emitTelegramMessageSentHooks(params) { const hookRunner = getGlobalHookRunner(); emitMessageSentHooks({ ...params, hookRunner, enabled: hookRunner?.hasHooks("message_sent") ?? false }); } async function deliverReplies(params) { const progress = { hasReplied: false, hasDelivered: false, deliveredCount: 0 }; const mediaLoader = params.mediaLoader ?? loadWebMedia; const transcriptMirror = params.transcriptMirror; const deliveredContents = []; const hookRunner = getGlobalHookRunner(); const hasMessageSendingHooks = hookRunner?.hasHooks("message_sending") ?? false; const hasMessageSentHooks = hookRunner?.hasHooks("message_sent") ?? false; const chunkText = buildChunkTextResolver({ textLimit: params.textLimit, chunkMode: params.chunkMode ?? "length", tableMode: params.tableMode }); const candidateReplies = []; for (const reply of params.replies) { if (!reply || typeof reply !== "object") { params.runtime.error?.(danger("reply missing text/media")); continue; } candidateReplies.push(reply); } const normalizedReplies = projectOutboundPayloadPlanForDelivery(createOutboundPayloadPlan(candidateReplies, { cfg: params.cfg, sessionKey: params.policySessionKey ?? params.sessionKeyForInternalHooks, surface: "telegram" })); for (const originalReply of normalizedReplies) { let reply = originalReply; const mediaList = reply?.mediaUrls?.length ? reply.mediaUrls : reply?.mediaUrl ? [reply.mediaUrl] : []; const hasMedia = mediaList.length > 0; const presentation = normalizeMessagePresentation(reply?.presentation); const interactive = reply?.interactive; const resolvedReplyText = resolveTelegramInteractiveTextFallback({ text: reply?.text, interactive, presentation }) ?? reply?.text ?? ""; if (reply && resolvedReplyText !== (reply.text ?? "")) reply = { ...reply, text: resolvedReplyText }; if (!resolvedReplyText && !hasMedia) { if (reply?.audioAsVoice) { logVerbose("telegram reply has audioAsVoice without media/text; skipping"); continue; } params.runtime.error?.(danger("reply missing text/media")); continue; } const rawContent = resolvedReplyText; const spokenHookContent = !rawContent && reply.audioAsVoice === true && reply.spokenText?.trim() ? reply.spokenText : void 0; const hookContent = spokenHookContent ?? rawContent; const replyToId = params.replyToMode === "off" ? void 0 : resolveTelegramReplyId(reply.replyToId); const replyQuote = resolveReplyQuoteForSend({ replyToId, replyQuoteByMessageId: params.replyQuoteByMessageId, replyQuoteMessageId: params.replyQuoteMessageId, replyQuoteText: params.replyQuoteText, replyQuotePosition: params.replyQuotePosition, replyQuoteEntities: params.replyQuoteEntities }); if (hasMessageSendingHooks) { const hookResult = await hookRunner?.runMessageSending({ to: params.chatId, content: hookContent, replyToId, threadId: params.thread?.id, metadata: { channel: "telegram", mediaUrls: mediaList, threadId: params.thread?.id } }, { channelId: "telegram", accountId: params.accountId, conversationId: params.chatId }); if (hookResult?.cancel) continue; if (typeof hookResult?.content === "string" && hookResult.content !== hookContent) reply = spokenHookContent ? { ...reply, spokenText: hookResult.content } : { ...reply, text: hookResult.content }; } let contentForSentHook = reply.text || (reply.audioAsVoice === true ? resolveVoiceFallbackText(reply) : "") || ""; try { const deliveredCountBeforeReply = progress.deliveredCount; const telegramData = reply.channelData?.telegram; const replyMarkup = buildInlineKeyboard(resolveTelegramInlineButtons({ buttons: telegramData?.buttons, presentation, interactive })); let firstDeliveredMessageId; if (mediaList.length === 0) firstDeliveredMessageId = await deliverTextReply({ bot: params.bot, chatId: params.chatId, runtime: params.runtime, thread: params.thread, chunkText, replyText: reply.text || "", replyMarkup, replyQuoteMessageId: replyQuote.messageId, replyQuoteText: replyQuote.text, replyQuotePosition: replyQuote.position, replyQuoteEntities: replyQuote.entities, linkPreview: params.linkPreview, silent: params.silent, replyToId, replyToMode: params.replyToMode, progress }); else { const mediaDelivery = await deliverMediaReply({ reply, mediaList, bot: params.bot, chatId: params.chatId, runtime: params.runtime, thread: params.thread, tableMode: params.tableMode, mediaLocalRoots: params.mediaLocalRoots, mediaMaxBytes: params.mediaMaxBytes, chunkText, mediaLoader, onVoiceRecording: params.onVoiceRecording, linkPreview: params.linkPreview, silent: params.silent, replyQuoteMessageId: replyQuote.messageId, replyQuoteText: replyQuote.text, replyQuotePosition: replyQuote.position, replyQuoteEntities: replyQuote.entities, replyMarkup, replyToId, replyToMode: params.replyToMode, progress }); firstDeliveredMessageId = mediaDelivery.firstDeliveredMessageId; if (mediaDelivery.visibleFallbackText) contentForSentHook = mediaDelivery.visibleFallbackText; } await maybePinFirstDeliveredMessage({ pin: reply.delivery?.pin, bot: params.bot, chatId: params.chatId, runtime: params.runtime, firstDeliveredMessageId }); if (progress.deliveredCount > deliveredCountBeforeReply && transcriptMirror) deliveredContents.push({ text: contentForSentHook, mediaUrls: mediaList }); emitMessageSentHooks({ hookRunner, enabled: hasMessageSentHooks, sessionKeyForInternalHooks: params.sessionKeyForInternalHooks, chatId: params.chatId, accountId: params.accountId, content: contentForSentHook, success: progress.deliveredCount > deliveredCountBeforeReply, messageId: firstDeliveredMessageId, isGroup: params.mirrorIsGroup, groupId: params.mirrorGroupId }); } catch (error) { emitMessageSentHooks({ hookRunner, enabled: hasMessageSentHooks, sessionKeyForInternalHooks: params.sessionKeyForInternalHooks, chatId: params.chatId, accountId: params.accountId, content: contentForSentHook, success: false, error: formatErrorMessage(error), isGroup: params.mirrorIsGroup, groupId: params.mirrorGroupId }); throw error; } } if (progress.hasDelivered && transcriptMirror) { const text = deliveredContents.map((content) => content.text).filter(Boolean).join("\n\n"); const mediaUrls = deliveredContents.flatMap((content) => content.mediaUrls); if (text || mediaUrls.length > 0) try { await transcriptMirror({ text: text || void 0, mediaUrls: mediaUrls.length > 0 ? mediaUrls : void 0 }); } catch (mirrorErr) { logVerbose(`telegram transcriptMirror failed: ${formatErrorMessage(mirrorErr)}`); } } return { delivered: progress.hasDelivered }; } //#endregion //#region extensions/telegram/src/bot/delivery.resolve-media.ts const FILE_TOO_BIG_RE = /file is too big/i; const GrammyErrorCtor = typeof GrammyError === "function" ? GrammyError : void 0; function buildTelegramMediaSsrfPolicy(apiRoot, dangerouslyAllowPrivateNetwork) { const hostnames = ["api.telegram.org"]; let allowedHostnames; if (apiRoot) try { const customHost = new URL(apiRoot).hostname; if (customHost && !hostnames.includes(customHost)) { hostnames.push(customHost); allowedHostnames = [customHost]; } } catch (err) { logVerbose(`telegram: invalid apiRoot URL "${apiRoot}": ${String(err)}`); } return { hostnameAllowlist: hostnames, ...allowedHostnames ? { allowedHostnames } : {}, ...dangerouslyAllowPrivateNetwork ? { allowPrivateNetwork: true } : {}, allowRfc2544BenchmarkRange: true }; } /** * Returns true if the error is Telegram's "file is too big" error. * This happens when trying to download files >20MB via the Bot API. * Unlike network errors, this is a permanent error and should not be retried. */ function isFileTooBigError(err) { if (GrammyErrorCtor && err instanceof GrammyErrorCtor) return FILE_TOO_BIG_RE.test(err.description); return FILE_TOO_BIG_RE.test(formatErrorMessage(err)); } /** * Returns true if the error is a transient network error that should be retried. * Returns false for permanent errors like "file is too big" (400 Bad Request). */ function isRetryableGetFileError(err) { if (isFileTooBigError(err)) return false; return true; } function resolveMediaMetadata(msg) { return { fileRef: msg.photo?.[msg.photo.length - 1] ?? msg.video ?? msg.video_note ?? msg.document ?? msg.audio ?? msg.voice, fileName: msg.document?.file_name ?? msg.audio?.file_name ?? msg.video?.file_name ?? msg.animation?.file_name, mimeType: msg.audio?.mime_type ?? msg.voice?.mime_type ?? msg.video?.mime_type ?? msg.document?.mime_type ?? msg.animation?.mime_type }; } async function resolveTelegramFileWithRetry(ctx) { try { return await retryAsync(() => ctx.getFile(), { attempts: 3, minDelayMs: 1e3, maxDelayMs: 4e3, jitter: .2, label: "telegram:getFile", shouldRetry: isRetryableGetFileError, onRetry: ({ attempt, maxAttempts }) => logVerbose(`telegram: getFile retry ${attempt}/${maxAttempts}`) }); } catch (err) { if (isFileTooBigError(err)) { logVerbose(warn("telegram: getFile failed - file exceeds Telegram Bot API 20MB limit; skipping attachment")); return null; } logVerbose(`telegram: getFile failed after retries: ${String(err)}`); return null; } } function resolveRequiredTelegramTransport(transport) { if (transport) return transport; const resolvedFetch = globalThis.fetch; if (!resolvedFetch) throw new Error("fetch is not available; set channels.telegram.proxy in config"); return { fetch: resolvedFetch, sourceFetch: resolvedFetch, close: async () => {} }; } /** Default idle timeout for Telegram media downloads (30 seconds). */ const TELEGRAM_DOWNLOAD_IDLE_TIMEOUT_MS = 3e4; function usesTrustedTelegramExplicitProxy(transport) { return transport.dispatcherAttempts?.some((attempt) => attempt.dispatcherPolicy?.mode === "explicit-proxy") ?? false; } function resolveTrustedLocalTelegramRoot(filePath, trustedLocalFileRoots) { if (!path.isAbsolute(filePath)) return null; for (const rootDir of trustedLocalFileRoots ?? []) { const relativePath = path.relative(rootDir, filePath); if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) continue; return { rootDir, relativePath }; } return null; } async function downloadAndSaveTelegramFile(params) { const trustedLocalFile = resolveTrustedLocalTelegramRoot(params.filePath, params.trustedLocalFileRoots); if (trustedLocalFile) { let localFile; try { localFile = await (await root(trustedLocalFile.rootDir)).read(trustedLocalFile.relativePath, { maxBytes: params.maxBytes }); } catch (err) { throw new MediaFetchError("fetch_failed", `Failed to read local Telegram Bot API media from ${params.filePath}: ${formatErrorMessage(err)}`, { cause: err }); } return await saveMediaBuffer(localFile.buffer, params.mimeType, "inbound", params.maxBytes, params.telegramFileName ?? path.basename(localFile.realPath)); } if (path.isAbsolute(params.filePath)) throw new MediaFetchError("fetch_failed", `Telegram Bot API returned absolute file path ${params.filePath} outside trustedLocalFileRoots`); const transport = resolveRequiredTelegramTransport(params.transport); return await saveRemoteMedia({ url: `${resolveTelegramApiBase(params.apiRoot)}/file/bot${params.token}/${params.filePath}`, fetchImpl: transport.sourceFetch, dispatcherAttempts: transport.dispatcherAttempts, trustExplicitProxyDns: usesTrustedTelegramExplicitProxy(transport), shouldRetryFetchError: shouldRetryTelegramTransportFallback, retry: { attempts: 3, minDelayMs: 1e3, maxDelayMs: 4e3, jitter: .2, label: "telegram:media-download", onRetry: ({ attempt, maxAttempts }) => logVerbose(`telegram: media download retry ${attempt}/${maxAttempts}`) }, filePathHint: params.filePath, maxBytes: params.maxBytes, readIdleTimeoutMs: TELEGRAM_DOWNLOAD_IDLE_TIMEOUT_MS, ssrfPolicy: buildTelegramMediaSsrfPolicy(params.apiRoot, params.dangerouslyAllowPrivateNetwork), fallbackContentType: params.mimeType, originalFilename: params.telegramFileName }); } async function resolveStickerMedia(params) { const { msg, ctx, maxBytes, token, transport } = params; if (!msg.sticker) return; const sticker = msg.sticker; if (sticker.is_animated || sticker.is_video) { logVerbose("telegram: skipping animated/video sticker (only static stickers supported)"); return null; } if (!sticker.file_id) return null; try { const file = await resolveTelegramFileWithRetry(ctx); if (!file?.file_path) { logVerbose("telegram: getFile returned no file_path for sticker"); return null; } const saved = await downloadAndSaveTelegramFile({ filePath: file.file_path, token, transport, maxBytes, apiRoot: params.apiRoot, trustedLocalFileRoots: params.trustedLocalFileRoots, dangerouslyAllowPrivateNetwork: params.dangerouslyAllowPrivateNetwork }); const cached = sticker.file_unique_id ? getCachedSticker(sticker.file_unique_id) : null; if (cached) { logVerbose(`telegram: sticker cache hit for ${sticker.file_unique_id}`); const fileId = sticker.file_id ?? cached.fileId; const emoji = sticker.emoji ?? cached.emoji; const setName = sticker.set_name ?? cached.setName; if (fileId !== cached.fileId || emoji !== cached.emoji || setName !== cached.setName) cacheSticker({ ...cached, fileId, emoji, setName }); return { path: saved.path, contentType: saved.contentType, placeholder: "<media:sticker>", stickerMetadata: { emoji, setName, fileId, fileUniqueId: sticker.file_unique_id, cachedDescription: cached.description } }; } return { path: saved.path, contentType: saved.contentType, placeholder: "<media:sticker>", stickerMetadata: { emoji: sticker.emoji ?? void 0, setName: sticker.set_name ?? void 0, fileId: sticker.file_id, fileUniqueId: sticker.file_unique_id } }; } catch (err) { logVerbose(`telegram: failed to process sticker: ${String(err)}`); return null; } } async function resolveMedia(params) { const { ctx, maxBytes, token, transport, apiRoot, trustedLocalFileRoots, dangerouslyAllowPrivateNetwork } = params; const msg = ctx.message; const stickerResolved = await resolveStickerMedia({ msg, ctx, maxBytes, token, transport, apiRoot, trustedLocalFileRoots, dangerouslyAllowPrivateNetwork }); if (stickerResolved !== void 0) return stickerResolved; const metadata = resolveMediaMetadata(msg); if (!metadata.fileRef?.file_id) return null; const file = await resolveTelegramFileWithRetry(ctx); if (!file) return null; if (!file.file_path) throw new Error("Telegram getFile returned no file_path"); const saved = await downloadAndSaveTelegramFile({ filePath: file.file_path, token, transport, maxBytes, telegramFileName: metadata.fileName, mimeType: metadata.mimeType, apiRoot, trustedLocalFileRoots, dangerouslyAllowPrivateNetwork }); const placeholder = resolveTelegramMediaPlaceholder(msg) ?? "<media:document>"; return { path: saved.path, contentType: saved.contentType, placeholder }; } //#endregion export { emitTelegramMessageSentHooks as i, deliverReplies as n, emitInternalMessageSentHook as r, resolveMedia as t };