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

lucasliet / llm-telegram-bot / 23172248446

17 Mar 2026 12:19AM UTC coverage: 54.15% (-0.4%) from 54.504%
23172248446

push

github

lucasliet
feat: add Groq provider and provider-builder skill

183 of 377 branches covered (48.54%)

Branch coverage included in aggregate %.

21 of 37 new or added lines in 5 files covered. (56.76%)

201 existing lines in 7 files now uncovered.

2988 of 5479 relevant lines covered (54.54%)

18.09 hits per line

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

54.4
/src/service/TelegramService.ts
1
import { Context } from 'grammy';
2
import { Voice } from 'grammy-types';
3

4
import { getCurrentModel, setCurrentModel } from '@/repository/ChatRepository.ts';
16✔
5

6
import { ModelCommand, modelCommands, WHITELISTED_MODELS } from '@/config/models.ts';
16✔
7
import { escapeMarkdownV1 } from '@/util/MarkdownUtils.ts';
16✔
8

9
import {
16✔
10
        handleAntigravity,
16✔
11
        handleArta,
16✔
12
        handleCloudflare,
16✔
13
        handleFala,
16✔
14
        handleGemini,
16✔
15
        handleGithubCopilot,
16✔
16
        handleGroq,
16✔
17
        handleOpenAI,
16✔
18
        handleOpenRouter,
16✔
19
        handleOpenWebUI,
16✔
20
        handlePerplexity,
16✔
21
        handlePollinations,
16✔
22
        handleVertex,
16✔
23
        handleZai,
16✔
24
} from '@/handlers/index.ts';
16✔
25

26
import { FileUtils } from '@/util/FileUtils.ts';
16✔
27
import GithubCopilotService from './openai/GithubCopilotService.ts';
16✔
28

29
const TOKEN = Deno.env.get('BOT_TOKEN') as string;
16✔
30
const ADMIN_USER_IDS: number[] = (Deno.env.get('ADMIN_USER_IDS') as string)
16✔
31
        .split('|')
16✔
32
        .map((id) => parseInt(id));
16✔
33

34
/**
1✔
35
 * Helper to keep Deno job alive during long-running requests
36
 * @returns Interval ID
37
 */
16✔
38
function keepDenoJobAlive(): number {
16✔
39
        return setInterval(() => true, 2000);
×
40
}
18✔
41

42
/**
1✔
43
 * Service for handling Telegram bot interactions
44
 */
16✔
45
export default {
16✔
46
        /**
16✔
47
         * Sets the webhook URL for the Telegram bot
48
         * @returns Response from Telegram API
49
         */
16✔
50
        setWebhook(): Promise<Response> {
16✔
51
                console.log('Setting webhook...');
17✔
52
                return fetch(`https://api.telegram.org/bot${TOKEN}/setWebhook`, {
17✔
53
                        method: 'POST',
17✔
54
                        headers: {
17✔
55
                                'Content-Type': 'application/json',
17✔
56
                        },
17✔
57
                        body: JSON.stringify({
17✔
58
                                url: 'https://llm-telegram-bot.deno.dev/webhook',
17✔
59
                        }),
17✔
60
                });
17✔
61
        },
16✔
62

63
        /**
16✔
64
         * Calls a model function if the user is an admin, otherwise defaults to text content
65
         * @param ctx - Telegram context
66
         * @param modelCallFunction - Function to call for the specific model
67
         */
16✔
68
        callAdminModel(
16✔
69
                ctx: Context,
16✔
70
                modelCallFunction: (ctx: Context) => Promise<void>,
16✔
71
        ): void {
72
                const userId = ctx.from?.id;
18✔
73
                if (userId && ADMIN_USER_IDS.includes(userId)) {
18✔
74
                        this.callModel(ctx, modelCallFunction);
19✔
75
                } else {
19✔
76
                        this.callModel(ctx, this.replyTextContent);
19✔
77
                }
19✔
78
        },
16✔
79

80
        /**
16✔
81
         * Generic model call handler with timeout and logging
82
         * @param ctx - Telegram context
83
         * @param modelCallFunction - Function to call for the specific model
84
         */
16✔
85
        callModel(
16✔
86
                ctx: Context,
16✔
87
                modelCallFunction: (ctx: Context) => Promise<void>,
16✔
88
        ): void {
89
                console.info(`user: ${ctx.msg?.from?.id}, message: ${ctx.message?.text}`);
18✔
90

91
                const startTime = Date.now();
18✔
92
                const keepAliveId = keepDenoJobAlive();
18✔
93
                const typingId = ctx.startTypingIndicator();
18✔
94

95
                modelCallFunction(ctx)
18✔
96
                        .then(() => {
18✔
97
                                ctx.chatAction = undefined;
19✔
98
                                clearInterval(typingId);
19✔
99
                                clearInterval(keepAliveId);
19✔
100
                                console.log(`Request processed in ${Date.now() - startTime}ms`);
19✔
101
                        })
18✔
102
                        .catch((err) => {
18✔
103
                                ctx.chatAction = undefined;
19✔
104
                                clearInterval(typingId);
19✔
105
                                clearInterval(keepAliveId);
19✔
106
                                console.error(err);
19✔
107
                                ctx.reply(`Eita, algo deu errado: ${err.message}`, {
19✔
108
                                        reply_to_message_id: ctx.msg?.message_id,
19✔
109
                                });
19✔
110
                        });
18✔
111
        },
16✔
112

113
        /**
16✔
114
         * Retrieves and sends GitHub Copilot usage information to the user if the user is an admin.
115
         * @param ctx - The Telegram context.
116
         * @returns A promise that resolves when the usage information has been sent.
117
         */
16✔
118
        async getUsage(ctx: Context): Promise<any> {
16✔
119
                const { userId } = await ctx.extractContextKeys();
17✔
120
                if (!userId || !ADMIN_USER_IDS.includes(userId)) return;
×
121

122
                const data = await getUsage();
17✔
123

124
                const quotas = (data as any).quota_snapshots ?? {};
×
125
                const chat = quotas.chat as Quota | undefined;
17✔
126
                const completions = quotas.completions as Quota | undefined;
17✔
127
                const premium = quotas.premium_interactions as Quota | undefined;
17✔
128

129
                const formatted = `🤖
17✔
130
GitHub Copilot - Status de Uso
131

132
📋
133
Informações Gerais:
134
• *Plano*: ${escapeMarkdownV1((data as any).copilot_plan ?? 'n/a')}
×
135
• *Tipo de acesso*: ${escapeMarkdownV1((data as any).access_type_sku ?? 'n/a')}
×
136
• *Chat habilitado*: ${(data as any).chat_enabled ? 'Sim' : 'Não'}
×
137
• *Data de atribuição*: ${formatDate((data as any).assigned_date)}
17✔
138
• *Próxima renovação de cota*: ${formatDate((data as any).quota_reset_date)}
17✔
139

140
📊
141
Cotas de Uso:
142

143
🗨️
144
Chat:
145
• *Status*: ${formatQuota(chat)}
17✔
146
• *Overage permitido*: ${chat?.overage_permitted ? 'Sim' : 'Não'}
×
147
• *Contador de overage*: ${chat?.overage_count ?? 0}
×
148

149
💡
150
Completions (Autocompletar):
151
• *Status*: ${formatQuota(completions)}
17✔
152
• *Overage permitido*: ${completions?.overage_permitted ? 'Sim' : 'Não'}
×
153
• *Contador de overage*: ${completions?.overage_count ?? 0}
17!
154

155
⭐
156
Interações Premium:
157
• *Status*: ${formatQuota(premium)}
17✔
158
• *Overage permitido*: ${premium?.overage_permitted ? 'Sim' : 'Não'}
×
159
• *Contador de overage*: ${premium?.overage_count ?? 0}`;
×
160

161
                ctx.reply(formatted, { parse_mode: 'Markdown' });
34✔
162
        },
17✔
163

164
        /**
16✔
165
         * Returns admin IDs if the requesting user is an admin
166
         * @param ctx - Telegram context
167
         * @returns Array of admin user IDs
168
         */
16✔
169
        async getAdminIds(ctx: Context): Promise<number[]> {
16✔
170
                const { userId } = await ctx.extractContextKeys();
18✔
171
                if (userId && ADMIN_USER_IDS.includes(userId)) return ADMIN_USER_IDS;
18✔
172
                return [];
19✔
173
        },
19✔
174

175
        /**
16✔
176
         * Gets the current model for the user
177
         * @param ctx - Telegram context
178
         * @returns Current model command
179
         */
×
180
        async getCurrentModel(ctx: Context): Promise<ModelCommand> {
×
181
                const { userKey } = await ctx.extractContextKeys();
×
182
                return getCurrentModel(userKey);
×
183
        },
×
184

185
        /**
×
186
         * Sets the current model for the user
187
         * @param ctx - Telegram context
188
         */
16✔
189
        async setCurrentModel(ctx: Context): Promise<void> {
16✔
190
                console.info(`user: ${ctx.msg?.from?.id}, message: ${ctx.message?.text}`);
27✔
191
                const {
27✔
192
                        userId,
27✔
193
                        userKey,
27✔
194
                        contextMessage: message,
27✔
195
                } = await ctx.extractContextKeys();
27✔
196

197
                const command = (message || ctx.callbackQuery?.data) as ModelCommand;
×
198

199
                const isValidCommand = modelCommands.includes(command);
×
200
                const isAuthorizedUser = (userId && ADMIN_USER_IDS.includes(userId)) || WHITELISTED_MODELS.includes(command);
27✔
201

202
                if (!isValidCommand || !isAuthorizedUser) return;
27✔
203

204
                await setCurrentModel(userKey, command);
27✔
205
                ctx.reply(`Novo modelo de inteligência escolhido: ${command}`);
32✔
206
        },
32✔
207

208
        /**
16✔
209
         * Replies with text content based on the user's selected model
210
         * @param ctx - Telegram context
211
         */
×
212
        async replyTextContent(ctx: Context): Promise<void> {
×
213
                const { userKey, contextMessage: message } = await ctx.extractContextKeys();
×
214
                const currentModel = await getCurrentModel(userKey);
×
215

216
                const modelHandlers: Record<ModelCommand, () => Promise<void>> = {
×
217
                        '/polli': () => handlePollinations(ctx, `polli: ${message}`),
×
218
                        '/gpt': () => handleGithubCopilot(ctx, `gpt: ${message}`),
×
NEW
219
                        '/oss': () => handleGroq(ctx, `oss: ${message}`),
×
NEW
220
                        '/llama': () => handleGroq(ctx, `llama: ${message!}`),
×
221
                        '/gemini': () => handleVertex(ctx, `gemini: ${message}`),
×
222
                        '/geminiPro': () => handleVertex(ctx, `geminiPro: ${message}`),
×
223
                        '/antigravity': () => handleAntigravity(ctx, `antigravity: ${message}`),
×
224
                        '/antigeminipro': () => handleAntigravity(ctx, `antigeminipro: ${message}`),
×
225
                        '/zai': () => handleZai(ctx, `zai: ${message}`),
×
226
                        '/glm': () => handleZai(ctx, `glm: ${message}`),
×
227
                        '/glmflash': () => handleZai(ctx, `glmflash: ${message}`),
×
228
                };
×
229

230
                const handler = modelHandlers[currentModel];
×
231

232
                if (handler) {
×
233
                        await handler();
×
234
                } else {
×
235
                        ctx.reply('Modelo de inteligência não encontrado.', {
×
236
                                reply_to_message_id: ctx.message?.message_id,
×
237
                        });
×
238
                }
×
239
        },
×
240

241
        callPerplexityModel(ctx: Context, commandMessage?: string): Promise<void> {
×
242
                return handlePerplexity(ctx, commandMessage);
×
243
        },
×
244
        callOpenAIModel(ctx: Context, commandMessage?: string): Promise<void> {
×
245
                return handleOpenAI(ctx, commandMessage);
17✔
246
        },
17✔
247
        callCloudflareModel(ctx: Context, commandMessage?: string): Promise<void> {
×
248
                return handleCloudflare(ctx, commandMessage);
×
249
        },
×
250
        callOpenRouterModel(ctx: Context, commandMessage?: string): Promise<void> {
×
251
                return handleOpenRouter(ctx, commandMessage);
×
252
        },
×
253
        callGithubCopilotModel(ctx: Context, commandMessage?: string): Promise<void> {
×
254
                return handleGithubCopilot(ctx, commandMessage);
×
255
        },
×
256
        callOpenWebUIModel(ctx: Context, commandMessage?: string): Promise<void> {
×
257
                return handleOpenWebUI(ctx, commandMessage);
×
258
        },
×
259
        callGeminiModel(ctx: Context, commandMessage?: string): Promise<void> {
×
260
                return handleGemini(ctx, commandMessage);
×
261
        },
×
262
        callPollinationsModel(ctx: Context, commandMessage?: string): Promise<void> {
×
263
                return handlePollinations(ctx, commandMessage);
×
264
        },
×
265
        callVertexModel(ctx: Context, commandMessage?: string): Promise<void> {
×
266
                return handleVertex(ctx, commandMessage);
×
267
        },
×
268
        callArtaModel(ctx: Context, commandMessage?: string): Promise<void> {
×
269
                return handleArta(ctx, commandMessage);
×
270
        },
×
271
        callFala(ctx: Context, commandMessage?: string): Promise<void> {
×
272
                return handleFala(ctx, new GithubCopilotService(), commandMessage);
×
273
        },
×
274
        callAntigravityModel(ctx: Context, commandMessage?: string): Promise<void> {
×
275
                return handleAntigravity(ctx, commandMessage);
×
276
        },
×
277
        callZaiModel(ctx: Context, commandMessage?: string): Promise<void> {
×
278
                return handleZai(ctx, commandMessage);
×
279
        },
×
NEW
280
        callGroqModel(ctx: Context, commandMessage?: string): Promise<void> {
×
NEW
281
                return handleGroq(ctx, commandMessage);
×
NEW
282
        },
×
UNCOV
283
};
×
284

285
export const downloadTelegramFile = FileUtils.downloadTelegramFile;
×
286
export const transcribeAudio = (
×
287
        userId: number,
×
288
        userKey: string,
×
289
        ctx: Context,
×
290
        audio: Voice,
×
291
): Promise<string> => {
292
        return FileUtils.transcribeAudio(userId, userKey, ctx, audio);
×
293
};
×
294

295
export async function textToSpeech(ctx: Context, text: string): Promise<void> {
×
296
        const audioFile = await FileUtils.textToSpeech(text);
18✔
297

298
        ctx.replyWithVoice(audioFile, {
×
299
                reply_to_message_id: ctx.message?.message_id,
×
300
        });
×
301
}
18✔
302

303
/**
1✔
304
 * Retrieves Copilot usage information if the requesting user is an admin.
305
 * @param ctx - Telegram context.
306
 * @returns A promise that resolves to the Copilot usage data or an empty object if not authorized.
307
 */
16✔
308
export async function getUsage() {
16✔
309
        const url = 'https://api.github.com/copilot_internal/user';
17✔
310
        const headers: Record<string, string> = {
17✔
311
                Accept: 'application/json',
17✔
312
                Authorization: `token ${Deno.env.get('COPILOT_GITHUB_TOKEN')}`,
17✔
313
                'Editor-Version': 'vscode/1.98.1',
17✔
314
                'Editor-Plugin-Version': 'copilot-chat/0.26.7',
17✔
315
                'User-Agent': 'GitHubCopilotChat/0.26.7',
17✔
316
                'X-Github-Api-Version': '2025-04-01',
17✔
317
        };
17✔
318

319
        const res = await fetch(url, { headers });
51✔
320
        const text = await res.text();
17✔
321
        if (!res.ok) {
17!
322
                let body: any;
×
323
                try {
×
324
                        body = JSON.parse(text);
×
325
                } catch {
×
326
                        body = text;
×
327
                }
×
328
                throw new Error(`Copilot API error ${res.status}: ${JSON.stringify(body)}`);
×
329
        }
×
330

331
        try {
×
332
                return JSON.parse(text);
17✔
333
        } catch {
×
334
                return text;
×
335
        }
×
336
}
×
337

338
/**
1✔
339
 * Formats a date value into a localized string (pt-BR).
340
 * @param value - The date value to format. Can be a string, number, Date object, or undefined/null.
341
 * @returns The formatted date string, or 'n/a' if the value is null/undefined, or the original string if parsing fails.
342
 */
16✔
343
function formatDate(value: string | number | Date | undefined | null): string {
16✔
344
        if (!value) return 'n/a';
×
345
        try {
×
346
                const d = new Date(value as any);
18✔
347
                if (isNaN(d.getTime())) return String(value);
×
348
                return d.toLocaleString('pt-BR', {
×
349
                        day: '2-digit',
18✔
350
                        month: '2-digit',
18✔
351
                        year: 'numeric',
18✔
352
                        hour: '2-digit',
18✔
353
                        minute: '2-digit',
18✔
354
                });
18✔
355
        } catch {
×
356
                return String(value);
×
357
        }
×
358
}
×
359

360
type Quota = {
361
        unlimited?: boolean;
362
        entitlement?: number;
363
        remaining?: number;
364
        percent_remaining?: number;
365
        overage_permitted?: boolean;
366
        overage_count?: number;
367
};
368

369
/**
1✔
370
 * Formats a Quota object into a human-readable string.
371
 * @param q - The Quota object to format.
372
 * @returns A string representing the quota status, e.g., 'Ilimitado', 'Usadas X de Y (Z% restante)', or 'n/a'.
373
 */
16✔
374
function formatQuota(q: Quota | undefined): string {
16✔
375
        if (!q) return 'n/a';
×
376
        if (q.unlimited) return 'Ilimitado';
×
377
        const entitlement = q.entitlement ?? 0;
✔
378
        const remaining = q.remaining ?? 0;
19✔
379
        if (entitlement > 0) {
19✔
380
                const used = Math.max(0, entitlement - remaining);
19✔
381
                const percent = Math.max(0, Math.min(100, (remaining / entitlement) * 100));
21✔
382
                return `Usadas ${used} de ${entitlement} (${percent.toFixed(0)}% restante)`;
21✔
383
        }
21✔
384
        if (typeof q.percent_remaining === 'number') {
19✔
385
                return `${q.percent_remaining.toFixed(2)}% restante`;
20✔
386
        }
20✔
387
        return 'n/a';
×
388
}
×
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