@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/docs/ui-style.md CHANGED
@@ -46,7 +46,7 @@ Examples:
46
46
 
47
47
  ## Boolean Toggles
48
48
 
49
- Boolean settings use a horizontal `On` / `Off` pair, like a checkbox stretched across two buttons.
49
+ Boolean settings use a horizontal `On` / `Off` pair.
50
50
 
51
51
  Rules:
52
52
 
package/index.ts CHANGED
@@ -147,6 +147,8 @@ export default function (pi: Pi.ExtensionAPI) {
147
147
  sendRecordVoiceAction,
148
148
  sendMessageDraft,
149
149
  sendMessage,
150
+ sendRichMessage,
151
+ sendRichMessageDraft,
150
152
  downloadFile: downloadTelegramBridgeFile,
151
153
  editMessageText: editTelegramMessageText,
152
154
  answerCallbackQuery,
@@ -161,7 +163,6 @@ export default function (pi: Pi.ExtensionAPI) {
161
163
  // --- Message Delivery ---
162
164
 
163
165
  const sendGuestReply = Replies.createGuestMarkdownReplySender({
164
- renderTelegramMessage: Replies.renderTelegramMessage,
165
166
  answerGuestQuery,
166
167
  });
167
168
 
@@ -189,6 +190,7 @@ export default function (pi: Pi.ExtensionAPI) {
189
190
 
190
191
  const replyRuntime = Replies.createTelegramRenderedMessageDeliveryRuntime({
191
192
  sendMessage,
193
+ sendRichMessage,
192
194
  editMessage: editTelegramMessageText,
193
195
  });
194
196
  const { replyTransport, editInteractiveMessage, sendInteractiveMessage } =
@@ -221,9 +223,16 @@ export default function (pi: Pi.ExtensionAPI) {
221
223
  isAssistantMessage: Replies.isAssistantAgentMessage,
222
224
  getMessageText: Replies.getAgentMessageText,
223
225
  getDefaultReplyToMessageId: activeTurnRuntime.getReplyToMessageId,
224
- sendDraft: sendMessageDraft,
226
+ sendDraft: TelegramApi.createTelegramNativeMarkdownDraftSender({
227
+ sendMessageDraft,
228
+ sendRichMessageDraft,
229
+ }),
225
230
  sendMessage,
226
231
  editMessageText: editTelegramMessageText,
232
+ sendMarkdownReply,
233
+ editMarkdownMessage: Preview.createTelegramNativeMarkdownMessageEditor({
234
+ editMessageText: editTelegramMessageText,
235
+ }),
227
236
  recordRuntimeEvent,
228
237
  ...replyTransport,
229
238
  });
package/lib/commands.ts CHANGED
@@ -494,7 +494,7 @@ export interface TelegramCompactConfirmationDeps {
494
494
  sendInteractiveMessage: (
495
495
  chatId: number,
496
496
  text: string,
497
- mode: "html" | "plain",
497
+ mode: "markdown" | "html" | "plain",
498
498
  replyMarkup: TelegramCompactConfirmationReplyMarkup,
499
499
  ) => Promise<number | undefined>;
500
500
  }
@@ -515,7 +515,7 @@ export interface TelegramCompactConfirmationCallbackDeps<TContext> {
515
515
  chatId: number,
516
516
  messageId: number,
517
517
  text: string,
518
- mode: "html" | "plain",
518
+ mode: "markdown" | "html" | "plain",
519
519
  replyMarkup: TelegramCompactConfirmationReplyMarkup,
520
520
  ) => Promise<void>;
521
521
  runCompact: (
package/lib/keyboard.ts CHANGED
@@ -12,3 +12,42 @@ export interface TelegramInlineKeyboardButton {
12
12
  export interface TelegramInlineKeyboardMarkup {
13
13
  inline_keyboard: TelegramInlineKeyboardButton[][];
14
14
  }
15
+
16
+ export const TELEGRAM_CALLBACK_DATA_MAX_BYTES = 64;
17
+
18
+ export function getTelegramCallbackDataByteLength(value: string): number {
19
+ return new TextEncoder().encode(value).byteLength;
20
+ }
21
+
22
+ export function assertTelegramCallbackData(
23
+ callbackData: string,
24
+ context = "Telegram callback_data",
25
+ ): string {
26
+ const byteLength = getTelegramCallbackDataByteLength(callbackData);
27
+ if (byteLength > TELEGRAM_CALLBACK_DATA_MAX_BYTES) {
28
+ throw new Error(
29
+ `${context} exceeds ${TELEGRAM_CALLBACK_DATA_MAX_BYTES} bytes (${byteLength}). Use a shorter action/payload or store state behind a compact key.`,
30
+ );
31
+ }
32
+ return callbackData;
33
+ }
34
+
35
+ export function assertTelegramInlineKeyboardCallbackData(
36
+ replyMarkup: unknown,
37
+ context = "Telegram inline keyboard callback_data",
38
+ ): void {
39
+ if (!replyMarkup || typeof replyMarkup !== "object") return;
40
+ const keyboard = (replyMarkup as { inline_keyboard?: unknown })
41
+ .inline_keyboard;
42
+ if (!Array.isArray(keyboard)) return;
43
+ for (const row of keyboard) {
44
+ if (!Array.isArray(row)) continue;
45
+ for (const button of row) {
46
+ if (!button || typeof button !== "object") continue;
47
+ const callbackData = (button as { callback_data?: unknown })
48
+ .callback_data;
49
+ if (typeof callbackData !== "string") continue;
50
+ assertTelegramCallbackData(callbackData, context);
51
+ }
52
+ }
53
+ }
package/lib/lifecycle.ts CHANGED
@@ -138,6 +138,11 @@ export function createTelegramSessionContextTracker(
138
138
 
139
139
  type TelegramLifecycleTimer = number | ReturnType<typeof setTimeout>;
140
140
 
141
+ function unrefTelegramLifecycleTimer(timer: TelegramLifecycleTimer): void {
142
+ if (!timer || typeof timer !== "object") return;
143
+ if (typeof timer.unref === "function") timer.unref();
144
+ }
145
+
141
146
  export interface TelegramCompactionObserverRuntimeDeps<TContext> {
142
147
  setCompactionInProgress: (inProgress: boolean) => void;
143
148
  updateStatus: (ctx: TContext) => void;
@@ -196,6 +201,7 @@ export function createTelegramCompactionObserverRuntime<TContext>(
196
201
  );
197
202
  requestDispatch();
198
203
  }, timeoutMs);
204
+ unrefTelegramLifecycleTimer(fallbackTimer);
199
205
  },
200
206
  onSessionCompact: (_event, ctx) => {
201
207
  clearFallbackTimer();
package/lib/menu-model.ts CHANGED
@@ -128,13 +128,13 @@ export interface TelegramMenuMessageRuntimeDeps {
128
128
  chatId: number,
129
129
  messageId: number,
130
130
  text: string,
131
- mode: "html" | "plain",
131
+ mode: "markdown" | "html" | "plain",
132
132
  replyMarkup: TelegramReplyMarkup,
133
133
  ) => Promise<void>;
134
134
  sendInteractiveMessage: (
135
135
  chatId: number,
136
136
  text: string,
137
- mode: "html" | "plain",
137
+ mode: "markdown" | "html" | "plain",
138
138
  replyMarkup: TelegramReplyMarkup,
139
139
  ) => Promise<number | undefined>;
140
140
  }
@@ -200,7 +200,7 @@ export interface TelegramModelMenuPage<TModel extends MenuModel = MenuModel> {
200
200
  export interface TelegramMenuRenderPayload {
201
201
  nextMode: TelegramModelMenuState["mode"];
202
202
  text: string;
203
- mode: "html" | "plain";
203
+ mode: "markdown" | "html" | "plain";
204
204
  replyMarkup: TelegramReplyMarkup;
205
205
  }
206
206
 
@@ -98,13 +98,13 @@ export interface TelegramSettingsMenuRuntimeDeps<
98
98
  chatId: number,
99
99
  messageId: number,
100
100
  text: string,
101
- mode: "html" | "plain",
101
+ mode: "markdown" | "html" | "plain",
102
102
  replyMarkup: TelegramSettingsMenuReplyMarkup,
103
103
  ) => Promise<void>;
104
104
  sendInteractiveMessage: (
105
105
  chatId: number,
106
106
  text: string,
107
- mode: "html" | "plain",
107
+ mode: "markdown" | "html" | "plain",
108
108
  replyMarkup: TelegramSettingsMenuReplyMarkup,
109
109
  ) => Promise<number | undefined>;
110
110
  answerCallbackQuery: (
package/lib/menu.ts CHANGED
@@ -228,13 +228,13 @@ export interface TelegramMenuCallbackRuntimeDeps<
228
228
  chatId: number,
229
229
  messageId: number,
230
230
  text: string,
231
- mode: "html" | "plain",
231
+ mode: "markdown" | "html" | "plain",
232
232
  replyMarkup: TelegramReplyMarkup,
233
233
  ) => Promise<void>;
234
234
  sendInteractiveMessage?: (
235
235
  chatId: number,
236
236
  text: string,
237
- mode: "html" | "plain",
237
+ mode: "markdown" | "html" | "plain",
238
238
  replyMarkup: TelegramReplyMarkup,
239
239
  ) => Promise<number | undefined>;
240
240
  enqueueSectionPrompt?: (prompt: string, ctx: TContext) => Promise<void>;
@@ -464,13 +464,13 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
464
464
  chatId: number,
465
465
  messageId: number,
466
466
  text: string,
467
- mode: "html" | "plain",
467
+ mode: "markdown" | "html" | "plain",
468
468
  replyMarkup: TelegramReplyMarkup,
469
469
  ) => Promise<void>;
470
470
  sendInteractiveMessage?: (
471
471
  chatId: number,
472
472
  text: string,
473
- mode: "html" | "plain",
473
+ mode: "markdown" | "html" | "plain",
474
474
  replyMarkup: TelegramReplyMarkup,
475
475
  ) => Promise<number | undefined>;
476
476
  enqueueSectionPrompt?: (prompt: string, ctx: TContext) => Promise<void>;
@@ -474,6 +474,7 @@ export async function sendQueuedTelegramOutboundAttachments(
474
474
  const method = isPhoto ? "sendPhoto" : "sendDocument";
475
475
  const fieldName = isPhoto ? "photo" : "document";
476
476
  const replyParameters = buildTelegramMultipartReplyParameters(
477
+ turn.chatId,
477
478
  turn.replyToMessageId,
478
479
  );
479
480
  await deps.sendMultipart(
@@ -135,6 +135,7 @@ export function createTelegramButtonActionStore(
135
135
  cleanup(currentTime);
136
136
  const action = actions.get(callbackData);
137
137
  if (!action) return undefined;
138
+ actions.delete(callbackData);
138
139
  return { text: action.text, prompt: action.prompt };
139
140
  },
140
141
  };
@@ -7,6 +7,7 @@
7
7
  import { unlink } from "node:fs/promises";
8
8
  import { basename, extname } from "node:path";
9
9
 
10
+ import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
10
11
  import { getTelegramVoiceSynthesisProviders } from "./voice.ts";
11
12
 
12
13
  export interface TelegramVoiceReplyTurnView {
@@ -125,6 +126,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
125
126
  },
126
127
  ): Promise<void> {
127
128
  const voiceFilePath = await ensureTelegramVoiceFileFormat(filePath);
129
+ assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
128
130
  await sendVoiceChatAction(deps, turn.chatId);
129
131
  const replyParameters = buildVoiceReplyParameters(
130
132
  options?.replyToPrompt,
package/lib/polling.ts CHANGED
@@ -187,11 +187,21 @@ export function shouldStartTelegramPolling(
187
187
  export async function stopTelegramPollingRuntime<TContext>(
188
188
  deps: TelegramPollingRuntimeDeps<TContext>,
189
189
  ): Promise<void> {
190
- deps.stopTypingLoop();
191
- deps.getPollingController()?.abort();
192
- deps.setPollingController(undefined);
193
- await deps.getPollingPromise()?.catch(() => undefined);
194
- deps.setPollingPromise(undefined);
190
+ const pollingPromise = deps.getPollingPromise();
191
+ const pollingController = deps.getPollingController();
192
+ try {
193
+ deps.stopTypingLoop();
194
+ } catch (error) {
195
+ deps.recordRuntimeEvent?.("polling", error, { phase: "typing-stop" });
196
+ }
197
+ pollingController?.abort();
198
+ await pollingPromise?.catch(() => undefined);
199
+ if (deps.getPollingPromise() === pollingPromise) {
200
+ deps.setPollingPromise(undefined);
201
+ }
202
+ if (deps.getPollingController() === pollingController) {
203
+ deps.setPollingController(undefined);
204
+ }
195
205
  }
196
206
 
197
207
  function updateTelegramPollingStatusSafely<TContext>(
@@ -224,9 +234,12 @@ export function startTelegramPollingRuntime<TContext>(
224
234
  }
225
235
  const controller = deps.createAbortController?.() ?? new AbortController();
226
236
  deps.setPollingController(controller);
227
- const promise = deps.runPollLoop(ctx, controller.signal).finally(() => {
228
- deps.setPollingPromise(undefined);
229
- deps.setPollingController(undefined);
237
+ let promise: Promise<void>;
238
+ promise = deps.runPollLoop(ctx, controller.signal).finally(() => {
239
+ if (deps.getPollingPromise() === promise) deps.setPollingPromise(undefined);
240
+ if (deps.getPollingController() === controller) {
241
+ deps.setPollingController(undefined);
242
+ }
230
243
  updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
231
244
  recordRuntimeEvent: deps.recordRuntimeEvent,
232
245
  });