11// Telegram plugin module implements bot handlers behavior.
22import { randomUUID } from "node:crypto" ;
3+ import path from "node:path" ;
34import type { Message , ReactionTypeEmoji } from "grammy/types" ;
45import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime" ;
56import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-helpers" ;
@@ -160,6 +161,39 @@ import {
160161} from "./network-errors.js" ;
161162import { buildInlineKeyboard } from "./send.js" ;
162163
164+ function resolveTelegramPromptMediaPath ( mediaPath : string ) : string | undefined {
165+ const toInboundMediaPath = ( id : string ) : string | undefined => {
166+ if (
167+ ! id ||
168+ id === "." ||
169+ id === ".." ||
170+ id . includes ( "/" ) ||
171+ id . includes ( "\\" ) ||
172+ id . includes ( "\0" )
173+ ) {
174+ return undefined ;
175+ }
176+ return `media://inbound/${ encodeURIComponent ( id ) } ` ;
177+ } ;
178+ const decodeInboundMediaId = ( id : string ) : string | undefined => {
179+ try {
180+ return decodeURIComponent ( id ) ;
181+ } catch {
182+ return undefined ;
183+ }
184+ } ;
185+ const canonicalMatch = / ^ m e d i a : \/ \/ i n b o u n d \/ ( [ ^ / \\ ] + ) $ / i. exec ( mediaPath ) ;
186+ if ( canonicalMatch ?. [ 1 ] ) {
187+ const id = decodeInboundMediaId ( canonicalMatch [ 1 ] ) ;
188+ return id ? toInboundMediaPath ( id ) : undefined ;
189+ }
190+ const normalized = mediaPath . replace ( / \\ / g, "/" ) ;
191+ if ( ! normalized . includes ( "/media/inbound/" ) ) {
192+ return undefined ;
193+ }
194+ return toInboundMediaPath ( path . posix . basename ( normalized ) ) ;
195+ }
196+
163197export const registerTelegramHandlers = ( {
164198 cfg,
165199 accountId,
@@ -1109,16 +1143,21 @@ export const registerTelegramHandlers = ({
11091143 media ?: TelegramMediaRef ,
11101144 ) : TelegramReplyChainEntry => {
11111145 const { sourceMessage : _sourceMessage , ...entry } = node ;
1146+ if ( ! media ?. path ) {
1147+ return entry ;
1148+ }
1149+ const { mediaRef : _mediaRef , ...entryWithoutProviderMediaRef } = entry ;
11121150 return {
1113- ...entry ,
1114- ... ( media ?. path ? { mediaPath : media . path } : { } ) ,
1151+ ...entryWithoutProviderMediaRef ,
1152+ mediaPath : media . path ,
11151153 ...( media ?. contentType ? { mediaType : media . contentType } : { } ) ,
11161154 } ;
11171155 } ;
11181156
11191157 const toPromptContextMessage = (
11201158 node : TelegramCachedMessageNode ,
11211159 flags ?: { replyTarget ?: boolean } ,
1160+ media ?: TelegramMediaRef ,
11221161 ) => ( {
11231162 message_id : node . messageId ,
11241163 thread_id : node . threadId ,
@@ -1127,8 +1166,9 @@ export const registerTelegramHandlers = ({
11271166 sender_username : node . senderUsername ,
11281167 timestamp_ms : node . timestamp ,
11291168 body : node . body ,
1130- media_type : node . mediaType ,
1131- media_ref : node . mediaRef ,
1169+ media_type : media ?. contentType ?? node . mediaType ,
1170+ media_path : media ?. path ,
1171+ media_ref : media ?. path ? undefined : node . mediaRef ,
11321172 reply_to_id : node . replyToId ,
11331173 is_reply_target : flags ?. replyTarget === true ? true : undefined ,
11341174 } ) ;
@@ -1137,6 +1177,7 @@ export const registerTelegramHandlers = ({
11371177 msg : Message ,
11381178 replyChainNodes : TelegramCachedMessageNode [ ] ,
11391179 options ?: TelegramMessageContextOptions ,
1180+ mediaByMessageId ?: ReadonlyMap < string , TelegramMediaRef > ,
11401181 ) : Promise < TelegramPromptContextEntry [ ] > => {
11411182 const messageId = typeof msg . message_id === "number" ? String ( msg . message_id ) : undefined ;
11421183 const currentNode = await messageCache . get ( {
@@ -1168,7 +1209,11 @@ export const registerTelegramHandlers = ({
11681209 order : "chronological" ,
11691210 relation : "selected_for_current_message" ,
11701211 messages : conversationContext . map ( ( entry ) =>
1171- toPromptContextMessage ( entry . node , { replyTarget : entry . isReplyTarget } ) ,
1212+ toPromptContextMessage (
1213+ entry . node ,
1214+ { replyTarget : entry . isReplyTarget } ,
1215+ entry . node . messageId ? mediaByMessageId ?. get ( entry . node . messageId ) : undefined ,
1216+ ) ,
11721217 ) ,
11731218 } ,
11741219 } ,
@@ -1240,10 +1285,39 @@ export const registerTelegramHandlers = ({
12401285 params . ctx ,
12411286 replyChainNodes ,
12421287 ) ;
1288+ const promptContextMediaByMessageId = new Map < string , TelegramMediaRef > ( ) ;
1289+ const currentMessageId =
1290+ typeof params . msg . message_id === "number" ? String ( params . msg . message_id ) : undefined ;
1291+ const currentMedia = params . allMedia [ 0 ] ;
1292+ const currentPromptMediaPath = currentMedia ?. path
1293+ ? resolveTelegramPromptMediaPath ( currentMedia . path )
1294+ : undefined ;
1295+ if ( currentMessageId && currentMedia && currentPromptMediaPath ) {
1296+ promptContextMediaByMessageId . set ( currentMessageId , {
1297+ ...currentMedia ,
1298+ path : currentPromptMediaPath ,
1299+ } ) ;
1300+ }
1301+ const isGroupConversation =
1302+ params . msg . chat . type === "group" || params . msg . chat . type === "supergroup" ;
1303+ if ( ! isGroupConversation ) {
1304+ for ( const entry of replyChain ) {
1305+ const promptMediaPath = entry . mediaPath
1306+ ? resolveTelegramPromptMediaPath ( entry . mediaPath )
1307+ : undefined ;
1308+ if ( entry . messageId && entry . mediaPath && promptMediaPath ) {
1309+ promptContextMediaByMessageId . set ( entry . messageId , {
1310+ path : promptMediaPath ,
1311+ ...( entry . mediaType ? { contentType : entry . mediaType } : { } ) ,
1312+ } ) ;
1313+ }
1314+ }
1315+ }
12431316 const promptContext = await buildPromptContextForMessage (
12441317 params . msg ,
12451318 replyChainNodes ,
12461319 params . options ,
1320+ promptContextMediaByMessageId ,
12471321 ) ;
12481322 const result = await processMessage (
12491323 params . ctx ,
0 commit comments