@llblab/pi-telegram 0.16.5 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -8
- package/BACKLOG.md +8 -3
- package/CHANGELOG.md +21 -0
- package/README.md +9 -5
- package/docs/README.md +2 -1
- package/docs/architecture.md +14 -12
- package/docs/outbound.md +2 -2
- package/docs/public-api.md +3 -0
- package/docs/sections.md +4 -2
- package/docs/telegram-bot-api-rich-messages.md +890 -0
- package/docs/ui-style.md +1 -1
- package/index.ts +11 -2
- package/lib/commands.ts +2 -2
- package/lib/keyboard.ts +39 -0
- package/lib/lifecycle.ts +6 -0
- package/lib/menu-model.ts +3 -3
- package/lib/menu-settings.ts +2 -2
- package/lib/menu.ts +4 -4
- package/lib/outbound-attachments.ts +1 -0
- package/lib/outbound-buttons.ts +1 -0
- package/lib/outbound-voice.ts +2 -0
- package/lib/polling.ts +21 -8
- package/lib/preview.ts +152 -196
- package/lib/prompts.ts +3 -3
- package/lib/queue.ts +1 -1
- package/lib/rendering.ts +3 -349
- package/lib/replies.ts +199 -37
- package/lib/routing.ts +2 -2
- package/lib/sections.ts +15 -18
- package/lib/telegram-api.ts +103 -15
- package/lib/updates.ts +2 -1
- package/package.json +1 -1
package/lib/replies.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram reply delivery helpers
|
|
3
|
-
* Zones: telegram outbound, rendering transport
|
|
4
|
-
* Owns rendered
|
|
3
|
+
* Zones: telegram outbound, native rich markdown, UI/compat rendering transport
|
|
4
|
+
* Owns native assistant replies, rendered UI delivery, reply transport wiring, and plain text replies
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
|
|
7
8
|
import type {
|
|
9
|
+
TelegramInputRichMessage,
|
|
8
10
|
TelegramReplyParameters,
|
|
11
|
+
TelegramSendRichMessageBody,
|
|
9
12
|
TelegramSentMessage,
|
|
10
13
|
} from "./telegram-api.ts";
|
|
11
14
|
import {
|
|
@@ -20,6 +23,9 @@ export {
|
|
|
20
23
|
type TelegramRenderMode,
|
|
21
24
|
};
|
|
22
25
|
|
|
26
|
+
export const TELEGRAM_RICH_MESSAGE_MAX_CHARS = 32768;
|
|
27
|
+
export const TELEGRAM_RICH_MESSAGE_MAX_BLOCKS = 500;
|
|
28
|
+
|
|
23
29
|
// --- Reply Dedup ---
|
|
24
30
|
|
|
25
31
|
/** Non-persistent reply deduplication for a single agent turn.
|
|
@@ -50,25 +56,29 @@ export function createReplyDedupRuntime(): ReplyDedupRuntime {
|
|
|
50
56
|
|
|
51
57
|
// --- Transport-level dedup ---
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
const lastRepliedToMessageIdByChat = new Map<number, number>();
|
|
54
60
|
|
|
55
61
|
export function resetTransportReplyDedup(): void {
|
|
56
|
-
|
|
62
|
+
lastRepliedToMessageIdByChat.clear();
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
export function buildTelegramReplyParameters(
|
|
66
|
+
chatId: number,
|
|
60
67
|
messageId: number | undefined,
|
|
61
68
|
): TelegramReplyParameters | undefined {
|
|
62
69
|
if (messageId === undefined) return undefined;
|
|
63
|
-
if (
|
|
64
|
-
|
|
70
|
+
if (lastRepliedToMessageIdByChat.get(chatId) === messageId) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
lastRepliedToMessageIdByChat.set(chatId, messageId);
|
|
65
74
|
return { message_id: messageId, allow_sending_without_reply: true };
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
export function buildTelegramMultipartReplyParameters(
|
|
78
|
+
chatId: number,
|
|
69
79
|
messageId: number | undefined,
|
|
70
80
|
): string | undefined {
|
|
71
|
-
const parameters = buildTelegramReplyParameters(messageId);
|
|
81
|
+
const parameters = buildTelegramReplyParameters(chatId, messageId);
|
|
72
82
|
return parameters ? JSON.stringify(parameters) : undefined;
|
|
73
83
|
}
|
|
74
84
|
|
|
@@ -133,7 +143,8 @@ export interface TelegramReplyDeliveryDeps<TReplyMarkup> {
|
|
|
133
143
|
editMessage: (body: {
|
|
134
144
|
chat_id: number;
|
|
135
145
|
message_id: number;
|
|
136
|
-
text
|
|
146
|
+
text?: string;
|
|
147
|
+
rich_message?: TelegramInputRichMessage;
|
|
137
148
|
parse_mode?: "HTML";
|
|
138
149
|
reply_markup?: TReplyMarkup;
|
|
139
150
|
}) => Promise<unknown>;
|
|
@@ -178,11 +189,12 @@ export async function sendTelegramRenderedChunks<TReplyMarkup>(
|
|
|
178
189
|
deps: TelegramReplyDeliveryDeps<TReplyMarkup>,
|
|
179
190
|
options?: { replyMarkup?: TReplyMarkup; replyToMessageId?: number },
|
|
180
191
|
): Promise<number | undefined> {
|
|
192
|
+
assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
|
|
181
193
|
let lastMessageId: number | undefined;
|
|
182
194
|
for (const [index, chunk] of chunks.entries()) {
|
|
183
195
|
const replyParameters =
|
|
184
196
|
index === 0
|
|
185
|
-
? buildTelegramReplyParameters(options?.replyToMessageId)
|
|
197
|
+
? buildTelegramReplyParameters(chatId, options?.replyToMessageId)
|
|
186
198
|
: undefined;
|
|
187
199
|
const sent = await deps.sendMessage({
|
|
188
200
|
chat_id: chatId,
|
|
@@ -204,6 +216,7 @@ export async function editTelegramRenderedMessage<TReplyMarkup>(
|
|
|
204
216
|
deps: TelegramReplyDeliveryDeps<TReplyMarkup>,
|
|
205
217
|
options?: { replyMarkup?: TReplyMarkup },
|
|
206
218
|
): Promise<number | undefined> {
|
|
219
|
+
assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
|
|
207
220
|
if (chunks.length === 0) return messageId;
|
|
208
221
|
const [firstChunk, ...remainingChunks] = chunks;
|
|
209
222
|
await deps.editMessage({
|
|
@@ -244,24 +257,180 @@ export async function sendTelegramPlainReply(
|
|
|
244
257
|
return deps.sendRenderedChunks(chunks);
|
|
245
258
|
}
|
|
246
259
|
|
|
247
|
-
export
|
|
260
|
+
export function splitTelegramNativeMarkdown(markdown: string): string[] {
|
|
261
|
+
if (
|
|
262
|
+
markdown.length <= TELEGRAM_RICH_MESSAGE_MAX_CHARS &&
|
|
263
|
+
countTelegramNativeMarkdownBlocks(markdown) <= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS
|
|
264
|
+
) {
|
|
265
|
+
return [markdown];
|
|
266
|
+
}
|
|
267
|
+
const chunks: string[] = [];
|
|
268
|
+
let current = "";
|
|
269
|
+
let currentBlockCount = 0;
|
|
270
|
+
for (const rawBlock of splitTelegramNativeMarkdownBlocks(markdown)) {
|
|
271
|
+
for (const block of splitTelegramNativeMarkdownCountedBlocks(rawBlock)) {
|
|
272
|
+
const blockCount = countTelegramNativeMarkdownBlocks(block);
|
|
273
|
+
const candidate = current ? `${current}\n\n${block}` : block;
|
|
274
|
+
const exceedsChars = candidate.length > TELEGRAM_RICH_MESSAGE_MAX_CHARS;
|
|
275
|
+
const exceedsBlocks = currentBlockCount + blockCount > TELEGRAM_RICH_MESSAGE_MAX_BLOCKS;
|
|
276
|
+
if (!exceedsChars && !exceedsBlocks) {
|
|
277
|
+
current = candidate;
|
|
278
|
+
currentBlockCount += blockCount;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (current) chunks.push(current.trimEnd());
|
|
282
|
+
if (
|
|
283
|
+
block.length <= TELEGRAM_RICH_MESSAGE_MAX_CHARS &&
|
|
284
|
+
blockCount <= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS
|
|
285
|
+
) {
|
|
286
|
+
current = block;
|
|
287
|
+
currentBlockCount = blockCount;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
chunks.push(...splitTelegramNativeMarkdownLongBlock(block));
|
|
291
|
+
current = "";
|
|
292
|
+
currentBlockCount = 0;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (current) chunks.push(current.trimEnd());
|
|
296
|
+
return chunks;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function splitTelegramNativeMarkdownBlocks(markdown: string): string[] {
|
|
300
|
+
const blocks: string[] = [];
|
|
301
|
+
const current: string[] = [];
|
|
302
|
+
let fence: { marker: "`" | "~"; length: number } | undefined;
|
|
303
|
+
const flush = (): void => {
|
|
304
|
+
if (current.length === 0) return;
|
|
305
|
+
blocks.push(current.join("\n"));
|
|
306
|
+
current.length = 0;
|
|
307
|
+
};
|
|
308
|
+
for (const line of markdown.split("\n")) {
|
|
309
|
+
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
310
|
+
if (!fence && line.trim().length === 0) {
|
|
311
|
+
flush();
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
current.push(line);
|
|
315
|
+
if (!fence && fenceMatch) {
|
|
316
|
+
const markerText = fenceMatch[1] ?? "```";
|
|
317
|
+
fence = { marker: markerText[0] as "`" | "~", length: markerText.length };
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (
|
|
321
|
+
fence &&
|
|
322
|
+
new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)
|
|
323
|
+
) {
|
|
324
|
+
fence = undefined;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
flush();
|
|
328
|
+
return blocks;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function splitTelegramNativeMarkdownCountedBlocks(block: string): string[] {
|
|
332
|
+
if (countTelegramNativeMarkdownBlocks(block) <= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS) {
|
|
333
|
+
return [block];
|
|
334
|
+
}
|
|
335
|
+
const chunks: string[] = [];
|
|
336
|
+
let current: string[] = [];
|
|
337
|
+
let fence: { marker: "`" | "~"; length: number } | undefined;
|
|
338
|
+
for (const line of block.split("\n")) {
|
|
339
|
+
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
340
|
+
if (!fence && current.length >= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS) {
|
|
341
|
+
chunks.push(current.join("\n"));
|
|
342
|
+
current = [];
|
|
343
|
+
}
|
|
344
|
+
current.push(line);
|
|
345
|
+
if (!fence && fenceMatch) {
|
|
346
|
+
const markerText = fenceMatch[1] ?? "```";
|
|
347
|
+
fence = { marker: markerText[0] as "`" | "~", length: markerText.length };
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (
|
|
351
|
+
fence &&
|
|
352
|
+
new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)
|
|
353
|
+
) {
|
|
354
|
+
fence = undefined;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (current.length > 0) chunks.push(current.join("\n"));
|
|
358
|
+
return chunks;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function countTelegramNativeMarkdownBlocks(block: string): number {
|
|
362
|
+
if (/^ {0,3}(`{3,}|~{3,})/.test(block)) return 1;
|
|
363
|
+
const lines = block.split("\n").filter((line) => line.trim().length > 0);
|
|
364
|
+
if (lines.some((line) => /^\s*([-*+] |\d+\. |>|\|)/.test(line))) {
|
|
365
|
+
return Math.max(1, lines.length);
|
|
366
|
+
}
|
|
367
|
+
return 1;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function splitTelegramNativeMarkdownLongBlock(block: string): string[] {
|
|
371
|
+
const chunks: string[] = [];
|
|
372
|
+
let remaining = block;
|
|
373
|
+
while (remaining.length > TELEGRAM_RICH_MESSAGE_MAX_CHARS) {
|
|
374
|
+
const window = remaining.slice(0, TELEGRAM_RICH_MESSAGE_MAX_CHARS + 1);
|
|
375
|
+
const splitIndex = findTelegramNativeMarkdownSplitIndex(window);
|
|
376
|
+
chunks.push(remaining.slice(0, splitIndex).trimEnd());
|
|
377
|
+
remaining = remaining.slice(splitIndex).trimStart();
|
|
378
|
+
}
|
|
379
|
+
if (remaining.length > 0) chunks.push(remaining);
|
|
380
|
+
return chunks;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function findTelegramNativeMarkdownSplitIndex(text: string): number {
|
|
384
|
+
const hardLimit = TELEGRAM_RICH_MESSAGE_MAX_CHARS;
|
|
385
|
+
const paragraphIndex = text.lastIndexOf("\n\n", hardLimit);
|
|
386
|
+
if (paragraphIndex > 0) return paragraphIndex + 2;
|
|
387
|
+
const lineIndex = text.lastIndexOf("\n", hardLimit);
|
|
388
|
+
if (lineIndex > 0) return lineIndex + 1;
|
|
389
|
+
const spaceIndex = text.lastIndexOf(" ", hardLimit);
|
|
390
|
+
if (spaceIndex > 0) return spaceIndex + 1;
|
|
391
|
+
return hardLimit;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
|
|
395
|
+
chatId: number,
|
|
396
|
+
replyToMessageId: number | undefined,
|
|
248
397
|
markdown: string,
|
|
249
|
-
deps:
|
|
398
|
+
deps: {
|
|
399
|
+
sendRichMessage: (
|
|
400
|
+
body: TelegramSendRichMessageBody,
|
|
401
|
+
) => Promise<TelegramSentMessage>;
|
|
402
|
+
},
|
|
250
403
|
options?: { replyMarkup?: TReplyMarkup },
|
|
251
404
|
): Promise<number | undefined> {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
405
|
+
assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
|
|
406
|
+
let lastMessageId: number | undefined;
|
|
407
|
+
const chunks = splitTelegramNativeMarkdown(markdown);
|
|
408
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
409
|
+
const replyParameters =
|
|
410
|
+
index === 0
|
|
411
|
+
? buildTelegramReplyParameters(chatId, replyToMessageId)
|
|
412
|
+
: undefined;
|
|
413
|
+
const sent = await deps.sendRichMessage({
|
|
414
|
+
chat_id: chatId,
|
|
415
|
+
rich_message: { markdown: chunk, skip_entity_detection: true },
|
|
416
|
+
reply_markup: index === chunks.length - 1 ? options?.replyMarkup : undefined,
|
|
417
|
+
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
418
|
+
});
|
|
419
|
+
lastMessageId = sent.message_id;
|
|
255
420
|
}
|
|
256
|
-
return
|
|
421
|
+
return lastMessageId;
|
|
257
422
|
}
|
|
258
423
|
|
|
424
|
+
// UI/compat regular-message runtime for bridge-owned text and interactive
|
|
425
|
+
// surfaces. Assistant and guest Markdown delivery bypass this path and use
|
|
426
|
+
// native Rich Message helpers above.
|
|
259
427
|
export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
|
|
260
428
|
renderTelegramMessage: (
|
|
261
429
|
text: string,
|
|
262
430
|
options?: { mode?: TelegramRenderMode },
|
|
263
431
|
) => TelegramRenderedChunk[];
|
|
264
432
|
replyTransport: TelegramReplyTransport<TReplyMarkup>;
|
|
433
|
+
sendRichMessage: (body: TelegramSendRichMessageBody) => Promise<TelegramSentMessage>;
|
|
265
434
|
}
|
|
266
435
|
|
|
267
436
|
export interface TelegramRenderedMessageRuntime<TReplyMarkup> {
|
|
@@ -305,6 +474,7 @@ export interface TelegramRenderedMessageDeliveryRuntimeDeps<
|
|
|
305
474
|
text: string,
|
|
306
475
|
options?: { mode?: TelegramRenderMode },
|
|
307
476
|
) => TelegramRenderedChunk[];
|
|
477
|
+
sendRichMessage: (body: TelegramSendRichMessageBody) => Promise<TelegramSentMessage>;
|
|
308
478
|
}
|
|
309
479
|
|
|
310
480
|
export function createTelegramRenderedMessageDeliveryRuntime<TReplyMarkup>(
|
|
@@ -320,6 +490,7 @@ export function createTelegramRenderedMessageDeliveryRuntime<TReplyMarkup>(
|
|
|
320
490
|
renderTelegramMessage:
|
|
321
491
|
deps.renderTelegramMessage ?? renderTelegramMessage,
|
|
322
492
|
replyTransport,
|
|
493
|
+
sendRichMessage: deps.sendRichMessage,
|
|
323
494
|
}),
|
|
324
495
|
};
|
|
325
496
|
}
|
|
@@ -342,18 +513,11 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
|
|
|
342
513
|
);
|
|
343
514
|
},
|
|
344
515
|
sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
|
|
345
|
-
return
|
|
516
|
+
return sendTelegramNativeMarkdownReply(
|
|
517
|
+
chatId,
|
|
518
|
+
replyToMessageId,
|
|
346
519
|
markdown,
|
|
347
|
-
{
|
|
348
|
-
renderTelegramMessage: deps.renderTelegramMessage,
|
|
349
|
-
sendRenderedChunks: (chunks, chunkOptions) =>
|
|
350
|
-
deps.replyTransport.sendRenderedChunks(chatId, chunks, {
|
|
351
|
-
replyToMessageId,
|
|
352
|
-
replyMarkup: chunkOptions?.replyMarkup as
|
|
353
|
-
| TReplyMarkup
|
|
354
|
-
| undefined,
|
|
355
|
-
}),
|
|
356
|
-
},
|
|
520
|
+
{ sendRichMessage: deps.sendRichMessage },
|
|
357
521
|
options,
|
|
358
522
|
);
|
|
359
523
|
},
|
|
@@ -431,23 +595,21 @@ export function dedupSendMarkdownReply<TReplyMarkup = unknown>(
|
|
|
431
595
|
}
|
|
432
596
|
|
|
433
597
|
/**
|
|
434
|
-
* Guest reply sender:
|
|
435
|
-
*
|
|
436
|
-
*
|
|
598
|
+
* Guest reply sender: answers guest queries with native Rich Markdown content.
|
|
599
|
+
* Guest queries use InlineQueryResult input_message_content rather than chat
|
|
600
|
+
* sendRichMessage, so this stays as a dedicated guest transport adapter.
|
|
601
|
+
*/
|
|
437
602
|
export function createGuestMarkdownReplySender(deps: {
|
|
438
|
-
renderTelegramMessage: (
|
|
439
|
-
text: string,
|
|
440
|
-
options?: { mode?: TelegramRenderMode },
|
|
441
|
-
) => TelegramRenderedChunk[];
|
|
442
603
|
answerGuestQuery: (
|
|
443
604
|
guestQueryId: string,
|
|
444
605
|
text?: string,
|
|
445
|
-
options?: { parseMode?: string },
|
|
606
|
+
options?: { parseMode?: string; richMessage?: TelegramInputRichMessage },
|
|
446
607
|
) => Promise<void>;
|
|
447
608
|
}) {
|
|
448
609
|
return async (guestQueryId: string, markdown: string) => {
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
610
|
+
const [richMarkdown = markdown] = splitTelegramNativeMarkdown(markdown);
|
|
611
|
+
await deps.answerGuestQuery(guestQueryId, undefined, {
|
|
612
|
+
richMessage: { markdown: richMarkdown, skip_entity_detection: true },
|
|
613
|
+
});
|
|
452
614
|
};
|
|
453
615
|
}
|
package/lib/routing.ts
CHANGED
|
@@ -93,13 +93,13 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
93
93
|
chatId: number,
|
|
94
94
|
messageId: number,
|
|
95
95
|
text: string,
|
|
96
|
-
mode: "html" | "plain",
|
|
96
|
+
mode: "markdown" | "html" | "plain",
|
|
97
97
|
replyMarkup: Menu.TelegramReplyMarkup,
|
|
98
98
|
) => Promise<void>;
|
|
99
99
|
sendInteractiveMessage?: (
|
|
100
100
|
chatId: number,
|
|
101
101
|
text: string,
|
|
102
|
-
mode: "html" | "plain",
|
|
102
|
+
mode: "markdown" | "html" | "plain",
|
|
103
103
|
replyMarkup: Menu.TelegramReplyMarkup,
|
|
104
104
|
) => Promise<number | undefined>;
|
|
105
105
|
deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
|
package/lib/sections.ts
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Owns section registration, global registry binding, token mapping, main-menu/settings row injection, and section callback dispatch
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
assertTelegramCallbackData,
|
|
9
|
+
type TelegramInlineKeyboardMarkup,
|
|
10
|
+
} from "./keyboard.ts";
|
|
8
11
|
|
|
9
12
|
const SECTION_REGISTRY_KEY = "__piTelegramSectionRegistry__";
|
|
10
|
-
const TELEGRAM_CALLBACK_DATA_MAX_BYTES = 64;
|
|
11
13
|
|
|
12
14
|
// --- Core Types ---
|
|
13
15
|
|
|
@@ -22,7 +24,12 @@ export type TelegramSectionCallbackResult = "handled" | "pass";
|
|
|
22
24
|
|
|
23
25
|
export interface TelegramSectionView {
|
|
24
26
|
text: string;
|
|
25
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Source format for companion section content.
|
|
29
|
+
* Defaults to "html" for explicit Telegram UI markup; use "markdown"
|
|
30
|
+
* when a section naturally owns Markdown content, or "plain" for text.
|
|
31
|
+
*/
|
|
32
|
+
parseMode?: "markdown" | "html" | "plain";
|
|
26
33
|
replyMarkup?: TelegramInlineKeyboardMarkup;
|
|
27
34
|
}
|
|
28
35
|
|
|
@@ -136,13 +143,13 @@ export interface TelegramSectionRuntimeDeps {
|
|
|
136
143
|
chatId: number,
|
|
137
144
|
messageId: number,
|
|
138
145
|
text: string,
|
|
139
|
-
mode: "html" | "plain",
|
|
146
|
+
mode: "markdown" | "html" | "plain",
|
|
140
147
|
replyMarkup: TelegramInlineKeyboardMarkup,
|
|
141
148
|
) => Promise<void>;
|
|
142
149
|
sendInteractiveMessage: (
|
|
143
150
|
chatId: number,
|
|
144
151
|
text: string,
|
|
145
|
-
mode: "html" | "plain",
|
|
152
|
+
mode: "markdown" | "html" | "plain",
|
|
146
153
|
replyMarkup: TelegramInlineKeyboardMarkup,
|
|
147
154
|
) => Promise<number | undefined>;
|
|
148
155
|
enqueuePrompt: (prompt: string) => Promise<void>;
|
|
@@ -301,10 +308,6 @@ const BACK_NAV_ROW = {
|
|
|
301
308
|
text: "⬆️ Back",
|
|
302
309
|
} as const;
|
|
303
310
|
|
|
304
|
-
function getUtf8ByteLength(value: string): number {
|
|
305
|
-
return new TextEncoder().encode(value).byteLength;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
311
|
function sectionErrorMessage(error: unknown): string {
|
|
309
312
|
return error instanceof Error ? error.message : String(error);
|
|
310
313
|
}
|
|
@@ -317,13 +320,7 @@ function buildTelegramSectionCallbackData(
|
|
|
317
320
|
const data = payload
|
|
318
321
|
? `section:${token}:${action}:${payload}`
|
|
319
322
|
: `section:${token}:${action}`;
|
|
320
|
-
|
|
321
|
-
if (byteLength > TELEGRAM_CALLBACK_DATA_MAX_BYTES) {
|
|
322
|
-
throw new Error(
|
|
323
|
-
`Telegram section callback_data exceeds ${TELEGRAM_CALLBACK_DATA_MAX_BYTES} bytes (${byteLength}). Use a shorter action/payload or store state behind a compact key.`,
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
return data;
|
|
323
|
+
return assertTelegramCallbackData(data, "Telegram section callback_data");
|
|
327
324
|
}
|
|
328
325
|
|
|
329
326
|
function prependBackRow(
|
|
@@ -513,13 +510,13 @@ export interface TelegramSectionCallbackHandlerDeps {
|
|
|
513
510
|
chatId: number,
|
|
514
511
|
messageId: number,
|
|
515
512
|
text: string,
|
|
516
|
-
mode: "html" | "plain",
|
|
513
|
+
mode: "markdown" | "html" | "plain",
|
|
517
514
|
replyMarkup: TelegramInlineKeyboardMarkup,
|
|
518
515
|
) => Promise<void>;
|
|
519
516
|
sendInteractiveMessage: (
|
|
520
517
|
chatId: number,
|
|
521
518
|
text: string,
|
|
522
|
-
mode: "html" | "plain",
|
|
519
|
+
mode: "markdown" | "html" | "plain",
|
|
523
520
|
replyMarkup: TelegramInlineKeyboardMarkup,
|
|
524
521
|
) => Promise<number | undefined>;
|
|
525
522
|
enqueuePrompt: (prompt: string) => Promise<void>;
|
package/lib/telegram-api.ts
CHANGED
|
@@ -193,11 +193,38 @@ export type TelegramSendMessageBody = Record<string, unknown> & {
|
|
|
193
193
|
reply_parameters?: TelegramReplyParameters;
|
|
194
194
|
};
|
|
195
195
|
|
|
196
|
+
export type TelegramInputRichMessage =
|
|
197
|
+
| {
|
|
198
|
+
markdown: string;
|
|
199
|
+
html?: never;
|
|
200
|
+
is_rtl?: boolean;
|
|
201
|
+
skip_entity_detection?: boolean;
|
|
202
|
+
}
|
|
203
|
+
| {
|
|
204
|
+
html: string;
|
|
205
|
+
markdown?: never;
|
|
206
|
+
is_rtl?: boolean;
|
|
207
|
+
skip_entity_detection?: boolean;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export type TelegramSendRichMessageBody = Record<string, unknown> & {
|
|
211
|
+
chat_id: number;
|
|
212
|
+
rich_message: TelegramInputRichMessage;
|
|
213
|
+
reply_markup?: unknown;
|
|
214
|
+
reply_parameters?: TelegramReplyParameters;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export type TelegramInputRichMessageContent = {
|
|
218
|
+
rich_message: TelegramInputRichMessage;
|
|
219
|
+
};
|
|
220
|
+
|
|
196
221
|
export type TelegramEditMessageTextBody = Record<string, unknown> & {
|
|
197
222
|
chat_id: number;
|
|
198
223
|
message_id: number;
|
|
199
|
-
text
|
|
224
|
+
text?: string;
|
|
225
|
+
rich_message?: TelegramInputRichMessage;
|
|
200
226
|
parse_mode?: "HTML";
|
|
227
|
+
reply_markup?: unknown;
|
|
201
228
|
};
|
|
202
229
|
|
|
203
230
|
export type TelegramSendMessageDraftBody = Record<string, unknown> & {
|
|
@@ -209,6 +236,13 @@ export type TelegramSendMessageDraftBody = Record<string, unknown> & {
|
|
|
209
236
|
message_thread_id?: number;
|
|
210
237
|
};
|
|
211
238
|
|
|
239
|
+
export type TelegramSendRichMessageDraftBody = Record<string, unknown> & {
|
|
240
|
+
chat_id: number;
|
|
241
|
+
draft_id: number;
|
|
242
|
+
rich_message: TelegramInputRichMessage;
|
|
243
|
+
message_thread_id?: number;
|
|
244
|
+
};
|
|
245
|
+
|
|
212
246
|
interface TelegramApiResponse<T> {
|
|
213
247
|
ok: boolean;
|
|
214
248
|
result?: T;
|
|
@@ -234,6 +268,14 @@ export interface TelegramFileDownloadOptions {
|
|
|
234
268
|
maxFileSizeBytes?: number;
|
|
235
269
|
}
|
|
236
270
|
|
|
271
|
+
export interface TelegramAnswerCallbackQueryOptions {
|
|
272
|
+
recordRuntimeEvent?: (
|
|
273
|
+
kind: "api",
|
|
274
|
+
error: unknown,
|
|
275
|
+
details?: Record<string, unknown>,
|
|
276
|
+
) => void;
|
|
277
|
+
}
|
|
278
|
+
|
|
237
279
|
export interface TelegramApiClient {
|
|
238
280
|
call: <TResponse>(
|
|
239
281
|
method: string,
|
|
@@ -261,7 +303,7 @@ export interface TelegramApiClient {
|
|
|
261
303
|
answerGuestQuery?: (
|
|
262
304
|
guestQueryId: string,
|
|
263
305
|
text?: string,
|
|
264
|
-
options?: { parseMode?: string },
|
|
306
|
+
options?: { parseMode?: string; richMessage?: TelegramInputRichMessage },
|
|
265
307
|
) => Promise<void>;
|
|
266
308
|
}
|
|
267
309
|
|
|
@@ -314,6 +356,12 @@ export interface TelegramBridgeApiRuntime {
|
|
|
314
356
|
},
|
|
315
357
|
) => Promise<boolean>;
|
|
316
358
|
sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
|
|
359
|
+
sendRichMessage: (
|
|
360
|
+
body: TelegramSendRichMessageBody,
|
|
361
|
+
) => Promise<TelegramSentMessage>;
|
|
362
|
+
sendRichMessageDraft: (
|
|
363
|
+
body: TelegramSendRichMessageDraftBody,
|
|
364
|
+
) => Promise<boolean>;
|
|
317
365
|
editMessageText: (
|
|
318
366
|
body: TelegramEditMessageTextBody,
|
|
319
367
|
) => Promise<"edited" | "unchanged">;
|
|
@@ -324,7 +372,7 @@ export interface TelegramBridgeApiRuntime {
|
|
|
324
372
|
answerGuestQuery: (
|
|
325
373
|
guestQueryId: string,
|
|
326
374
|
text?: string,
|
|
327
|
-
options?: { parseMode?: string },
|
|
375
|
+
options?: { parseMode?: string; richMessage?: TelegramInputRichMessage },
|
|
328
376
|
) => Promise<void>;
|
|
329
377
|
deleteMessage: (chatId: number, messageId: number) => Promise<void>;
|
|
330
378
|
prepareTempDir: () => Promise<number>;
|
|
@@ -664,6 +712,7 @@ export async function answerTelegramCallbackQuery(
|
|
|
664
712
|
botToken: string | undefined,
|
|
665
713
|
callbackQueryId: string,
|
|
666
714
|
text?: string,
|
|
715
|
+
options: TelegramAnswerCallbackQueryOptions = {},
|
|
667
716
|
): Promise<void> {
|
|
668
717
|
try {
|
|
669
718
|
await callTelegram<boolean>(
|
|
@@ -673,8 +722,10 @@ export async function answerTelegramCallbackQuery(
|
|
|
673
722
|
? { callback_query_id: callbackQueryId, text }
|
|
674
723
|
: { callback_query_id: callbackQueryId },
|
|
675
724
|
);
|
|
676
|
-
} catch {
|
|
677
|
-
|
|
725
|
+
} catch (error) {
|
|
726
|
+
options.recordRuntimeEvent?.("api", error, {
|
|
727
|
+
method: "answerCallbackQuery",
|
|
728
|
+
});
|
|
678
729
|
}
|
|
679
730
|
}
|
|
680
731
|
|
|
@@ -700,12 +751,33 @@ export function createTelegramChatActionSender<TAction extends string>(
|
|
|
700
751
|
return (chatId) => sendChatAction(chatId, action);
|
|
701
752
|
}
|
|
702
753
|
|
|
754
|
+
export function createTelegramNativeMarkdownDraftSender(deps: {
|
|
755
|
+
sendMessageDraft: TelegramBridgeApiRuntime["sendMessageDraft"];
|
|
756
|
+
sendRichMessageDraft: TelegramBridgeApiRuntime["sendRichMessageDraft"];
|
|
757
|
+
}): TelegramBridgeApiRuntime["sendMessageDraft"] {
|
|
758
|
+
return (chatId, draftId, text, options) => {
|
|
759
|
+
if (text === undefined) {
|
|
760
|
+
return deps.sendMessageDraft(chatId, draftId, text, options);
|
|
761
|
+
}
|
|
762
|
+
return deps.sendRichMessageDraft({
|
|
763
|
+
chat_id: chatId,
|
|
764
|
+
draft_id: draftId,
|
|
765
|
+
rich_message: { markdown: text, skip_entity_detection: true },
|
|
766
|
+
...(options?.message_thread_id !== undefined
|
|
767
|
+
? { message_thread_id: options.message_thread_id }
|
|
768
|
+
: {}),
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
703
773
|
export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
704
774
|
getBotToken: () => string | undefined;
|
|
705
775
|
recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
|
|
706
776
|
}): TelegramBridgeApiRuntime {
|
|
707
777
|
return createTelegramBridgeApiRuntime({
|
|
708
|
-
client: createTelegramApiClient(deps.getBotToken
|
|
778
|
+
client: createTelegramApiClient(deps.getBotToken, {
|
|
779
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
780
|
+
}),
|
|
709
781
|
tempDir: getTelegramApiTempDir(),
|
|
710
782
|
maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
|
|
711
783
|
tempFileMaxAgeMs: TELEGRAM_TEMP_FILE_MAX_AGE_MS,
|
|
@@ -824,6 +896,10 @@ export function createTelegramBridgeApiRuntime(
|
|
|
824
896
|
},
|
|
825
897
|
sendMessage: (body) =>
|
|
826
898
|
callRecorded<TelegramSentMessage>("sendMessage", body),
|
|
899
|
+
sendRichMessage: (body) =>
|
|
900
|
+
callRecorded<TelegramSentMessage>("sendRichMessage", body),
|
|
901
|
+
sendRichMessageDraft: (body) =>
|
|
902
|
+
callRecorded<boolean>("sendRichMessageDraft", body),
|
|
827
903
|
editMessageText: async (body) => {
|
|
828
904
|
try {
|
|
829
905
|
await deps.client.call("editMessageText", body);
|
|
@@ -834,20 +910,26 @@ export function createTelegramBridgeApiRuntime(
|
|
|
834
910
|
throw error;
|
|
835
911
|
}
|
|
836
912
|
},
|
|
837
|
-
answerCallbackQuery: (callbackQueryId, text) => {
|
|
838
|
-
|
|
913
|
+
answerCallbackQuery: async (callbackQueryId, text) => {
|
|
914
|
+
try {
|
|
915
|
+
await deps.client.answerCallbackQuery(callbackQueryId, text);
|
|
916
|
+
} catch (error) {
|
|
917
|
+
deps.recordRuntimeEvent("api", error, {
|
|
918
|
+
method: "answerCallbackQuery",
|
|
919
|
+
});
|
|
920
|
+
}
|
|
839
921
|
},
|
|
840
922
|
answerGuestQuery: (
|
|
841
923
|
guestQueryId: string,
|
|
842
924
|
text: string | undefined,
|
|
843
|
-
options: { parseMode?: string } | undefined,
|
|
925
|
+
options: { parseMode?: string; richMessage?: TelegramInputRichMessage } | undefined,
|
|
844
926
|
) => {
|
|
845
927
|
const body: Record<string, unknown> = { guest_query_id: guestQueryId };
|
|
846
|
-
if (text !== undefined) {
|
|
847
|
-
const inputContent: Record<string, unknown> =
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (options?.parseMode) {
|
|
928
|
+
if (text !== undefined || options?.richMessage) {
|
|
929
|
+
const inputContent: Record<string, unknown> = options?.richMessage
|
|
930
|
+
? { rich_message: options.richMessage }
|
|
931
|
+
: { message_text: text };
|
|
932
|
+
if (!options?.richMessage && options?.parseMode) {
|
|
851
933
|
inputContent.parse_mode = options.parseMode;
|
|
852
934
|
}
|
|
853
935
|
body.result = {
|
|
@@ -876,6 +958,7 @@ export function createTelegramBridgeApiRuntime(
|
|
|
876
958
|
*/
|
|
877
959
|
export function createTelegramApiClient(
|
|
878
960
|
getBotToken: () => string | undefined,
|
|
961
|
+
options: TelegramAnswerCallbackQueryOptions = {},
|
|
879
962
|
): TelegramApiClient {
|
|
880
963
|
return {
|
|
881
964
|
call: async (method, body, options) => {
|
|
@@ -909,7 +992,12 @@ export function createTelegramApiClient(
|
|
|
909
992
|
);
|
|
910
993
|
},
|
|
911
994
|
answerCallbackQuery: async (callbackQueryId, text) => {
|
|
912
|
-
await answerTelegramCallbackQuery(
|
|
995
|
+
await answerTelegramCallbackQuery(
|
|
996
|
+
getBotToken(),
|
|
997
|
+
callbackQueryId,
|
|
998
|
+
text,
|
|
999
|
+
options,
|
|
1000
|
+
);
|
|
913
1001
|
},
|
|
914
1002
|
};
|
|
915
1003
|
}
|
package/lib/updates.ts
CHANGED
|
@@ -406,7 +406,8 @@ export function buildTelegramUpdateExecutionPlan<
|
|
|
406
406
|
return {
|
|
407
407
|
kind: "guest",
|
|
408
408
|
guestMessage: action.guestMessage,
|
|
409
|
-
|
|
409
|
+
// Guest mode is an extension of an already paired bridge, not a pairing surface.
|
|
410
|
+
shouldDeny: action.authorization.kind !== "allow",
|
|
410
411
|
};
|
|
411
412
|
}
|
|
412
413
|
}
|