11import { spawnSync } from "node:child_process" ;
22import { Bot , InlineKeyboard , type Context } from "grammy" ;
3+ import type { Chat } from "grammy/types" ;
34import type { Part } from "@opencode-ai/sdk" ;
45import * as log from "./log.js" ;
56import type { PermissionEvent } from "./events.js" ;
@@ -32,8 +33,22 @@ export const BOT_COMMANDS = [
3233 { command : "stop_llama" , description : "Stop llama service" } ,
3334] ;
3435
36+ function isChannel ( chat : Chat ) : boolean {
37+ return chat . type === "channel" || chat . type === "supergroup" ;
38+ }
39+
40+ function formatAsQuote ( text : string ) : string {
41+ const escaped = escapeMarkdownV2 ( text ) ;
42+ const lines = escaped . split ( "\n" ) ;
43+ const quoted = lines . map ( ( line ) => `> ${ line } ` ) . join ( "\n" ) ;
44+ return quoted ;
45+ }
46+
3547const THROTTLE_MS = 2000 ;
3648
49+ // Track channel membership for quoting
50+ let channelChatId : number | null = null ;
51+
3752// Telegram callback data is limited to 64 bytes. Permission IDs are too long,
3853// so we store them in a map keyed by a short incrementing counter.
3954let permCounter = 0 ;
@@ -69,6 +84,14 @@ function formatPermissionMessage(perm: PermissionEvent): string {
6984 return lines . join ( "\n" ) ;
7085}
7186
87+ function trackChannel ( ctx : Context ) : void {
88+ const chat = ctx . chat ;
89+ if ( ! chat ) return ;
90+ if ( isChannel ( chat ) ) {
91+ channelChatId = chat . id ;
92+ }
93+ }
94+
7295export function createBot ( token : string , allowedUsers : number [ ] ) : Bot {
7396 const bot = new Bot ( token ) ;
7497
@@ -81,6 +104,7 @@ export function createBot(token: string, allowedUsers: number[]): Bot {
81104 await ctx . reply ( "Not authorized." ) ;
82105 return ;
83106 }
107+ trackChannel ( ctx ) ;
84108 await next ( ) ;
85109 } ) ;
86110
@@ -325,12 +349,15 @@ export function createBot(token: string, allowedUsers: number[]): Bot {
325349 . then ( async ( parts ) => {
326350 const chunks = splitMessage ( formatTextParts ( parts ) ) ;
327351 for ( const chunk of chunks ) {
328- await ctx . api . sendMessage ( chatId , chunk , { parse_mode : "MarkdownV2" } ) ;
352+ const chunkToSend = channelChatId !== null ? formatAsQuote ( chunk ) : chunk ;
353+ await ctx . api . sendMessage ( chatId , chunkToSend , { parse_mode : "MarkdownV2" } ) ;
329354 }
330355 } )
331356 . catch ( async ( err ) => {
332357 log . error ( `[remember] error:` , err ) ;
333- await ctx . reply ( escapeMarkdownV2 ( `Error: ${ String ( err ) } ` ) , { parse_mode : "MarkdownV2" } ) ;
358+ const errText = escapeMarkdownV2 ( `Error: ${ String ( err ) } ` ) ;
359+ const errTextToSend = channelChatId !== null ? formatAsQuote ( errText ) : errText ;
360+ await ctx . reply ( errTextToSend , { parse_mode : "MarkdownV2" } ) ;
334361 } ) ;
335362 } catch ( err ) {
336363 log . error ( `[cmd] /remember error:` , err ) ;
@@ -373,7 +400,8 @@ export function createBot(token: string, allowedUsers: number[]): Bot {
373400
374401 const flushEdit = ( ) => {
375402 if ( latestPreview && responseMsgId !== null ) {
376- void editMessage ( ctx , responseMsgId , latestPreview ) ;
403+ const textToSend = channelChatId !== null ? formatAsQuote ( latestPreview ) : latestPreview ;
404+ void editMessage ( ctx , responseMsgId , textToSend ) ;
377405 lastEditTime = Date . now ( ) ;
378406 }
379407 } ;
@@ -387,7 +415,8 @@ export function createBot(token: string, allowedUsers: number[]): Bot {
387415 if ( responseMsgId === null ) {
388416 if ( sendingFirst ) return ;
389417 sendingFirst = true ;
390- void ctx . api . sendMessage ( chatId , latestPreview , { parse_mode : "MarkdownV2" } )
418+ const textToSend = channelChatId !== null ? formatAsQuote ( latestPreview ) : latestPreview ;
419+ void ctx . api . sendMessage ( chatId , textToSend , { parse_mode : "MarkdownV2" } )
391420 . then ( ( msg ) => { responseMsgId = msg . message_id ; lastEditTime = Date . now ( ) ; } )
392421 . catch ( ( err ) => log . error ( `[prompt] failed to send first message:` , err ) ) ;
393422 return ;
@@ -430,7 +459,8 @@ export function createBot(token: string, allowedUsers: number[]): Bot {
430459 const chunks = splitMessage ( textContent ) ;
431460 log . info ( `[prompt] done session=${ sessionId } chunks=${ chunks . length } ` ) ;
432461 for ( const chunk of chunks ) {
433- await ctx . api . sendMessage ( chatId , chunk , { parse_mode : "MarkdownV2" } ) ;
462+ const textToSend = channelChatId !== null ? formatAsQuote ( chunk ) : chunk ;
463+ await ctx . api . sendMessage ( chatId , textToSend , { parse_mode : "MarkdownV2" } ) ;
434464 }
435465 }
436466 // Save exchange to disk for qmd indexing
@@ -444,10 +474,11 @@ export function createBot(token: string, allowedUsers: number[]): Bot {
444474 cleanup ( ) ;
445475 log . error ( `[prompt] error session=${ sessionId } :` , err ) ;
446476 const errText = escapeMarkdownV2 ( `Error: ${ String ( err ) } ` ) ;
477+ const errTextQuoted = channelChatId !== null ? formatAsQuote ( errText ) : errText ;
447478 if ( responseMsgId !== null ) {
448- await editMessage ( ctx , responseMsgId , errText ) ;
479+ await editMessage ( ctx , responseMsgId , errTextQuoted ) ;
449480 } else {
450- await ctx . api . sendMessage ( chatId , errText , { parse_mode : "MarkdownV2" } ) ;
481+ await ctx . api . sendMessage ( chatId , errTextQuoted , { parse_mode : "MarkdownV2" } ) ;
451482 }
452483 } ) ;
453484 } catch ( err ) {
0 commit comments