@llblab/pi-telegram 0.2.8 → 0.2.10

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,187 +1,10 @@
1
1
  /**
2
- * Telegram reply and preview domain helpers
3
- * Owns preview text decisions, preview runtime behavior, rendered-message delivery, and plain or markdown reply sending
2
+ * Telegram reply delivery helpers
3
+ * Owns rendered-message delivery, reply transport wiring, and plain or markdown final replies
4
4
  */
5
5
 
6
6
  import type { TelegramRenderedChunk, TelegramRenderMode } from "./rendering.ts";
7
7
 
8
- // --- Preview ---
9
-
10
- export interface TelegramPreviewStateLike {
11
- mode: "draft" | "message";
12
- draftId?: number;
13
- messageId?: number;
14
- pendingText: string;
15
- lastSentText: string;
16
- }
17
-
18
- export interface TelegramPreviewRuntimeState extends TelegramPreviewStateLike {
19
- flushTimer?: ReturnType<typeof setTimeout>;
20
- }
21
-
22
- export interface TelegramPreviewRuntimeDeps {
23
- getState: () => TelegramPreviewRuntimeState | undefined;
24
- setState: (state: TelegramPreviewRuntimeState | undefined) => void;
25
- clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
26
- maxMessageLength: number;
27
- renderPreviewText: (markdown: string) => string;
28
- getDraftSupport: () => "unknown" | "supported" | "unsupported";
29
- setDraftSupport: (support: "unknown" | "supported" | "unsupported") => void;
30
- allocateDraftId: () => number;
31
- sendDraft: (chatId: number, draftId: number, text: string) => Promise<void>;
32
- sendMessage: (
33
- chatId: number,
34
- text: string,
35
- ) => Promise<TelegramSentMessageLike>;
36
- editMessageText: (
37
- chatId: number,
38
- messageId: number,
39
- text: string,
40
- ) => Promise<void>;
41
- renderTelegramMessage: (
42
- text: string,
43
- options?: { mode?: TelegramRenderMode },
44
- ) => TelegramRenderedChunk[];
45
- sendRenderedChunks: (
46
- chatId: number,
47
- chunks: TelegramRenderedChunk[],
48
- ) => Promise<number | undefined>;
49
- editRenderedMessage: (
50
- chatId: number,
51
- messageId: number,
52
- chunks: TelegramRenderedChunk[],
53
- ) => Promise<number | undefined>;
54
- }
55
-
56
- export function buildTelegramPreviewFlushText(options: {
57
- state: TelegramPreviewStateLike;
58
- maxMessageLength: number;
59
- renderPreviewText: (markdown: string) => string;
60
- }): string | undefined {
61
- const rawText = options.state.pendingText.trim();
62
- const previewText = options.renderPreviewText(rawText).trim();
63
- if (!previewText || previewText === options.state.lastSentText) {
64
- return undefined;
65
- }
66
- return previewText.length > options.maxMessageLength
67
- ? previewText.slice(0, options.maxMessageLength)
68
- : previewText;
69
- }
70
-
71
- export function buildTelegramPreviewFinalText(
72
- state: TelegramPreviewStateLike,
73
- ): string | undefined {
74
- const finalText = (state.pendingText.trim() || state.lastSentText).trim();
75
- return finalText || undefined;
76
- }
77
-
78
- export function shouldUseTelegramDraftPreview(options: {
79
- draftSupport: "unknown" | "supported" | "unsupported";
80
- }): boolean {
81
- return options.draftSupport !== "unsupported";
82
- }
83
-
84
- export async function clearTelegramPreview(
85
- chatId: number,
86
- deps: TelegramPreviewRuntimeDeps,
87
- ): Promise<void> {
88
- const state = deps.getState();
89
- if (!state) return;
90
- deps.clearScheduledFlush(state);
91
- deps.setState(undefined);
92
- if (state.mode !== "draft" || state.draftId === undefined) return;
93
- try {
94
- await deps.sendDraft(chatId, state.draftId, "");
95
- } catch {
96
- // ignore
97
- }
98
- }
99
-
100
- export async function flushTelegramPreview(
101
- chatId: number,
102
- deps: TelegramPreviewRuntimeDeps,
103
- ): Promise<void> {
104
- const state = deps.getState();
105
- if (!state) return;
106
- state.flushTimer = undefined;
107
- const truncated = buildTelegramPreviewFlushText({
108
- state,
109
- maxMessageLength: deps.maxMessageLength,
110
- renderPreviewText: deps.renderPreviewText,
111
- });
112
- if (!truncated) return;
113
- if (shouldUseTelegramDraftPreview({ draftSupport: deps.getDraftSupport() })) {
114
- const draftId = state.draftId ?? deps.allocateDraftId();
115
- state.draftId = draftId;
116
- try {
117
- await deps.sendDraft(chatId, draftId, truncated);
118
- deps.setDraftSupport("supported");
119
- state.mode = "draft";
120
- state.lastSentText = truncated;
121
- return;
122
- } catch {
123
- deps.setDraftSupport("unsupported");
124
- }
125
- }
126
- if (state.messageId === undefined) {
127
- const sent = await deps.sendMessage(chatId, truncated);
128
- state.messageId = sent.message_id;
129
- state.mode = "message";
130
- state.lastSentText = truncated;
131
- return;
132
- }
133
- await deps.editMessageText(chatId, state.messageId, truncated);
134
- state.mode = "message";
135
- state.lastSentText = truncated;
136
- }
137
-
138
- export async function finalizeTelegramPreview(
139
- chatId: number,
140
- deps: TelegramPreviewRuntimeDeps,
141
- ): Promise<boolean> {
142
- const state = deps.getState();
143
- if (!state) return false;
144
- await flushTelegramPreview(chatId, deps);
145
- const finalText = buildTelegramPreviewFinalText(state);
146
- if (!finalText) {
147
- await clearTelegramPreview(chatId, deps);
148
- return false;
149
- }
150
- if (state.mode === "draft") {
151
- await deps.sendMessage(chatId, finalText);
152
- await clearTelegramPreview(chatId, deps);
153
- return true;
154
- }
155
- deps.setState(undefined);
156
- return state.messageId !== undefined;
157
- }
158
-
159
- export async function finalizeTelegramMarkdownPreview(
160
- chatId: number,
161
- markdown: string,
162
- deps: TelegramPreviewRuntimeDeps,
163
- ): Promise<boolean> {
164
- const state = deps.getState();
165
- if (!state) return false;
166
- await flushTelegramPreview(chatId, deps);
167
- const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
168
- if (chunks.length === 0) {
169
- await clearTelegramPreview(chatId, deps);
170
- return false;
171
- }
172
- if (state.mode === "draft") {
173
- await deps.sendRenderedChunks(chatId, chunks);
174
- await clearTelegramPreview(chatId, deps);
175
- return true;
176
- }
177
- if (state.messageId === undefined) return false;
178
- await deps.editRenderedMessage(chatId, state.messageId, chunks);
179
- deps.setState(undefined);
180
- return true;
181
- }
182
-
183
- // --- Delivery ---
184
-
185
8
  export interface TelegramSentMessageLike {
186
9
  message_id: number;
187
10
  }
@@ -278,8 +101,6 @@ export async function editTelegramRenderedMessage<TReplyMarkup>(
278
101
  return messageId;
279
102
  }
280
103
 
281
- // --- Reply Runtime ---
282
-
283
104
  export interface TelegramReplyRuntimeDeps {
284
105
  renderTelegramMessage: (
285
106
  text: string,
package/lib/turns.ts CHANGED
@@ -89,6 +89,92 @@ export function buildTelegramTurnPrompt(options: {
89
89
  return prompt;
90
90
  }
91
91
 
92
+ function splitTelegramPromptAttachmentSuffix(prompt: string): {
93
+ promptWithoutAttachments: string;
94
+ attachmentSuffix: string;
95
+ attachmentFiles: DownloadedTelegramTurnFileLike[];
96
+ } {
97
+ const marker = "\n\nTelegram attachments were saved locally:";
98
+ const markerIndex = prompt.indexOf(marker);
99
+ if (markerIndex === -1) {
100
+ return {
101
+ promptWithoutAttachments: prompt,
102
+ attachmentSuffix: "",
103
+ attachmentFiles: [],
104
+ };
105
+ }
106
+ const promptWithoutAttachments = prompt.slice(0, markerIndex);
107
+ const attachmentSuffix = prompt.slice(markerIndex);
108
+ const attachmentFiles = attachmentSuffix
109
+ .split("\n")
110
+ .map((line) => line.match(/^- (.+)$/)?.[1]?.trim())
111
+ .filter((path): path is string => !!path)
112
+ .map((path) => ({ path, fileName: basename(path), isImage: false }));
113
+ return { promptWithoutAttachments, attachmentSuffix, attachmentFiles };
114
+ }
115
+
116
+ function buildEditedTelegramPromptText(options: {
117
+ existingPrompt: string;
118
+ telegramPrefix: string;
119
+ rawText: string;
120
+ }): { text: string; attachmentFiles: DownloadedTelegramTurnFileLike[] } {
121
+ const { promptWithoutAttachments, attachmentSuffix, attachmentFiles } =
122
+ splitTelegramPromptAttachmentSuffix(options.existingPrompt);
123
+ const currentMessageMarker = "Current Telegram message:";
124
+ const currentMessageIndex = promptWithoutAttachments.lastIndexOf(
125
+ currentMessageMarker,
126
+ );
127
+ if (currentMessageIndex !== -1) {
128
+ const prefix = promptWithoutAttachments.slice(
129
+ 0,
130
+ currentMessageIndex + currentMessageMarker.length,
131
+ );
132
+ const separator = options.rawText.length > 0 ? "\n" : "";
133
+ return {
134
+ text: `${prefix}${separator}${options.rawText}${attachmentSuffix}`,
135
+ attachmentFiles,
136
+ };
137
+ }
138
+ const promptText =
139
+ options.rawText.length > 0
140
+ ? `${options.telegramPrefix} ${options.rawText}`
141
+ : options.telegramPrefix;
142
+ return {
143
+ text: `${promptText}${attachmentSuffix}`,
144
+ attachmentFiles,
145
+ };
146
+ }
147
+
148
+ export function updateTelegramPromptTurnText(options: {
149
+ turn: PendingTelegramTurn;
150
+ telegramPrefix: string;
151
+ rawText: string;
152
+ }): PendingTelegramTurn {
153
+ let attachmentFiles: DownloadedTelegramTurnFileLike[] = [];
154
+ const nextContent = options.turn.content.map((block, index) => {
155
+ if (index !== 0 || block.type !== "text") return block;
156
+ const updated = buildEditedTelegramPromptText({
157
+ existingPrompt: block.text,
158
+ telegramPrefix: options.telegramPrefix,
159
+ rawText: options.rawText,
160
+ });
161
+ attachmentFiles = updated.attachmentFiles;
162
+ return {
163
+ ...block,
164
+ text: updated.text,
165
+ };
166
+ });
167
+ return {
168
+ ...options.turn,
169
+ content: nextContent,
170
+ historyText: formatTelegramHistoryText(options.rawText, attachmentFiles),
171
+ statusSummary: formatTelegramTurnStatusSummary(
172
+ options.rawText,
173
+ attachmentFiles,
174
+ ),
175
+ };
176
+ }
177
+
92
178
  export async function buildTelegramPromptTurn(options: {
93
179
  telegramPrefix: string;
94
180
  messages: TelegramTurnMessageLike[];
package/lib/types.ts ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Telegram Bot API transport shape types
3
+ * Centralizes Telegram update, message, callback, reaction, and response shapes used by the runtime
4
+ */
5
+
6
+ export interface TelegramApiResponse<T> {
7
+ ok: boolean;
8
+ result?: T;
9
+ description?: string;
10
+ error_code?: number;
11
+ }
12
+
13
+ export interface TelegramUser {
14
+ id: number;
15
+ is_bot: boolean;
16
+ first_name: string;
17
+ username?: string;
18
+ }
19
+
20
+ export interface TelegramChat {
21
+ id: number;
22
+ type: string;
23
+ }
24
+
25
+ export interface TelegramPhotoSize {
26
+ file_id: string;
27
+ file_size?: number;
28
+ }
29
+
30
+ export interface TelegramDocument {
31
+ file_id: string;
32
+ file_name?: string;
33
+ mime_type?: string;
34
+ file_size?: number;
35
+ }
36
+
37
+ export interface TelegramVideo {
38
+ file_id: string;
39
+ file_name?: string;
40
+ mime_type?: string;
41
+ file_size?: number;
42
+ }
43
+
44
+ export interface TelegramAudio {
45
+ file_id: string;
46
+ file_name?: string;
47
+ mime_type?: string;
48
+ file_size?: number;
49
+ }
50
+
51
+ export interface TelegramVoice {
52
+ file_id: string;
53
+ mime_type?: string;
54
+ file_size?: number;
55
+ }
56
+
57
+ export interface TelegramAnimation {
58
+ file_id: string;
59
+ file_name?: string;
60
+ mime_type?: string;
61
+ file_size?: number;
62
+ }
63
+
64
+ export interface TelegramSticker {
65
+ file_id: string;
66
+ emoji?: string;
67
+ }
68
+
69
+ export interface TelegramMessage {
70
+ message_id: number;
71
+ chat: TelegramChat;
72
+ from?: TelegramUser;
73
+ text?: string;
74
+ caption?: string;
75
+ media_group_id?: string;
76
+ photo?: TelegramPhotoSize[];
77
+ document?: TelegramDocument;
78
+ video?: TelegramVideo;
79
+ audio?: TelegramAudio;
80
+ voice?: TelegramVoice;
81
+ animation?: TelegramAnimation;
82
+ sticker?: TelegramSticker;
83
+ }
84
+
85
+ export interface TelegramCallbackQuery {
86
+ id: string;
87
+ from: TelegramUser;
88
+ message?: TelegramMessage;
89
+ data?: string;
90
+ }
91
+
92
+ export interface TelegramReactionTypeEmoji {
93
+ type: "emoji";
94
+ emoji: string;
95
+ }
96
+
97
+ export interface TelegramReactionTypeCustomEmoji {
98
+ type: "custom_emoji";
99
+ custom_emoji_id: string;
100
+ }
101
+
102
+ export interface TelegramReactionTypePaid {
103
+ type: "paid";
104
+ }
105
+
106
+ export type TelegramReactionType =
107
+ | TelegramReactionTypeEmoji
108
+ | TelegramReactionTypeCustomEmoji
109
+ | TelegramReactionTypePaid;
110
+
111
+ export interface TelegramMessageReactionUpdated {
112
+ chat: TelegramChat;
113
+ message_id: number;
114
+ user?: TelegramUser;
115
+ actor_chat?: TelegramChat;
116
+ old_reaction: TelegramReactionType[];
117
+ new_reaction: TelegramReactionType[];
118
+ date: number;
119
+ }
120
+
121
+ export interface TelegramUpdate {
122
+ update_id: number;
123
+ message?: TelegramMessage;
124
+ edited_message?: TelegramMessage;
125
+ callback_query?: TelegramCallbackQuery;
126
+ message_reaction?: TelegramMessageReactionUpdated;
127
+ deleted_business_messages?: { message_ids?: unknown };
128
+ }
129
+
130
+ export interface TelegramSentMessage {
131
+ message_id: number;
132
+ }
133
+
134
+ export interface TelegramBotCommand {
135
+ command: string;
136
+ description: string;
137
+ }
package/lib/updates.ts CHANGED
@@ -119,7 +119,22 @@ export function getAuthorizedTelegramCallbackQuery(
119
119
  export function getAuthorizedTelegramMessage(
120
120
  update: TelegramUpdateRoutingLike,
121
121
  ): TelegramMessageLike | undefined {
122
- const message = update.message || update.edited_message;
122
+ const message = update.message;
123
+ if (
124
+ !message ||
125
+ message.chat.type !== "private" ||
126
+ !message.from ||
127
+ message.from.is_bot
128
+ ) {
129
+ return undefined;
130
+ }
131
+ return message;
132
+ }
133
+
134
+ export function getAuthorizedTelegramEditedMessage(
135
+ update: TelegramUpdateRoutingLike,
136
+ ): TelegramMessageLike | undefined {
137
+ const message = update.edited_message;
123
138
  if (
124
139
  !message ||
125
140
  message.chat.type !== "private" ||
@@ -156,6 +171,11 @@ export type TelegramUpdateFlowAction =
156
171
  kind: "message";
157
172
  message: TelegramMessageLike & { from: TelegramUserLike };
158
173
  authorization: TelegramAuthorizationState;
174
+ }
175
+ | {
176
+ kind: "edited-message";
177
+ message: TelegramMessageLike & { from: TelegramUserLike };
178
+ authorization: TelegramAuthorizationState;
159
179
  };
160
180
 
161
181
  export function buildTelegramUpdateFlowAction(
@@ -191,6 +211,19 @@ export function buildTelegramUpdateFlowAction(
191
211
  ),
192
212
  };
193
213
  }
214
+ const editedMessage = getAuthorizedTelegramEditedMessage(update);
215
+ if (editedMessage?.from) {
216
+ return {
217
+ kind: "edited-message",
218
+ message: editedMessage as TelegramMessageLike & {
219
+ from: TelegramUserLike;
220
+ },
221
+ authorization: getTelegramAuthorizationState(
222
+ editedMessage.from.id,
223
+ allowedUserId,
224
+ ),
225
+ };
226
+ }
194
227
  return { kind: "ignore" };
195
228
  }
196
229
 
@@ -215,6 +248,12 @@ export type TelegramUpdateExecutionPlan =
215
248
  shouldPair: boolean;
216
249
  shouldNotifyPaired: boolean;
217
250
  shouldDeny: boolean;
251
+ }
252
+ | {
253
+ kind: "edited-message";
254
+ message: TelegramMessageLike & { from: TelegramUserLike };
255
+ shouldPair: boolean;
256
+ shouldDeny: boolean;
218
257
  };
219
258
 
220
259
  export function buildTelegramUpdateExecutionPlan(
@@ -242,6 +281,13 @@ export function buildTelegramUpdateExecutionPlan(
242
281
  shouldNotifyPaired: action.authorization.kind === "pair",
243
282
  shouldDeny: action.authorization.kind === "deny",
244
283
  };
284
+ case "edited-message":
285
+ return {
286
+ kind: "edited-message",
287
+ message: action.message,
288
+ shouldPair: action.authorization.kind === "pair",
289
+ shouldDeny: action.authorization.kind === "deny",
290
+ };
245
291
  }
246
292
  }
247
293
 
@@ -296,6 +342,13 @@ export interface TelegramUpdateRuntimeDeps {
296
342
  >["message"],
297
343
  ctx: ExtensionContext,
298
344
  ) => Promise<void>;
345
+ handleAuthorizedTelegramEditedMessage: (
346
+ message: Extract<
347
+ TelegramUpdateExecutionPlan,
348
+ { kind: "edited-message" }
349
+ >["message"],
350
+ ctx: ExtensionContext,
351
+ ) => Promise<void>;
299
352
  }
300
353
 
301
354
  function getTelegramCallbackQueryId(
@@ -368,7 +421,12 @@ export async function executeTelegramUpdatePlan(
368
421
  ? await deps.pairTelegramUserIfNeeded(plan.message.from.id, deps.ctx)
369
422
  : false;
370
423
  const replyTarget = getTelegramMessageReplyTarget(plan.message);
371
- if (pairedNow && plan.shouldNotifyPaired && replyTarget) {
424
+ if (
425
+ plan.kind === "message" &&
426
+ pairedNow &&
427
+ plan.shouldNotifyPaired &&
428
+ replyTarget
429
+ ) {
372
430
  await deps.sendTextReply(
373
431
  replyTarget.chatId,
374
432
  replyTarget.messageId,
@@ -385,5 +443,9 @@ export async function executeTelegramUpdatePlan(
385
443
  }
386
444
  return;
387
445
  }
446
+ if (plan.kind === "edited-message") {
447
+ await deps.handleAuthorizedTelegramEditedMessage(plan.message, deps.ctx);
448
+ return;
449
+ }
388
450
  await deps.handleAuthorizedTelegramMessage(plan.message, deps.ctx);
389
451
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "private": false,
5
5
  "description": "Better Telegram DM bridge extension for pi",
6
6
  "type": "module",