import { findModelInCatalog, loadModelCatalog, modelSupportsVision, } from "openclaw/plugin-sdk/agent-runtime"; import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime"; import { buildMentionRegexes, formatLocationText, logInboundDrop, matchesMentionWithExplicit, resolveMentionGatingWithBypass, type NormalizedLocation, } from "openclaw/plugin-sdk/channel-inbound"; import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth"; import { hasControlCommand } from "openclaw/plugin-sdk/command-auth"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { TelegramDirectConfig, TelegramGroupConfig, TelegramTopicConfig, } from "openclaw/plugin-sdk/config-runtime"; import { recordPendingHistoryEntryIfEnabled, type HistoryEntry, } from "openclaw/plugin-sdk/reply-history"; import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; 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.runtime.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, }; }