@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/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-message delivery, reply transport wiring, and plain or markdown final replies
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
- let lastRepliedToMessageId: number | undefined;
59
+ const lastRepliedToMessageIdByChat = new Map<number, number>();
54
60
 
55
61
  export function resetTransportReplyDedup(): void {
56
- lastRepliedToMessageId = undefined;
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 (messageId === lastRepliedToMessageId) return undefined;
64
- lastRepliedToMessageId = messageId;
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: string;
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 async function sendTelegramMarkdownReply<TReplyMarkup = unknown>(
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: TelegramReplyRuntimeDeps,
398
+ deps: {
399
+ sendRichMessage: (
400
+ body: TelegramSendRichMessageBody,
401
+ ) => Promise<TelegramSentMessage>;
402
+ },
250
403
  options?: { replyMarkup?: TReplyMarkup },
251
404
  ): Promise<number | undefined> {
252
- const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
253
- if (chunks.length === 0) {
254
- return sendTelegramPlainReply(markdown, deps);
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 deps.sendRenderedChunks(chunks, options);
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 sendTelegramMarkdownReply(
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: renders Markdown HTML, sends via answerGuestQuery.
435
- * Keeps guest rendering inside the replies domain so the orchestration layer
436
- * (index.ts) does not import from rendering.ts directly. */
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 chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
450
- const html = chunks.length > 0 ? chunks[0].text : markdown;
451
- await deps.answerGuestQuery(guestQueryId, html, { parseMode: "HTML" });
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 type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
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
- parseMode?: "html" | "plain";
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
- const byteLength = getUtf8ByteLength(data);
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>;
@@ -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: string;
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
- // ignore
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
- return deps.client.answerCallbackQuery(callbackQueryId, text);
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
- message_text: text,
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(getBotToken(), callbackQueryId, text);
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
- shouldDeny: action.authorization.kind === "deny",
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.16.5",
3
+ "version": "0.17.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"