@@ -18,6 +18,7 @@ import { Profile, ScrapeRequest, ScrapeResult, type Transaction, type Transactio
1818import { transactionsToCsv , transactionsToJson } from '@app/shared' ;
1919import { postScrapeService } from './postScrapeService.js' ;
2020import { buildUnifiedChatQueryWithMemory , mergeAndPersistAiMemory } from './unifiedAiChatMemory.js' ;
21+ import { getTelegramMaxMessageChars , splitTelegramHtmlChunks , splitTelegramPlainText } from '../utils/telegramTextSplit.js' ;
2122
2223const __filename = fileURLToPath ( import . meta. url ) ;
2324const __dirname = path . dirname ( __filename ) ;
@@ -1091,8 +1092,12 @@ export class TelegramBotService {
10911092
10921093 if ( statusMsg && chatIdNum ) {
10931094 const body = summaryLines . length > 0 ? summaryLines . join ( '\n' ) : this . t ( 'errorScraper' ) ;
1094- const finalText = `${ this . t ( 'scraperSuccess' ) } \n\n${ body } ` . slice ( 0 , 4096 ) ;
1095- await this . editTelegramStatusMessage ( ctx , chatIdNum , statusMsg . message_id , finalText ) ;
1095+ const finalText = `${ this . t ( 'scraperSuccess' ) } \n\n${ body } ` ;
1096+ const summaryChunks = splitTelegramPlainText ( finalText , getTelegramMaxMessageChars ( ) ) ;
1097+ await this . editTelegramStatusMessage ( ctx , chatIdNum , statusMsg . message_id , summaryChunks [ 0 ] ) ;
1098+ for ( let i = 1 ; i < summaryChunks . length ; i ++ ) {
1099+ await ctx . reply ( summaryChunks [ i ] ) ;
1100+ }
10961101 }
10971102
10981103 if ( results . length > 0 ) {
@@ -1306,30 +1311,26 @@ export class TelegramBotService {
13061311 this . chatStates . set ( chatIdNum . toString ( ) , updatedState ) ;
13071312 }
13081313
1309- // If response already contains HTML tags, use as-is; otherwise convert Markdown to HTML
1314+ // If response already contains HTML tags, use as-is; otherwise convert Markdown to HTML per chunk
13101315 const looksLikeHtml = / < \/ ? \w + [ ^ > ] * > / . test ( response ) ;
1311- const htmlResponse = looksLikeHtml ? response : this . convertMarkdownToHtml ( response ) ;
1316+ const htmlChunks : string [ ] = looksLikeHtml
1317+ ? splitTelegramHtmlChunks ( response , 4080 )
1318+ : splitTelegramPlainText ( response , 3400 ) . flatMap ( ( ch ) => {
1319+ const h = this . convertMarkdownToHtml ( ch ) ;
1320+ return h . length > 4080 ? splitTelegramHtmlChunks ( h , 4080 ) : [ h ] ;
1321+ } ) ;
13121322
1313- if ( htmlResponse . length > 4096 ) {
1314- const chunks = htmlResponse . match ( / [ \s \S ] { 1 , 4096 } / g) || [ ] ;
1315- // Edit first chunk into the thinking message
1316- try {
1317- // @ts -ignore
1318- await ctx . telegram . editMessageText ( chatIdNum , thinkingMsg . message_id , undefined , chunks [ 0 ] , { parse_mode : 'HTML' } ) ;
1319- } catch ( e ) {
1320- // If edit fails, send as a new message
1321- await ctx . reply ( String ( chunks [ 0 ] ) , { parse_mode : 'HTML' } ) ;
1322- }
1323- for ( let i = 1 ; i < chunks . length ; i ++ ) {
1324- await ctx . reply ( String ( chunks [ i ] ) , { parse_mode : 'HTML' } ) ;
1325- }
1326- } else {
1327- try {
1328- // @ts -ignore
1329- await ctx . telegram . editMessageText ( chatIdNum , thinkingMsg . message_id , undefined , htmlResponse , { parse_mode : 'HTML' } ) ;
1330- } catch ( e ) {
1331- await ctx . reply ( String ( htmlResponse ) , { parse_mode : 'HTML' } ) ;
1332- }
1323+ const chunks = htmlChunks . length > 0 ? htmlChunks : [ this . convertMarkdownToHtml ( response || this . t ( 'noAiResponse' ) ) ] ;
1324+ const firstChunk = chunks [ 0 ] ;
1325+ const restChunks = chunks . slice ( 1 ) ;
1326+
1327+ try {
1328+ await ctx . telegram . editMessageText ( chatIdNum , thinkingMsg . message_id , undefined , firstChunk , { parse_mode : 'HTML' } ) ;
1329+ } catch ( e ) {
1330+ await ctx . reply ( String ( firstChunk ) , { parse_mode : 'HTML' } ) ;
1331+ }
1332+ for ( const part of restChunks ) {
1333+ await ctx . reply ( String ( part ) , { parse_mode : 'HTML' } ) ;
13331334 }
13341335 } catch ( error ) {
13351336 serverLogger . error ( 'Error in AI chat' , { error } ) ;
@@ -2095,24 +2096,81 @@ export class TelegramBotService {
20952096 return this . isRunning ;
20962097 }
20972098
2099+ /**
2100+ * Deterministic long payload for exercising Telegram chunking (plain or HTML).
2101+ */
2102+ private buildLargeTestTelegramPayload ( targetLen : number , mode : 'plain' | 'html' ) : string {
2103+ const cap = Math . min ( 50000 , Math . max ( 1 , Math . floor ( targetLen ) ) ) ;
2104+ if ( cap < 64 ) {
2105+ return 'x' . repeat ( cap ) ;
2106+ }
2107+ let s = mode === 'html' ? '<b>LARGE-TEST</b>\n' : 'LARGE-TEST\n' ;
2108+ let n = 0 ;
2109+ while ( s . length < cap ) {
2110+ const line = mode === 'html' ? `<i>${ n } </i> ${ 'x' . repeat ( 32 ) } \n` : `LINE ${ n } ${ 'x' . repeat ( 48 ) } \n` ;
2111+ const room = cap - s . length ;
2112+ if ( line . length > room ) {
2113+ s += mode === 'html' ? 'x' . repeat ( room ) : line . slice ( 0 , room ) ;
2114+ break ;
2115+ }
2116+ s += line ;
2117+ n ++ ;
2118+ }
2119+ return s . slice ( 0 , cap ) ;
2120+ }
2121+
20982122 /**
20992123 * Send a test message to the given chat(s). If chatId is provided, send only to that chat;
21002124 * otherwise send to all notification chat IDs.
2125+ * Optional `testCharCount` (1–50000) sends a large payload split the same way as production long messages.
21012126 */
2102- async sendTestMessage ( chatId ?: string ) : Promise < { sent : number ; errors : string [ ] } > {
2127+ async sendTestMessage (
2128+ chatId ?: string ,
2129+ options ?: { testCharCount ?: number ; mode ?: 'plain' | 'html' }
2130+ ) : Promise < { sent : number ; errors : string [ ] } > {
21032131 if ( ! this . bot || ! this . isRunning ) {
21042132 throw new Error ( 'Telegram bot is not running. Start the bot first.' ) ;
21052133 }
21062134 const targetIds = chatId ? [ chatId ] : this . config . notificationChatIds ;
21072135 if ( targetIds . length === 0 ) {
21082136 throw new Error ( 'No notification chats configured. Add at least one user to the Notification column.' ) ;
21092137 }
2110- const text = '✅ Test message from Israeli Bank Scraper. If you see this, notifications are working.' ;
2138+ const rawCount = options ?. testCharCount ;
2139+ const count =
2140+ rawCount != null && Number . isFinite ( Number ( rawCount ) ) ? Math . floor ( Number ( rawCount ) ) : 0 ;
2141+ const mode = options ?. mode === 'html' ? 'html' : 'plain' ;
2142+
21112143 const errors : string [ ] = [ ] ;
21122144 let sent = 0 ;
2145+
2146+ if ( count <= 0 ) {
2147+ const text = '✅ Test message from Israeli Bank Scraper. If you see this, notifications are working.' ;
2148+ for ( const id of targetIds ) {
2149+ try {
2150+ await this . bot . telegram . sendMessage ( id , text ) ;
2151+ sent ++ ;
2152+ } catch ( err : any ) {
2153+ errors . push ( `${ id } : ${ err ?. message || err } ` ) ;
2154+ }
2155+ }
2156+ return { sent, errors } ;
2157+ }
2158+
2159+ const payload = this . buildLargeTestTelegramPayload ( count , mode ) ;
2160+ const chunks =
2161+ mode === 'html'
2162+ ? splitTelegramHtmlChunks ( payload , 4080 )
2163+ : splitTelegramPlainText ( payload , getTelegramMaxMessageChars ( ) ) ;
2164+
21132165 for ( const id of targetIds ) {
21142166 try {
2115- await this . bot . telegram . sendMessage ( id , text ) ;
2167+ for ( const chunk of chunks ) {
2168+ await this . bot . telegram . sendMessage (
2169+ id ,
2170+ chunk ,
2171+ mode === 'html' ? { parse_mode : 'HTML' } : { }
2172+ ) ;
2173+ }
21162174 sent ++ ;
21172175 } catch ( err : any ) {
21182176 errors . push ( `${ id } : ${ err ?. message || err } ` ) ;
0 commit comments