From c2e1ae68a9e1c4c48c64d759f12232b9e2bd3e64 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 02:15:06 +0000 Subject: [PATCH] refactor(telegram): split bot message context helpers --- src/telegram/bot-message-context.body.ts | 284 ++++++++++ src/telegram/bot-message-context.session.ts | 316 +++++++++++ src/telegram/bot-message-context.ts | 578 ++------------------ src/telegram/bot-message-context.types.ts | 65 +++ 4 files changed, 725 insertions(+), 518 deletions(-) create mode 100644 src/telegram/bot-message-context.body.ts create mode 100644 src/telegram/bot-message-context.session.ts create mode 100644 src/telegram/bot-message-context.types.ts diff --git a/src/telegram/bot-message-context.body.ts b/src/telegram/bot-message-context.body.ts new file mode 100644 index 00000000000..56b18f1b944 --- /dev/null +++ b/src/telegram/bot-message-context.body.ts @@ -0,0 +1,284 @@ +import { + findModelInCatalog, + loadModelCatalog, + modelSupportsVision, +} from "../agents/model-catalog.js"; +import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; +import { hasControlCommand } from "../auto-reply/command-detection.js"; +import { + recordPendingHistoryEntryIfEnabled, + type HistoryEntry, +} from "../auto-reply/reply/history.js"; +import { buildMentionRegexes, matchesMentionWithExplicit } from "../auto-reply/reply/mentions.js"; +import type { MsgContext } from "../auto-reply/templating.js"; +import { resolveControlCommandGate } from "../channels/command-gating.js"; +import { formatLocationText, type NormalizedLocation } from "../channels/location.js"; +import { logInboundDrop } from "../channels/logging.js"; +import { resolveMentionGatingWithBypass } from "../channels/mention-gating.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { + TelegramDirectConfig, + TelegramGroupConfig, + TelegramTopicConfig, +} from "../config/types.js"; +import { logVerbose } from "../globals.js"; +import type { NormalizedAllowFrom } from "./bot-access.js"; +import { isSenderAllowed } from "./bot-access.js"; +import type { + TelegramLogger, + TelegramMediaRef, + TelegramMessageContextOptions, +} from "./bot-message-context.types.js"; +import { + buildSenderLabel, + buildTelegramGroupPeerId, + expandTextLinks, + extractTelegramLocation, + getTelegramTextParts, + hasBotMention, + resolveTelegramMediaPlaceholder, +} from "./bot/helpers.js"; +import type { TelegramContext } from "./bot/types.js"; +import { isTelegramForumServiceMessage } from "./forum-service-message.js"; + +export type TelegramInboundBodyResult = { + bodyText: string; + rawBody: string; + historyKey?: string; + commandAuthorized: boolean; + effectiveWasMentioned: boolean; + canDetectMention: boolean; + shouldBypassMention: boolean; + stickerCacheHit: boolean; + locationData?: NormalizedLocation; +}; + +async function resolveStickerVisionSupport(params: { + cfg: OpenClawConfig; + agentId?: string; +}): Promise { + try { + const catalog = await loadModelCatalog({ config: params.cfg }); + const defaultModel = resolveDefaultModelForAgent({ + cfg: params.cfg, + agentId: params.agentId, + }); + const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model); + if (!entry) { + return false; + } + return modelSupportsVision(entry); + } catch { + return false; + } +} + +export async function resolveTelegramInboundBody(params: { + cfg: OpenClawConfig; + primaryCtx: TelegramContext; + msg: TelegramContext["message"]; + allMedia: TelegramMediaRef[]; + isGroup: boolean; + chatId: number | string; + senderId: string; + senderUsername: string; + resolvedThreadId?: number; + routeAgentId?: string; + effectiveGroupAllow: NormalizedAllowFrom; + effectiveDmAllow: NormalizedAllowFrom; + groupConfig?: TelegramGroupConfig | TelegramDirectConfig; + topicConfig?: TelegramTopicConfig; + requireMention?: boolean; + options?: TelegramMessageContextOptions; + groupHistories: Map; + historyLimit: number; + logger: TelegramLogger; +}): Promise { + const { + cfg, + primaryCtx, + msg, + allMedia, + isGroup, + chatId, + senderId, + senderUsername, + resolvedThreadId, + routeAgentId, + effectiveGroupAllow, + effectiveDmAllow, + groupConfig, + topicConfig, + requireMention, + options, + groupHistories, + historyLimit, + logger, + } = params; + const botUsername = primaryCtx.me?.username?.toLowerCase(); + const mentionRegexes = buildMentionRegexes(cfg, routeAgentId); + const messageTextParts = getTelegramTextParts(msg); + const allowForCommands = isGroup ? effectiveGroupAllow : effectiveDmAllow; + const senderAllowedForCommands = isSenderAllowed({ + allow: allowForCommands, + senderId, + senderUsername, + }); + const useAccessGroups = cfg.commands?.useAccessGroups !== false; + const hasControlCommandInMessage = hasControlCommand(messageTextParts.text, cfg, { + botUsername, + }); + const commandGate = resolveControlCommandGate({ + useAccessGroups, + authorizers: [{ configured: allowForCommands.hasEntries, allowed: senderAllowedForCommands }], + allowTextCommands: true, + hasControlCommand: hasControlCommandInMessage, + }); + const commandAuthorized = commandGate.commandAuthorized; + const historyKey = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : undefined; + + let placeholder = resolveTelegramMediaPlaceholder(msg) ?? ""; + const cachedStickerDescription = allMedia[0]?.stickerMetadata?.cachedDescription; + const stickerSupportsVision = msg.sticker + ? await resolveStickerVisionSupport({ cfg, agentId: routeAgentId }) + : false; + const stickerCacheHit = Boolean(cachedStickerDescription) && !stickerSupportsVision; + if (stickerCacheHit) { + const emoji = allMedia[0]?.stickerMetadata?.emoji; + const setName = allMedia[0]?.stickerMetadata?.setName; + const stickerContext = [emoji, setName ? `from "${setName}"` : null].filter(Boolean).join(" "); + placeholder = `[Sticker${stickerContext ? ` ${stickerContext}` : ""}] ${cachedStickerDescription}`; + } + + const locationData = extractTelegramLocation(msg); + const locationText = locationData ? formatLocationText(locationData) : undefined; + const rawText = expandTextLinks(messageTextParts.text, messageTextParts.entities).trim(); + const hasUserText = Boolean(rawText || locationText); + let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim(); + if (!rawBody) { + rawBody = placeholder; + } + if (!rawBody && allMedia.length === 0) { + return null; + } + + let bodyText = rawBody; + const hasAudio = allMedia.some((media) => media.contentType?.startsWith("audio/")); + const disableAudioPreflight = + (topicConfig?.disableAudioPreflight ?? + (groupConfig as TelegramGroupConfig | undefined)?.disableAudioPreflight) === true; + + let preflightTranscript: string | undefined; + const needsPreflightTranscription = + isGroup && + requireMention && + hasAudio && + !hasUserText && + mentionRegexes.length > 0 && + !disableAudioPreflight; + + if (needsPreflightTranscription) { + try { + const { transcribeFirstAudio } = await import("../media-understanding/audio-preflight.js"); + const tempCtx: MsgContext = { + MediaPaths: allMedia.length > 0 ? allMedia.map((m) => m.path) : undefined, + MediaTypes: + allMedia.length > 0 + ? (allMedia.map((m) => m.contentType).filter(Boolean) as string[]) + : undefined, + }; + preflightTranscript = await transcribeFirstAudio({ + ctx: tempCtx, + cfg, + agentDir: undefined, + }); + } catch (err) { + logVerbose(`telegram: audio preflight transcription failed: ${String(err)}`); + } + } + + if (hasAudio && bodyText === "" && preflightTranscript) { + bodyText = preflightTranscript; + } + + if (!bodyText && allMedia.length > 0) { + if (hasAudio) { + bodyText = preflightTranscript || ""; + } else { + bodyText = `${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`; + } + } + + const hasAnyMention = messageTextParts.entities.some((ent) => ent.type === "mention"); + const explicitlyMentioned = botUsername ? hasBotMention(msg, botUsername) : false; + const computedWasMentioned = matchesMentionWithExplicit({ + text: messageTextParts.text, + mentionRegexes, + explicit: { + hasAnyMention, + isExplicitlyMentioned: explicitlyMentioned, + canResolveExplicit: Boolean(botUsername), + }, + transcript: preflightTranscript, + }); + const wasMentioned = options?.forceWasMentioned === true ? true : computedWasMentioned; + + if (isGroup && commandGate.shouldBlock) { + logInboundDrop({ + log: logVerbose, + channel: "telegram", + reason: "control command (unauthorized)", + target: senderId ?? "unknown", + }); + return null; + } + + const botId = primaryCtx.me?.id; + const replyFromId = msg.reply_to_message?.from?.id; + const replyToBotMessage = botId != null && replyFromId === botId; + const isReplyToServiceMessage = + replyToBotMessage && isTelegramForumServiceMessage(msg.reply_to_message); + const implicitMention = replyToBotMessage && !isReplyToServiceMessage; + const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0; + const mentionGate = resolveMentionGatingWithBypass({ + isGroup, + requireMention: Boolean(requireMention), + canDetectMention, + wasMentioned, + implicitMention: isGroup && Boolean(requireMention) && implicitMention, + hasAnyMention, + allowTextCommands: true, + hasControlCommand: hasControlCommandInMessage, + commandAuthorized, + }); + const effectiveWasMentioned = mentionGate.effectiveWasMentioned; + if (isGroup && requireMention && canDetectMention && mentionGate.shouldSkip) { + logger.info({ chatId, reason: "no-mention" }, "skipping group message"); + recordPendingHistoryEntryIfEnabled({ + historyMap: groupHistories, + historyKey: historyKey ?? "", + limit: historyLimit, + entry: historyKey + ? { + sender: buildSenderLabel(msg, senderId || chatId), + body: rawBody, + timestamp: msg.date ? msg.date * 1000 : undefined, + messageId: typeof msg.message_id === "number" ? String(msg.message_id) : undefined, + } + : null, + }); + return null; + } + + return { + bodyText, + rawBody, + historyKey, + commandAuthorized, + effectiveWasMentioned, + canDetectMention, + shouldBypassMention: mentionGate.shouldBypassMention, + stickerCacheHit, + locationData: locationData ?? undefined, + }; +} diff --git a/src/telegram/bot-message-context.session.ts b/src/telegram/bot-message-context.session.ts new file mode 100644 index 00000000000..bde4ff3270b --- /dev/null +++ b/src/telegram/bot-message-context.session.ts @@ -0,0 +1,316 @@ +import { normalizeCommandBody } from "../auto-reply/commands-registry.js"; +import { formatInboundEnvelope, resolveEnvelopeFormatOptions } from "../auto-reply/envelope.js"; +import { + buildPendingHistoryContextFromMap, + type HistoryEntry, +} from "../auto-reply/reply/history.js"; +import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; +import { toLocationContext } from "../channels/location.js"; +import { recordInboundSession } from "../channels/session.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { readSessionUpdatedAt, resolveStorePath } from "../config/sessions.js"; +import type { + TelegramDirectConfig, + TelegramGroupConfig, + TelegramTopicConfig, +} from "../config/types.js"; +import { logVerbose, shouldLogVerbose } from "../globals.js"; +import type { ResolvedAgentRoute } from "../routing/resolve-route.js"; +import { resolveInboundLastRouteSessionKey } from "../routing/resolve-route.js"; +import { resolvePinnedMainDmOwnerFromAllowlist } from "../security/dm-policy-shared.js"; +import { normalizeAllowFrom } from "./bot-access.js"; +import type { + TelegramMediaRef, + TelegramMessageContextOptions, +} from "./bot-message-context.types.js"; +import { + buildGroupLabel, + buildSenderLabel, + buildSenderName, + buildTelegramGroupFrom, + describeReplyTarget, + normalizeForwardedContext, + type TelegramThreadSpec, +} from "./bot/helpers.js"; +import type { TelegramContext } from "./bot/types.js"; +import { resolveTelegramGroupPromptSettings } from "./group-config-helpers.js"; + +export async function buildTelegramInboundContextPayload(params: { + cfg: OpenClawConfig; + primaryCtx: TelegramContext; + msg: TelegramContext["message"]; + allMedia: TelegramMediaRef[]; + replyMedia: TelegramMediaRef[]; + isGroup: boolean; + isForum: boolean; + chatId: number | string; + senderId: string; + senderUsername: string; + resolvedThreadId?: number; + dmThreadId?: number; + threadSpec: TelegramThreadSpec; + route: ResolvedAgentRoute; + rawBody: string; + bodyText: string; + historyKey?: string; + historyLimit: number; + groupHistories: Map; + groupConfig?: TelegramGroupConfig | TelegramDirectConfig; + topicConfig?: TelegramTopicConfig; + stickerCacheHit: boolean; + effectiveWasMentioned: boolean; + commandAuthorized: boolean; + locationData?: import("../channels/location.js").NormalizedLocation; + options?: TelegramMessageContextOptions; + dmAllowFrom?: Array; +}): Promise<{ + ctxPayload: ReturnType; + skillFilter: string[] | undefined; +}> { + const { + cfg, + primaryCtx, + msg, + allMedia, + replyMedia, + isGroup, + isForum, + chatId, + senderId, + senderUsername, + resolvedThreadId, + dmThreadId, + threadSpec, + route, + rawBody, + bodyText, + historyKey, + historyLimit, + groupHistories, + groupConfig, + topicConfig, + stickerCacheHit, + effectiveWasMentioned, + commandAuthorized, + locationData, + options, + dmAllowFrom, + } = params; + const replyTarget = describeReplyTarget(msg); + const forwardOrigin = normalizeForwardedContext(msg); + const replyForwardAnnotation = replyTarget?.forwardedFrom + ? `[Forwarded from ${replyTarget.forwardedFrom.from}${ + replyTarget.forwardedFrom.date + ? ` at ${new Date(replyTarget.forwardedFrom.date * 1000).toISOString()}` + : "" + }]\n` + : ""; + const replySuffix = replyTarget + ? replyTarget.kind === "quote" + ? `\n\n[Quoting ${replyTarget.sender}${ + replyTarget.id ? ` id:${replyTarget.id}` : "" + }]\n${replyForwardAnnotation}"${replyTarget.body}"\n[/Quoting]` + : `\n\n[Replying to ${replyTarget.sender}${ + replyTarget.id ? ` id:${replyTarget.id}` : "" + }]\n${replyForwardAnnotation}${replyTarget.body}\n[/Replying]` + : ""; + const forwardPrefix = forwardOrigin + ? `[Forwarded from ${forwardOrigin.from}${ + forwardOrigin.date ? ` at ${new Date(forwardOrigin.date * 1000).toISOString()}` : "" + }]\n` + : ""; + const groupLabel = isGroup ? buildGroupLabel(msg, chatId, resolvedThreadId) : undefined; + const senderName = buildSenderName(msg); + const conversationLabel = isGroup + ? (groupLabel ?? `group:${chatId}`) + : buildSenderLabel(msg, senderId || chatId); + const storePath = resolveStorePath(cfg.session?.store, { + agentId: route.agentId, + }); + const envelopeOptions = resolveEnvelopeFormatOptions(cfg); + const previousTimestamp = readSessionUpdatedAt({ + storePath, + sessionKey: route.sessionKey, + }); + const body = formatInboundEnvelope({ + channel: "Telegram", + from: conversationLabel, + timestamp: msg.date ? msg.date * 1000 : undefined, + body: `${forwardPrefix}${bodyText}${replySuffix}`, + chatType: isGroup ? "group" : "direct", + sender: { + name: senderName, + username: senderUsername || undefined, + id: senderId || undefined, + }, + previousTimestamp, + envelope: envelopeOptions, + }); + let combinedBody = body; + if (isGroup && historyKey && historyLimit > 0) { + combinedBody = buildPendingHistoryContextFromMap({ + historyMap: groupHistories, + historyKey, + limit: historyLimit, + currentMessage: combinedBody, + formatEntry: (entry) => + formatInboundEnvelope({ + channel: "Telegram", + from: groupLabel ?? `group:${chatId}`, + timestamp: entry.timestamp, + body: `${entry.body} [id:${entry.messageId ?? "unknown"} chat:${chatId}]`, + chatType: "group", + senderLabel: entry.sender, + envelope: envelopeOptions, + }), + }); + } + + const { skillFilter, groupSystemPrompt } = resolveTelegramGroupPromptSettings({ + groupConfig, + topicConfig, + }); + const commandBody = normalizeCommandBody(rawBody, { + botUsername: primaryCtx.me?.username?.toLowerCase(), + }); + const inboundHistory = + isGroup && historyKey && historyLimit > 0 + ? (groupHistories.get(historyKey) ?? []).map((entry) => ({ + sender: entry.sender, + body: entry.body, + timestamp: entry.timestamp, + })) + : undefined; + const currentMediaForContext = stickerCacheHit ? [] : allMedia; + const contextMedia = [...currentMediaForContext, ...replyMedia]; + const ctxPayload = finalizeInboundContext({ + Body: combinedBody, + BodyForAgent: bodyText, + InboundHistory: inboundHistory, + RawBody: rawBody, + CommandBody: commandBody, + From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`, + To: `telegram:${chatId}`, + SessionKey: route.sessionKey, + AccountId: route.accountId, + ChatType: isGroup ? "group" : "direct", + ConversationLabel: conversationLabel, + GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined, + GroupSystemPrompt: isGroup || (!isGroup && groupConfig) ? groupSystemPrompt : undefined, + SenderName: senderName, + SenderId: senderId || undefined, + SenderUsername: senderUsername || undefined, + Provider: "telegram", + Surface: "telegram", + MessageSid: options?.messageIdOverride ?? String(msg.message_id), + ReplyToId: replyTarget?.id, + ReplyToBody: replyTarget?.body, + ReplyToSender: replyTarget?.sender, + ReplyToIsQuote: replyTarget?.kind === "quote" ? true : undefined, + ReplyToForwardedFrom: replyTarget?.forwardedFrom?.from, + ReplyToForwardedFromType: replyTarget?.forwardedFrom?.fromType, + ReplyToForwardedFromId: replyTarget?.forwardedFrom?.fromId, + ReplyToForwardedFromUsername: replyTarget?.forwardedFrom?.fromUsername, + ReplyToForwardedFromTitle: replyTarget?.forwardedFrom?.fromTitle, + ReplyToForwardedDate: replyTarget?.forwardedFrom?.date + ? replyTarget.forwardedFrom.date * 1000 + : undefined, + ForwardedFrom: forwardOrigin?.from, + ForwardedFromType: forwardOrigin?.fromType, + ForwardedFromId: forwardOrigin?.fromId, + ForwardedFromUsername: forwardOrigin?.fromUsername, + ForwardedFromTitle: forwardOrigin?.fromTitle, + ForwardedFromSignature: forwardOrigin?.fromSignature, + ForwardedFromChatType: forwardOrigin?.fromChatType, + ForwardedFromMessageId: forwardOrigin?.fromMessageId, + ForwardedDate: forwardOrigin?.date ? forwardOrigin.date * 1000 : undefined, + Timestamp: msg.date ? msg.date * 1000 : undefined, + WasMentioned: isGroup ? effectiveWasMentioned : undefined, + MediaPath: contextMedia.length > 0 ? contextMedia[0]?.path : undefined, + MediaType: contextMedia.length > 0 ? contextMedia[0]?.contentType : undefined, + MediaUrl: contextMedia.length > 0 ? contextMedia[0]?.path : undefined, + MediaPaths: contextMedia.length > 0 ? contextMedia.map((m) => m.path) : undefined, + MediaUrls: contextMedia.length > 0 ? contextMedia.map((m) => m.path) : undefined, + MediaTypes: + contextMedia.length > 0 + ? (contextMedia.map((m) => m.contentType).filter(Boolean) as string[]) + : undefined, + Sticker: allMedia[0]?.stickerMetadata, + StickerMediaIncluded: allMedia[0]?.stickerMetadata ? !stickerCacheHit : undefined, + ...(locationData ? toLocationContext(locationData) : undefined), + CommandAuthorized: commandAuthorized, + MessageThreadId: threadSpec.id, + IsForum: isForum, + OriginatingChannel: "telegram" as const, + OriginatingTo: `telegram:${chatId}`, + }); + + const pinnedMainDmOwner = !isGroup + ? resolvePinnedMainDmOwnerFromAllowlist({ + dmScope: cfg.session?.dmScope, + allowFrom: dmAllowFrom, + normalizeEntry: (entry) => normalizeAllowFrom([entry]).entries[0], + }) + : null; + const updateLastRouteSessionKey = resolveInboundLastRouteSessionKey({ + route, + sessionKey: route.sessionKey, + }); + + await recordInboundSession({ + storePath, + sessionKey: ctxPayload.SessionKey ?? route.sessionKey, + ctx: ctxPayload, + updateLastRoute: !isGroup + ? { + sessionKey: updateLastRouteSessionKey, + channel: "telegram", + to: `telegram:${chatId}`, + accountId: route.accountId, + threadId: dmThreadId != null ? String(dmThreadId) : undefined, + mainDmOwnerPin: + updateLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner && senderId + ? { + ownerRecipient: pinnedMainDmOwner, + senderRecipient: senderId, + onSkip: ({ ownerRecipient, senderRecipient }) => { + logVerbose( + `telegram: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`, + ); + }, + } + : undefined, + } + : undefined, + onRecordError: (err) => { + logVerbose(`telegram: failed updating session meta: ${String(err)}`); + }, + }); + + if (replyTarget && shouldLogVerbose()) { + const preview = replyTarget.body.replace(/\s+/g, " ").slice(0, 120); + logVerbose( + `telegram reply-context: replyToId=${replyTarget.id} replyToSender=${replyTarget.sender} replyToBody="${preview}"`, + ); + } + + if (forwardOrigin && shouldLogVerbose()) { + logVerbose( + `telegram forward-context: forwardedFrom="${forwardOrigin.from}" type=${forwardOrigin.fromType}`, + ); + } + + if (shouldLogVerbose()) { + const preview = body.slice(0, 200).replace(/\n/g, "\\n"); + const mediaInfo = allMedia.length > 1 ? ` mediaCount=${allMedia.length}` : ""; + const topicInfo = resolvedThreadId != null ? ` topic=${resolvedThreadId}` : ""; + logVerbose( + `telegram inbound: chatId=${chatId} from=${ctxPayload.From} len=${body.length}${mediaInfo}${topicInfo} preview="${preview}"`, + ); + } + + return { + ctxPayload, + skillFilter, + }; +} diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 4b5dae6789c..19962121628 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -1,81 +1,30 @@ -import type { Bot } from "grammy"; import { ensureConfiguredAcpRouteReady } from "../acp/persistent-bindings.route.js"; import { resolveAckReaction } from "../agents/identity.js"; -import { - findModelInCatalog, - loadModelCatalog, - modelSupportsVision, -} from "../agents/model-catalog.js"; -import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; -import { hasControlCommand } from "../auto-reply/command-detection.js"; -import { normalizeCommandBody } from "../auto-reply/commands-registry.js"; -import { formatInboundEnvelope, resolveEnvelopeFormatOptions } from "../auto-reply/envelope.js"; -import { - buildPendingHistoryContextFromMap, - recordPendingHistoryEntryIfEnabled, - type HistoryEntry, -} from "../auto-reply/reply/history.js"; -import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; -import { buildMentionRegexes, matchesMentionWithExplicit } from "../auto-reply/reply/mentions.js"; -import type { MsgContext } from "../auto-reply/templating.js"; import { shouldAckReaction as shouldAckReactionGate } from "../channels/ack-reactions.js"; -import { resolveControlCommandGate } from "../channels/command-gating.js"; -import { formatLocationText, toLocationContext } from "../channels/location.js"; import { logInboundDrop } from "../channels/logging.js"; -import { resolveMentionGatingWithBypass } from "../channels/mention-gating.js"; -import { recordInboundSession } from "../channels/session.js"; import { createStatusReactionController, type StatusReactionController, } from "../channels/status-reactions.js"; -import type { OpenClawConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js"; -import { readSessionUpdatedAt, resolveStorePath } from "../config/sessions.js"; -import type { - DmPolicy, - TelegramDirectConfig, - TelegramGroupConfig, - TelegramTopicConfig, -} from "../config/types.js"; -import { logVerbose, shouldLogVerbose } from "../globals.js"; +import type { TelegramDirectConfig, TelegramGroupConfig } from "../config/types.js"; +import { logVerbose } from "../globals.js"; import { recordChannelActivity } from "../infra/channel-activity.js"; -import { - buildAgentSessionKey, - deriveLastRoutePolicy, - resolveInboundLastRouteSessionKey, -} from "../routing/resolve-route.js"; +import { buildAgentSessionKey, deriveLastRoutePolicy } from "../routing/resolve-route.js"; import { DEFAULT_ACCOUNT_ID, resolveThreadSessionKeys } from "../routing/session-key.js"; -import { resolvePinnedMainDmOwnerFromAllowlist } from "../security/dm-policy-shared.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; +import { firstDefined, normalizeAllowFrom, normalizeDmAllowFromWithStore } from "./bot-access.js"; +import { resolveTelegramInboundBody } from "./bot-message-context.body.js"; +import { buildTelegramInboundContextPayload } from "./bot-message-context.session.js"; +import type { BuildTelegramMessageContextParams } from "./bot-message-context.types.js"; import { - firstDefined, - isSenderAllowed, - normalizeAllowFrom, - normalizeDmAllowFromWithStore, -} from "./bot-access.js"; -import { - buildGroupLabel, - buildSenderLabel, - buildSenderName, - buildTelegramGroupFrom, - buildTelegramGroupPeerId, buildTypingThreadParams, - expandTextLinks, - describeReplyTarget, - extractTelegramLocation, - getTelegramTextParts, - hasBotMention, - normalizeForwardedContext, resolveTelegramDirectPeerId, - resolveTelegramMediaPlaceholder, resolveTelegramThreadSpec, } from "./bot/helpers.js"; -import type { StickerMetadata, TelegramContext } from "./bot/types.js"; import { resolveTelegramConversationRoute } from "./conversation-route.js"; import { enforceTelegramDmAccess } from "./dm-access.js"; -import { isTelegramForumServiceMessage } from "./forum-service-message.js"; import { evaluateTelegramGroupBaseAccess } from "./group-access.js"; -import { resolveTelegramGroupPromptSettings } from "./group-config-helpers.js"; import { buildTelegramStatusReactionVariants, resolveTelegramAllowedEmojiReactions, @@ -83,80 +32,10 @@ import { resolveTelegramStatusReactionEmojis, } from "./status-reaction-variants.js"; -export type TelegramMediaRef = { - path: string; - contentType?: string; - stickerMetadata?: StickerMetadata; -}; - -type TelegramMessageContextOptions = { - forceWasMentioned?: boolean; - messageIdOverride?: string; -}; - -type TelegramLogger = { - info: (obj: Record, msg: string) => void; -}; - -type ResolveTelegramGroupConfig = ( - chatId: string | number, - messageThreadId?: number, -) => { - groupConfig?: TelegramGroupConfig | TelegramDirectConfig; - topicConfig?: TelegramTopicConfig; -}; - -type ResolveGroupActivation = (params: { - chatId: string | number; - agentId?: string; - messageThreadId?: number; - sessionKey?: string; -}) => boolean | undefined; - -type ResolveGroupRequireMention = (chatId: string | number) => boolean; - -export type BuildTelegramMessageContextParams = { - primaryCtx: TelegramContext; - allMedia: TelegramMediaRef[]; - replyMedia?: TelegramMediaRef[]; - storeAllowFrom: string[]; - options?: TelegramMessageContextOptions; - bot: Bot; - cfg: OpenClawConfig; - account: { accountId: string }; - historyLimit: number; - groupHistories: Map; - dmPolicy: DmPolicy; - allowFrom?: Array; - groupAllowFrom?: Array; - ackReactionScope: "off" | "none" | "group-mentions" | "group-all" | "direct" | "all"; - logger: TelegramLogger; - resolveGroupActivation: ResolveGroupActivation; - resolveGroupRequireMention: ResolveGroupRequireMention; - resolveTelegramGroupConfig: ResolveTelegramGroupConfig; - /** Global (per-account) handler for sendChatAction 401 backoff (#27092). */ - sendChatActionHandler: import("./sendchataction-401-backoff.js").TelegramSendChatActionHandler; -}; - -async function resolveStickerVisionSupport(params: { - cfg: OpenClawConfig; - agentId?: string; -}): Promise { - try { - const catalog = await loadModelCatalog({ config: params.cfg }); - const defaultModel = resolveDefaultModelForAgent({ - cfg: params.cfg, - agentId: params.agentId, - }); - const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model); - if (!entry) { - return false; - } - return modelSupportsVision(entry); - } catch { - return false; - } -} +export type { + BuildTelegramMessageContextParams, + TelegramMediaRef, +} from "./bot-message-context.types.js"; export const buildTelegramMessageContext = async ({ primaryCtx, @@ -375,7 +254,6 @@ export const buildTelegramMessageContext = async ({ mainSessionKey: route.mainSessionKey, }), }; - const mentionRegexes = buildMentionRegexes(cfg, route.agentId); // Compute requireMention after access checks and final route selection. const activationOverride = resolveGroupActivation({ chatId, @@ -397,179 +275,31 @@ export const buildTelegramMessageContext = async ({ direction: "inbound", }); - const botUsername = primaryCtx.me?.username?.toLowerCase(); - const messageTextParts = getTelegramTextParts(msg); - const allowForCommands = isGroup ? effectiveGroupAllow : effectiveDmAllow; - const senderAllowedForCommands = isSenderAllowed({ - allow: allowForCommands, + const bodyResult = await resolveTelegramInboundBody({ + cfg, + primaryCtx, + msg, + allMedia, + isGroup, + chatId, senderId, senderUsername, + resolvedThreadId, + routeAgentId: route.agentId, + effectiveGroupAllow, + effectiveDmAllow, + groupConfig, + topicConfig, + requireMention, + options, + groupHistories, + historyLimit, + logger, }); - const useAccessGroups = cfg.commands?.useAccessGroups !== false; - const hasControlCommandInMessage = hasControlCommand(messageTextParts.text, cfg, { - botUsername, - }); - const commandGate = resolveControlCommandGate({ - useAccessGroups, - authorizers: [{ configured: allowForCommands.hasEntries, allowed: senderAllowedForCommands }], - allowTextCommands: true, - hasControlCommand: hasControlCommandInMessage, - }); - const commandAuthorized = commandGate.commandAuthorized; - const historyKey = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : undefined; - - let placeholder = resolveTelegramMediaPlaceholder(msg) ?? ""; - - // Check if sticker has a cached description - if so, use it instead of sending the image - const cachedStickerDescription = allMedia[0]?.stickerMetadata?.cachedDescription; - const stickerSupportsVision = msg.sticker - ? await resolveStickerVisionSupport({ cfg, agentId: route.agentId }) - : false; - const stickerCacheHit = Boolean(cachedStickerDescription) && !stickerSupportsVision; - if (stickerCacheHit) { - // Format cached description with sticker context - const emoji = allMedia[0]?.stickerMetadata?.emoji; - const setName = allMedia[0]?.stickerMetadata?.setName; - const stickerContext = [emoji, setName ? `from "${setName}"` : null].filter(Boolean).join(" "); - placeholder = `[Sticker${stickerContext ? ` ${stickerContext}` : ""}] ${cachedStickerDescription}`; - } - - const locationData = extractTelegramLocation(msg); - const locationText = locationData ? formatLocationText(locationData) : undefined; - const rawText = expandTextLinks(messageTextParts.text, messageTextParts.entities).trim(); - const hasUserText = Boolean(rawText || locationText); - let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim(); - if (!rawBody) { - rawBody = placeholder; - } - if (!rawBody && allMedia.length === 0) { + if (!bodyResult) { return null; } - let bodyText = rawBody; - const hasAudio = allMedia.some((media) => media.contentType?.startsWith("audio/")); - - const disableAudioPreflight = - firstDefined( - topicConfig?.disableAudioPreflight, - (groupConfig as TelegramGroupConfig | undefined)?.disableAudioPreflight, - ) === true; - - // Preflight audio transcription for mention detection in groups - // This allows voice notes to be checked for mentions before being dropped - let preflightTranscript: string | undefined; - const needsPreflightTranscription = - isGroup && - requireMention && - hasAudio && - !hasUserText && - mentionRegexes.length > 0 && - !disableAudioPreflight; - - if (needsPreflightTranscription) { - try { - const { transcribeFirstAudio } = await import("../media-understanding/audio-preflight.js"); - // Build a minimal context for transcription - const tempCtx: MsgContext = { - MediaPaths: allMedia.length > 0 ? allMedia.map((m) => m.path) : undefined, - MediaTypes: - allMedia.length > 0 - ? (allMedia.map((m) => m.contentType).filter(Boolean) as string[]) - : undefined, - }; - preflightTranscript = await transcribeFirstAudio({ - ctx: tempCtx, - cfg, - agentDir: undefined, - }); - } catch (err) { - logVerbose(`telegram: audio preflight transcription failed: ${String(err)}`); - } - } - - // Replace audio placeholder with transcript when preflight succeeds. - if (hasAudio && bodyText === "" && preflightTranscript) { - bodyText = preflightTranscript; - } - - // Build bodyText fallback for messages that still have no text. - if (!bodyText && allMedia.length > 0) { - if (hasAudio) { - bodyText = preflightTranscript || ""; - } else { - bodyText = `${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`; - } - } - - const hasAnyMention = messageTextParts.entities.some((ent) => ent.type === "mention"); - const explicitlyMentioned = botUsername ? hasBotMention(msg, botUsername) : false; - - const computedWasMentioned = matchesMentionWithExplicit({ - text: messageTextParts.text, - mentionRegexes, - explicit: { - hasAnyMention, - isExplicitlyMentioned: explicitlyMentioned, - canResolveExplicit: Boolean(botUsername), - }, - transcript: preflightTranscript, - }); - const wasMentioned = options?.forceWasMentioned === true ? true : computedWasMentioned; - if (isGroup && commandGate.shouldBlock) { - logInboundDrop({ - log: logVerbose, - channel: "telegram", - reason: "control command (unauthorized)", - target: senderId ?? "unknown", - }); - return null; - } - // Reply-chain detection: replying to a bot message acts like an implicit mention. - // Exclude forum-topic service messages (auto-generated "Topic created" etc. messages - // by the bot) so that every message inside a bot-created topic does not incorrectly - // bypass requireMention (#32256). - // We detect service messages by the presence of Telegram's forum_topic_* fields - // rather than by the absence of text/caption, because legitimate bot media messages - // (stickers, voice notes, captionless photos) also lack text/caption. - const botId = primaryCtx.me?.id; - const replyFromId = msg.reply_to_message?.from?.id; - const replyToBotMessage = botId != null && replyFromId === botId; - const isReplyToServiceMessage = - replyToBotMessage && isTelegramForumServiceMessage(msg.reply_to_message); - const implicitMention = replyToBotMessage && !isReplyToServiceMessage; - const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0; - const mentionGate = resolveMentionGatingWithBypass({ - isGroup, - requireMention: Boolean(requireMention), - canDetectMention, - wasMentioned, - implicitMention: isGroup && Boolean(requireMention) && implicitMention, - hasAnyMention, - allowTextCommands: true, - hasControlCommand: hasControlCommandInMessage, - commandAuthorized, - }); - const effectiveWasMentioned = mentionGate.effectiveWasMentioned; - if (isGroup && requireMention && canDetectMention) { - if (mentionGate.shouldSkip) { - logger.info({ chatId, reason: "no-mention" }, "skipping group message"); - recordPendingHistoryEntryIfEnabled({ - historyMap: groupHistories, - historyKey: historyKey ?? "", - limit: historyLimit, - entry: historyKey - ? { - sender: buildSenderLabel(msg, senderId || chatId), - body: rawBody, - timestamp: msg.date ? msg.date * 1000 : undefined, - messageId: typeof msg.message_id === "number" ? String(msg.message_id) : undefined, - } - : null, - }); - return null; - } - } - if (!(await ensureConfiguredBindingReady())) { return null; } @@ -589,9 +319,9 @@ export const buildTelegramMessageContext = async ({ isGroup, isMentionableGroup: isGroup, requireMention: Boolean(requireMention), - canDetectMention, - effectiveWasMentioned, - shouldBypassMention: mentionGate.shouldBypassMention, + canDetectMention: bodyResult.canDetectMention, + effectiveWasMentioned: bodyResult.effectiveWasMentioned, + shouldBypassMention: bodyResult.shouldBypassMention, }), ); const api = bot.api as unknown as { @@ -683,223 +413,35 @@ export const buildTelegramMessageContext = async ({ ) : null; - const replyTarget = describeReplyTarget(msg); - const forwardOrigin = normalizeForwardedContext(msg); - // Build forward annotation for reply target if it was itself a forwarded message (issue #9619) - const replyForwardAnnotation = replyTarget?.forwardedFrom - ? `[Forwarded from ${replyTarget.forwardedFrom.from}${ - replyTarget.forwardedFrom.date - ? ` at ${new Date(replyTarget.forwardedFrom.date * 1000).toISOString()}` - : "" - }]\n` - : ""; - const replySuffix = replyTarget - ? replyTarget.kind === "quote" - ? `\n\n[Quoting ${replyTarget.sender}${ - replyTarget.id ? ` id:${replyTarget.id}` : "" - }]\n${replyForwardAnnotation}"${replyTarget.body}"\n[/Quoting]` - : `\n\n[Replying to ${replyTarget.sender}${ - replyTarget.id ? ` id:${replyTarget.id}` : "" - }]\n${replyForwardAnnotation}${replyTarget.body}\n[/Replying]` - : ""; - const forwardPrefix = forwardOrigin - ? `[Forwarded from ${forwardOrigin.from}${ - forwardOrigin.date ? ` at ${new Date(forwardOrigin.date * 1000).toISOString()}` : "" - }]\n` - : ""; - const groupLabel = isGroup ? buildGroupLabel(msg, chatId, resolvedThreadId) : undefined; - const senderName = buildSenderName(msg); - const conversationLabel = isGroup - ? (groupLabel ?? `group:${chatId}`) - : buildSenderLabel(msg, senderId || chatId); - const storePath = resolveStorePath(cfg.session?.store, { - agentId: route.agentId, - }); - const envelopeOptions = resolveEnvelopeFormatOptions(cfg); - const previousTimestamp = readSessionUpdatedAt({ - storePath, - sessionKey: sessionKey, - }); - const body = formatInboundEnvelope({ - channel: "Telegram", - from: conversationLabel, - timestamp: msg.date ? msg.date * 1000 : undefined, - body: `${forwardPrefix}${bodyText}${replySuffix}`, - chatType: isGroup ? "group" : "direct", - sender: { - name: senderName, - username: senderUsername || undefined, - id: senderId || undefined, - }, - previousTimestamp, - envelope: envelopeOptions, - }); - let combinedBody = body; - if (isGroup && historyKey && historyLimit > 0) { - combinedBody = buildPendingHistoryContextFromMap({ - historyMap: groupHistories, - historyKey, - limit: historyLimit, - currentMessage: combinedBody, - formatEntry: (entry) => - formatInboundEnvelope({ - channel: "Telegram", - from: groupLabel ?? `group:${chatId}`, - timestamp: entry.timestamp, - body: `${entry.body} [id:${entry.messageId ?? "unknown"} chat:${chatId}]`, - chatType: "group", - senderLabel: entry.sender, - envelope: envelopeOptions, - }), - }); - } - - const { skillFilter, groupSystemPrompt } = resolveTelegramGroupPromptSettings({ + const { ctxPayload, skillFilter } = await buildTelegramInboundContextPayload({ + cfg, + primaryCtx, + msg, + allMedia, + replyMedia, + isGroup, + isForum, + chatId, + senderId, + senderUsername, + resolvedThreadId, + dmThreadId, + threadSpec, + route, + rawBody: bodyResult.rawBody, + bodyText: bodyResult.bodyText, + historyKey: bodyResult.historyKey, + historyLimit, + groupHistories, groupConfig, topicConfig, + stickerCacheHit: bodyResult.stickerCacheHit, + effectiveWasMentioned: bodyResult.effectiveWasMentioned, + locationData: bodyResult.locationData, + options, + dmAllowFrom, + commandAuthorized: bodyResult.commandAuthorized, }); - const commandBody = normalizeCommandBody(rawBody, { botUsername }); - const inboundHistory = - isGroup && historyKey && historyLimit > 0 - ? (groupHistories.get(historyKey) ?? []).map((entry) => ({ - sender: entry.sender, - body: entry.body, - timestamp: entry.timestamp, - })) - : undefined; - const currentMediaForContext = stickerCacheHit ? [] : allMedia; - const contextMedia = [...currentMediaForContext, ...replyMedia]; - const ctxPayload = finalizeInboundContext({ - Body: combinedBody, - // Agent prompt should be the raw user text only; metadata/context is provided via system prompt. - BodyForAgent: bodyText, - InboundHistory: inboundHistory, - RawBody: rawBody, - CommandBody: commandBody, - From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`, - To: `telegram:${chatId}`, - SessionKey: sessionKey, - AccountId: route.accountId, - ChatType: isGroup ? "group" : "direct", - ConversationLabel: conversationLabel, - GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined, - GroupSystemPrompt: isGroup || (!isGroup && groupConfig) ? groupSystemPrompt : undefined, - SenderName: senderName, - SenderId: senderId || undefined, - SenderUsername: senderUsername || undefined, - Provider: "telegram", - Surface: "telegram", - MessageSid: options?.messageIdOverride ?? String(msg.message_id), - ReplyToId: replyTarget?.id, - ReplyToBody: replyTarget?.body, - ReplyToSender: replyTarget?.sender, - ReplyToIsQuote: replyTarget?.kind === "quote" ? true : undefined, - // Forward context from reply target (issue #9619: forward + comment bundling) - ReplyToForwardedFrom: replyTarget?.forwardedFrom?.from, - ReplyToForwardedFromType: replyTarget?.forwardedFrom?.fromType, - ReplyToForwardedFromId: replyTarget?.forwardedFrom?.fromId, - ReplyToForwardedFromUsername: replyTarget?.forwardedFrom?.fromUsername, - ReplyToForwardedFromTitle: replyTarget?.forwardedFrom?.fromTitle, - ReplyToForwardedDate: replyTarget?.forwardedFrom?.date - ? replyTarget.forwardedFrom.date * 1000 - : undefined, - ForwardedFrom: forwardOrigin?.from, - ForwardedFromType: forwardOrigin?.fromType, - ForwardedFromId: forwardOrigin?.fromId, - ForwardedFromUsername: forwardOrigin?.fromUsername, - ForwardedFromTitle: forwardOrigin?.fromTitle, - ForwardedFromSignature: forwardOrigin?.fromSignature, - ForwardedFromChatType: forwardOrigin?.fromChatType, - ForwardedFromMessageId: forwardOrigin?.fromMessageId, - ForwardedDate: forwardOrigin?.date ? forwardOrigin.date * 1000 : undefined, - Timestamp: msg.date ? msg.date * 1000 : undefined, - WasMentioned: isGroup ? effectiveWasMentioned : undefined, - // Filter out cached stickers from current-message media; reply media is still valid context. - MediaPath: contextMedia.length > 0 ? contextMedia[0]?.path : undefined, - MediaType: contextMedia.length > 0 ? contextMedia[0]?.contentType : undefined, - MediaUrl: contextMedia.length > 0 ? contextMedia[0]?.path : undefined, - MediaPaths: contextMedia.length > 0 ? contextMedia.map((m) => m.path) : undefined, - MediaUrls: contextMedia.length > 0 ? contextMedia.map((m) => m.path) : undefined, - MediaTypes: - contextMedia.length > 0 - ? (contextMedia.map((m) => m.contentType).filter(Boolean) as string[]) - : undefined, - Sticker: allMedia[0]?.stickerMetadata, - StickerMediaIncluded: allMedia[0]?.stickerMetadata ? !stickerCacheHit : undefined, - ...(locationData ? toLocationContext(locationData) : undefined), - CommandAuthorized: commandAuthorized, - // For groups: use resolved forum topic id; for DMs: use raw messageThreadId - MessageThreadId: threadSpec.id, - IsForum: isForum, - // Originating channel for reply routing. - OriginatingChannel: "telegram" as const, - OriginatingTo: `telegram:${chatId}`, - }); - - const pinnedMainDmOwner = !isGroup - ? resolvePinnedMainDmOwnerFromAllowlist({ - dmScope: cfg.session?.dmScope, - allowFrom: dmAllowFrom, - normalizeEntry: (entry) => normalizeAllowFrom([entry]).entries[0], - }) - : null; - const updateLastRouteSessionKey = resolveInboundLastRouteSessionKey({ - route, - sessionKey, - }); - - await recordInboundSession({ - storePath, - sessionKey: ctxPayload.SessionKey ?? sessionKey, - ctx: ctxPayload, - updateLastRoute: !isGroup - ? { - sessionKey: updateLastRouteSessionKey, - channel: "telegram", - to: `telegram:${chatId}`, - accountId: route.accountId, - // Preserve DM topic threadId for replies (fixes #8891) - threadId: dmThreadId != null ? String(dmThreadId) : undefined, - mainDmOwnerPin: - updateLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner && senderId - ? { - ownerRecipient: pinnedMainDmOwner, - senderRecipient: senderId, - onSkip: ({ ownerRecipient, senderRecipient }) => { - logVerbose( - `telegram: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`, - ); - }, - } - : undefined, - } - : undefined, - onRecordError: (err) => { - logVerbose(`telegram: failed updating session meta: ${String(err)}`); - }, - }); - - if (replyTarget && shouldLogVerbose()) { - const preview = replyTarget.body.replace(/\s+/g, " ").slice(0, 120); - logVerbose( - `telegram reply-context: replyToId=${replyTarget.id} replyToSender=${replyTarget.sender} replyToBody="${preview}"`, - ); - } - - if (forwardOrigin && shouldLogVerbose()) { - logVerbose( - `telegram forward-context: forwardedFrom="${forwardOrigin.from}" type=${forwardOrigin.fromType}`, - ); - } - - if (shouldLogVerbose()) { - const preview = body.slice(0, 200).replace(/\n/g, "\\n"); - const mediaInfo = allMedia.length > 1 ? ` mediaCount=${allMedia.length}` : ""; - const topicInfo = resolvedThreadId != null ? ` topic=${resolvedThreadId}` : ""; - logVerbose( - `telegram inbound: chatId=${chatId} from=${ctxPayload.From} len=${body.length}${mediaInfo}${topicInfo} preview="${preview}"`, - ); - } return { ctxPayload, @@ -911,7 +453,7 @@ export const buildTelegramMessageContext = async ({ threadSpec, replyThreadId, isForum, - historyKey, + historyKey: bodyResult.historyKey, historyLimit, groupHistories, route, diff --git a/src/telegram/bot-message-context.types.ts b/src/telegram/bot-message-context.types.ts new file mode 100644 index 00000000000..9f140b63907 --- /dev/null +++ b/src/telegram/bot-message-context.types.ts @@ -0,0 +1,65 @@ +import type { Bot } from "grammy"; +import type { HistoryEntry } from "../auto-reply/reply/history.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { + DmPolicy, + TelegramDirectConfig, + TelegramGroupConfig, + TelegramTopicConfig, +} from "../config/types.js"; +import type { StickerMetadata, TelegramContext } from "./bot/types.js"; + +export type TelegramMediaRef = { + path: string; + contentType?: string; + stickerMetadata?: StickerMetadata; +}; + +export type TelegramMessageContextOptions = { + forceWasMentioned?: boolean; + messageIdOverride?: string; +}; + +export type TelegramLogger = { + info: (obj: Record, msg: string) => void; +}; + +export type ResolveTelegramGroupConfig = ( + chatId: string | number, + messageThreadId?: number, +) => { + groupConfig?: TelegramGroupConfig | TelegramDirectConfig; + topicConfig?: TelegramTopicConfig; +}; + +export type ResolveGroupActivation = (params: { + chatId: string | number; + agentId?: string; + messageThreadId?: number; + sessionKey?: string; +}) => boolean | undefined; + +export type ResolveGroupRequireMention = (chatId: string | number) => boolean; + +export type BuildTelegramMessageContextParams = { + primaryCtx: TelegramContext; + allMedia: TelegramMediaRef[]; + replyMedia?: TelegramMediaRef[]; + storeAllowFrom: string[]; + options?: TelegramMessageContextOptions; + bot: Bot; + cfg: OpenClawConfig; + account: { accountId: string }; + historyLimit: number; + groupHistories: Map; + dmPolicy: DmPolicy; + allowFrom?: Array; + groupAllowFrom?: Array; + ackReactionScope: "off" | "none" | "group-mentions" | "group-all" | "direct" | "all"; + logger: TelegramLogger; + resolveGroupActivation: ResolveGroupActivation; + resolveGroupRequireMention: ResolveGroupRequireMention; + resolveTelegramGroupConfig: ResolveTelegramGroupConfig; + /** Global (per-account) handler for sendChatAction 401 backoff (#27092). */ + sendChatActionHandler: import("./sendchataction-401-backoff.js").TelegramSendChatActionHandler; +};