| 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<boolean> { |
| 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<string, HistoryEntry[]>; |
| historyLimit: number; |
| logger: TelegramLogger; |
| }): Promise<TelegramInboundBodyResult | null> { |
| 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 === "<media:audio>" && preflightTranscript) { |
| bodyText = preflightTranscript; |
| } |
|
|
| if (!bodyText && allMedia.length > 0) { |
| if (hasAudio) { |
| bodyText = preflightTranscript || "<media:audio>"; |
| } else { |
| bodyText = `<media:image>${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, |
| }; |
| } |
|
|