• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lucasliet / llm-telegram-bot / 21789336946

08 Feb 2026 12:24AM UTC coverage: 54.544% (-9.7%) from 64.266%
21789336946

Pull #24

github

web-flow
Merge 5a5e7ba2d into cb7c1a213
Pull Request #24: style: format code and documentation, add Antigravity provider

185 of 374 branches covered (49.47%)

Branch coverage included in aggregate %.

477 of 1576 new or added lines in 17 files covered. (30.27%)

4 existing lines in 2 files now uncovered.

2864 of 5216 relevant lines covered (54.91%)

17.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

58.08
/src/prototype/ContextExtensionPrototype.ts
1
import { Context } from 'grammy';
1✔
2
import { Action } from 'grammy-auto-chat-action-types';
3
import { Audio, Message, ParseMode, PhotoSize, Voice } from 'grammy-types';
4
import { transcribeAudio } from '@/service/TelegramService.ts';
1✔
5
import { toTelegramMarkdown } from '@/util/MarkdownUtils.ts';
1✔
6

7
const MARKDOWN_ERROR_MESSAGE = 'Error on markdown parse_mode, message:';
1✔
8

9
declare module 'grammy' {
10
        interface Context {
11
                replyWithQuote(
12
                        output: string,
13
                        config?: { parse_mode: ParseMode },
14
                ): Promise<Message.TextMessage>;
15

16
                replyWithVisionNotSupportedByModel(): Promise<Message.TextMessage>;
17

18
                startTypingIndicator(): number;
19

20
                replyInChunks(output: string): void;
21

22
                streamReply(
23
                        reader: ReadableStreamDefaultReader<Uint8Array>,
24
                        onComplete: (completedAnswer: string) => Promise<void>,
25
                        responseMap?: (responseBody: string) => string,
26
                        lastResult?: string,
27
                ): Promise<void>;
28

29
                extractContextKeys(): Promise<{
30
                        userId: number;
31
                        userKey: string;
32
                        contextMessage?: string;
33
                        audio?: Voice | Audio;
34
                        photos?: PhotoSize[];
35
                        caption?: string;
36
                        quote?: string;
37
                }>;
38

39
                chatAction: Action | undefined;
40
        }
41
}
42

43
/**
1✔
44
 * Reply to a message with quoting the original message
45
 */
×
46
Context.prototype.replyWithQuote = function (
×
47
        this: Context,
48
        output: string,
×
49
        config?: { parse_mode: ParseMode },
×
50
) {
51
        return this.reply(output, {
×
52
                reply_to_message_id: this.message?.message_id,
×
53
                ...config,
×
54
        });
×
55
};
×
56

57
/**
1✔
58
 * Reply that the model doesn't support vision capabilities
59
 */
1✔
60
Context.prototype.replyWithVisionNotSupportedByModel = function (
1✔
61
        this: Context,
62
) {
63
        return this.replyWithQuote('esse modelo não suporta leitura de foto');
2✔
64
};
1✔
65

66
/**
1✔
67
 * Start typing indicator that persists by re-sending every 4 seconds
68
 */
1✔
69
Context.prototype.startTypingIndicator = function (this: Context): number {
1✔
70
        this.chatAction = 'typing';
2✔
71
        return setInterval(() => {
×
72
                this.chatAction = 'typing';
×
73
        }, 4000);
×
74
};
1✔
75

76
/**
1✔
77
 * Split a large response into multiple message chunks
78
 */
1✔
79
Context.prototype.replyInChunks = function (
1✔
80
        this: Context,
81
        output: string,
1✔
82
): void {
83
        if (output.length > 4096) {
3✔
84
                const outputChunks = output.match(/[\s\S]{1,4093}/g)!;
4✔
85

86
                outputChunks.forEach((chunk, index) => {
4✔
87
                        const isLastChunk = index === outputChunks.length - 1;
6✔
88
                        const chunkOutput = `${chunk}${isLastChunk ? '' : '...'}`;
6✔
89
                        const sanitizedOutput = toTelegramMarkdown(chunkOutput);
6✔
90

91
                        this.replyWithQuote(sanitizedOutput, { parse_mode: 'Markdown' })
12✔
92
                                .catch(() => {
×
93
                                        console.warn(MARKDOWN_ERROR_MESSAGE, chunkOutput);
×
94
                                        this.replyWithQuote(chunkOutput);
×
95
                                });
×
96
                });
4✔
97
                return;
4✔
98
        }
4✔
99

100
        const sanitizedOutput = toTelegramMarkdown(output);
4✔
101
        this.replyWithQuote(sanitizedOutput, { parse_mode: 'Markdown' })
12✔
102
                .catch(() => {
4✔
103
                        console.warn(MARKDOWN_ERROR_MESSAGE, output);
5✔
104
                        this.replyWithQuote(output);
5✔
105
                });
4✔
106
};
1✔
107

108
/**
1✔
109
 * Stream a response to the user with periodic updates
110
 */
1✔
111
Context.prototype.streamReply = async function (
1✔
112
        this: Context,
113
        reader: ReadableStreamDefaultReader<Uint8Array>,
1✔
114
        onComplete: (completedAnswer: string) => Promise<void>,
1✔
115
        responseMap?: (responseBody: string) => string,
1✔
116
        lastResult?: string,
1✔
117
): Promise<void> {
118
        const { message_id } = await this.replyWithQuote('processando...');
2✔
119
        let result = lastResult || '';
2✔
120
        let lastUpdate = Date.now();
2✔
121
        let lastSentMessage = '';
2✔
122

123
        while (true) {
2✔
124
                const { done, value } = await reader.read();
5✔
125
                if (done) break;
5✔
126

127
                const chunk = decodeStreamResponseText(value, responseMap);
7✔
128
                result += chunk;
7✔
129

130
                if (result.length > 4093) {
×
131
                        result = result.removeThinkingChatCompletion()
×
132
                                .convertBlackBoxWebSearchSourcesToMarkdown();
×
133

134
                        if (result.length > 4093) {
×
135
                                const remainingChunk = result.substring(4093) + chunk;
×
136
                                result = result.substring(0, 4093);
×
137

138
                                const updateResult = await editMessageWithCompletionEvery3Seconds(
×
139
                                        this,
×
140
                                        message_id,
×
141
                                        result,
×
142
                                        lastUpdate,
×
143
                                        lastSentMessage,
×
144
                                        true,
×
145
                                );
146
                                lastUpdate = updateResult.timestamp;
×
147
                                lastSentMessage = updateResult.lastMessage;
×
148
                                onComplete(result);
×
149
                                return this.streamReply(
×
150
                                        reader,
×
151
                                        onComplete,
×
152
                                        responseMap,
×
153
                                        remainingChunk,
×
154
                                );
155
                        }
×
156
                }
✔
157

158
                const updateResult = await editMessageWithCompletionEvery3Seconds(
7✔
159
                        this,
7✔
160
                        message_id,
7✔
161
                        result,
7✔
162
                        lastUpdate,
7✔
163
                        lastSentMessage,
7✔
164
                );
165
                lastUpdate = updateResult.timestamp;
7✔
166
                lastSentMessage = updateResult.lastMessage;
7✔
167
        }
7✔
168

169
        let sanitizedResult = result.removeThinkingChatCompletion()
2✔
170
                .convertBlackBoxWebSearchSourcesToMarkdown();
2✔
171

172
        if (sanitizedResult.length > 4093) {
×
173
                const remainingChunk = sanitizedResult.substring(4093);
×
174
                sanitizedResult = sanitizedResult.substring(0, 4093) + '...';
×
175
                this.replyInChunks(remainingChunk);
×
176
        }
×
177

178
        this.api.editMessageText(this.chat!.id, message_id, toTelegramMarkdown(sanitizedResult), {
2✔
179
                parse_mode: 'Markdown',
2✔
180
        })
×
181
                .catch(() => {
×
182
                        console.warn(MARKDOWN_ERROR_MESSAGE, sanitizedResult);
×
183
                        this.api.editMessageText(this.chat!.id, message_id, sanitizedResult);
×
184
                });
×
185

186
        onComplete(result);
2✔
187
};
1✔
188

189
/**
1✔
190
 * Extract common context keys from the message
191
 */
1✔
192
Context.prototype.extractContextKeys = async function (this: Context) {
1✔
193
        const userId = this.from?.id!;
2✔
194
        const userKey = `user:${userId}`;
2✔
195
        const audio = this.message?.voice || this.message?.audio;
2✔
196
        const contextMessage = await getTextMessage(userId, userKey, this, audio);
2✔
197
        const photos = this.message?.photo;
2✔
198
        const caption = this.message?.caption;
2✔
199
        const quote = this.message?.reply_to_message?.text;
×
200

201
        return { userId, userKey, contextMessage, audio, photos, caption, quote };
18✔
202
};
1✔
203

204
/**
1✔
205
 * Helper function to get text from a message, transcribing audio if needed
206
 */
1✔
207
function getTextMessage(
1✔
208
        userId: number,
1✔
209
        userKey: string,
1✔
210
        ctx: Context,
1✔
211
        audio?: Voice,
1✔
212
): Promise<string | undefined> {
213
        if (audio) {
×
214
                return transcribeAudio(userId, userKey, ctx, audio);
×
215
        }
×
216
        return Promise.resolve(ctx.message?.text);
2✔
217
}
2✔
218

219
/**
1✔
220
 * Decode text from the response stream
221
 */
1✔
222
function decodeStreamResponseText(
1✔
223
        responseMessage: Uint8Array,
1✔
224
        responseMap?: (responseBody: string) => string,
1✔
225
): string {
226
        const decoder = new TextDecoder();
3✔
227
        const decodedText = decoder.decode(responseMessage);
3✔
228
        return responseMap ? responseMap(decodedText) : decodedText;
×
229
}
3✔
230

231
/**
1✔
232
 * Edit a message with updated content, respecting rate limits
233
 * Avoid hitting Telegram API rate limit https://core.telegram.org/bots/faq#broadcasting-to-users
234
 */
1✔
235
async function editMessageWithCompletionEvery3Seconds(
1✔
236
        ctx: Context,
1✔
237
        messageId: number,
1✔
238
        message: string,
1✔
239
        lastUpdate: number,
1✔
240
        lastSentMessage: string,
1✔
241
        isLastMessage = false,
1✔
242
): Promise<{ timestamp: number; lastMessage: string }> {
243
        const now = Date.now();
3✔
244
        const has2SecondsPassed = now - lastUpdate >= 2000;
3✔
245
        const displayMessage = message + (isLastMessage ? '' : '...');
×
246

247
        if ((isLastMessage || has2SecondsPassed) && displayMessage !== lastSentMessage) {
×
248
                try {
×
NEW
249
                        await ctx.api.editMessageText(ctx.chat!.id, messageId, toTelegramMarkdown(displayMessage), {
×
250
                                parse_mode: 'Markdown',
×
251
                        });
×
252
                        return { timestamp: now, lastMessage: displayMessage };
×
253
                } catch (error) {
×
254
                        if (error instanceof Error && error.message.includes('message is not modified')) {
×
255
                                return { timestamp: lastUpdate, lastMessage: lastSentMessage };
×
256
                        }
×
257
                        console.warn(MARKDOWN_ERROR_MESSAGE, displayMessage);
×
258
                        try {
×
259
                                await ctx.api.editMessageText(ctx.chat!.id, messageId, displayMessage);
×
260
                                return { timestamp: now, lastMessage: displayMessage };
×
261
                        } catch (fallbackError) {
×
262
                                console.error(`Failed to edit message ${messageId} in chat ${ctx.chat!.id}:`, fallbackError);
×
263
                                return { timestamp: lastUpdate, lastMessage: lastSentMessage };
×
264
                        }
×
265
                }
×
266
        }
×
267

268
        return { timestamp: lastUpdate, lastMessage: lastSentMessage };
12✔
269
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc