@llblab/pi-telegram 0.2.10 → 0.4.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.
Files changed (47) hide show
  1. package/README.md +52 -19
  2. package/docs/README.md +2 -3
  3. package/docs/architecture.md +62 -31
  4. package/docs/locks.md +136 -0
  5. package/index.ts +323 -1880
  6. package/lib/api.ts +396 -60
  7. package/lib/attachments.ts +128 -16
  8. package/lib/commands.ts +648 -14
  9. package/lib/config.ts +169 -0
  10. package/lib/handlers.ts +474 -0
  11. package/lib/locks.ts +306 -0
  12. package/lib/media.ts +196 -46
  13. package/lib/menu.ts +920 -338
  14. package/lib/model.ts +647 -0
  15. package/lib/pi.ts +90 -0
  16. package/lib/polling.ts +240 -14
  17. package/lib/preview.ts +420 -25
  18. package/lib/queue.ts +1137 -110
  19. package/lib/registration.ts +214 -31
  20. package/lib/rendering.ts +560 -366
  21. package/lib/replies.ts +198 -8
  22. package/lib/routing.ts +217 -0
  23. package/lib/runtime.ts +475 -0
  24. package/lib/setup.ts +143 -1
  25. package/lib/status.ts +432 -13
  26. package/lib/turns.ts +217 -36
  27. package/lib/updates.ts +340 -109
  28. package/package.json +18 -3
  29. package/AGENTS.md +0 -91
  30. package/BACKLOG.md +0 -5
  31. package/CHANGELOG.md +0 -34
  32. package/lib/model-switch.ts +0 -62
  33. package/lib/types.ts +0 -137
  34. package/tests/api.test.ts +0 -331
  35. package/tests/attachments.test.ts +0 -132
  36. package/tests/commands.test.ts +0 -85
  37. package/tests/config.test.ts +0 -80
  38. package/tests/media.test.ts +0 -166
  39. package/tests/menu.test.ts +0 -676
  40. package/tests/polling.test.ts +0 -202
  41. package/tests/preview.test.ts +0 -480
  42. package/tests/queue.test.ts +0 -3245
  43. package/tests/registration.test.ts +0 -268
  44. package/tests/rendering.test.ts +0 -526
  45. package/tests/replies.test.ts +0 -142
  46. package/tests/turns.test.ts +0 -247
  47. package/tests/updates.test.ts +0 -416
package/lib/updates.ts CHANGED
@@ -3,24 +3,29 @@
3
3
  * Owns update extraction, authorization, classification, execution planning, and runtime execution for Telegram updates
4
4
  */
5
5
 
6
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
6
+ import {
7
+ createTelegramUserPairingRuntime,
8
+ getTelegramAuthorizationState,
9
+ type TelegramAuthorizationState,
10
+ type TelegramUserPairingRuntimeDeps,
11
+ } from "./config.ts";
7
12
 
8
13
  // --- Extraction ---
9
14
 
10
- export interface TelegramReactionTypeEmojiLike {
15
+ export interface TelegramReactionTypeEmoji {
11
16
  type: "emoji";
12
17
  emoji: string;
13
18
  }
14
19
 
15
- export interface TelegramReactionTypeNonEmojiLike {
20
+ export interface TelegramReactionTypeNonEmoji {
16
21
  type: string;
17
22
  }
18
23
 
19
- export type TelegramReactionTypeLike =
20
- | TelegramReactionTypeEmojiLike
21
- | TelegramReactionTypeNonEmojiLike;
24
+ export type TelegramReactionType =
25
+ | TelegramReactionTypeEmoji
26
+ | TelegramReactionTypeNonEmoji;
22
27
 
23
- export interface TelegramUpdateLike {
28
+ export interface TelegramUpdateDeletion {
24
29
  deleted_business_messages?: { message_ids?: unknown };
25
30
  }
26
31
 
@@ -33,12 +38,12 @@ export function normalizeTelegramReactionEmoji(emoji: string): string {
33
38
  }
34
39
 
35
40
  export function collectTelegramReactionEmojis(
36
- reactions: TelegramReactionTypeLike[],
41
+ reactions: TelegramReactionType[],
37
42
  ): Set<string> {
38
43
  return new Set(
39
44
  reactions
40
45
  .filter(
41
- (reaction): reaction is TelegramReactionTypeEmojiLike =>
46
+ (reaction): reaction is TelegramReactionTypeEmoji =>
42
47
  reaction.type === "emoji",
43
48
  )
44
49
  .map((reaction) => normalizeTelegramReactionEmoji(reaction.emoji)),
@@ -46,7 +51,7 @@ export function collectTelegramReactionEmojis(
46
51
  }
47
52
 
48
53
  export function extractDeletedTelegramMessageIds(
49
- update: TelegramUpdateLike,
54
+ update: TelegramUpdateDeletion,
50
55
  ): number[] {
51
56
  const deletedBusinessMessageIds =
52
57
  update.deleted_business_messages?.message_ids;
@@ -58,55 +63,37 @@ export function extractDeletedTelegramMessageIds(
58
63
 
59
64
  // --- Routing ---
60
65
 
61
- export interface TelegramUserLike {
66
+ export interface TelegramUser {
62
67
  id: number;
63
68
  is_bot: boolean;
64
69
  }
65
70
 
66
- export interface TelegramChatLike {
71
+ export interface TelegramChat {
67
72
  id?: number;
68
73
  type: string;
69
74
  }
70
75
 
71
- export interface TelegramMessageLike {
72
- chat: TelegramChatLike;
73
- from?: TelegramUserLike;
76
+ export interface TelegramUpdateMessage {
77
+ chat: TelegramChat;
78
+ from?: TelegramUser;
74
79
  message_id?: number;
75
80
  }
76
81
 
77
- export interface TelegramCallbackQueryLike {
82
+ export interface TelegramCallbackQuery {
78
83
  id?: string;
79
- from: TelegramUserLike;
80
- message?: TelegramMessageLike;
84
+ from: TelegramUser;
85
+ message?: TelegramUpdateMessage;
81
86
  }
82
87
 
83
- export interface TelegramUpdateRoutingLike {
84
- message?: TelegramMessageLike;
85
- edited_message?: TelegramMessageLike;
86
- callback_query?: TelegramCallbackQueryLike;
87
- }
88
-
89
- export type TelegramAuthorizationState =
90
- | { kind: "pair"; userId: number }
91
- | { kind: "allow" }
92
- | { kind: "deny" };
93
-
94
- export function getTelegramAuthorizationState(
95
- userId: number,
96
- allowedUserId?: number,
97
- ): TelegramAuthorizationState {
98
- if (allowedUserId === undefined) {
99
- return { kind: "pair", userId };
100
- }
101
- if (userId === allowedUserId) {
102
- return { kind: "allow" };
103
- }
104
- return { kind: "deny" };
88
+ export interface TelegramUpdateRouting {
89
+ message?: TelegramUpdateMessage;
90
+ edited_message?: TelegramUpdateMessage;
91
+ callback_query?: TelegramCallbackQuery;
105
92
  }
106
93
 
107
94
  export function getAuthorizedTelegramCallbackQuery(
108
- update: TelegramUpdateRoutingLike,
109
- ): TelegramCallbackQueryLike | undefined {
95
+ update: TelegramUpdateRouting,
96
+ ): TelegramCallbackQuery | undefined {
110
97
  const query = update.callback_query;
111
98
  if (!query) return undefined;
112
99
  const message = query.message;
@@ -117,8 +104,8 @@ export function getAuthorizedTelegramCallbackQuery(
117
104
  }
118
105
 
119
106
  export function getAuthorizedTelegramMessage(
120
- update: TelegramUpdateRoutingLike,
121
- ): TelegramMessageLike | undefined {
107
+ update: TelegramUpdateRouting,
108
+ ): TelegramUpdateMessage | undefined {
122
109
  const message = update.message;
123
110
  if (
124
111
  !message ||
@@ -132,8 +119,8 @@ export function getAuthorizedTelegramMessage(
132
119
  }
133
120
 
134
121
  export function getAuthorizedTelegramEditedMessage(
135
- update: TelegramUpdateRoutingLike,
136
- ): TelegramMessageLike | undefined {
122
+ update: TelegramUpdateRouting,
123
+ ): TelegramUpdateMessage | undefined {
137
124
  const message = update.edited_message;
138
125
  if (
139
126
  !message ||
@@ -148,40 +135,54 @@ export function getAuthorizedTelegramEditedMessage(
148
135
 
149
136
  // --- Flow ---
150
137
 
151
- export interface TelegramMessageReactionUpdatedLike {
138
+ export interface TelegramMessageReactionUpdated {
152
139
  chat: { type: string };
153
- user?: TelegramUserLike;
140
+ user?: TelegramUser;
141
+ message_id: number;
142
+ old_reaction: TelegramReactionType[];
143
+ new_reaction: TelegramReactionType[];
154
144
  }
155
145
 
156
- export interface TelegramUpdateFlowLike
157
- extends TelegramUpdateRoutingLike, TelegramUpdateLike {
158
- message_reaction?: TelegramMessageReactionUpdatedLike;
146
+ export interface TelegramUpdateFlow
147
+ extends TelegramUpdateRouting, TelegramUpdateDeletion {
148
+ message_reaction?: TelegramMessageReactionUpdated;
159
149
  }
160
150
 
161
- export type TelegramUpdateFlowAction =
151
+ export type TelegramUpdateFlowAction<
152
+ TReactionUpdate extends TelegramMessageReactionUpdated =
153
+ TelegramMessageReactionUpdated,
154
+ TCallbackQuery extends TelegramCallbackQuery = TelegramCallbackQuery,
155
+ TMessage extends TelegramUpdateMessage = TelegramUpdateMessage,
156
+ > =
162
157
  | { kind: "ignore" }
163
158
  | { kind: "deleted"; messageIds: number[] }
164
- | { kind: "reaction"; reactionUpdate: TelegramMessageReactionUpdatedLike }
159
+ | { kind: "reaction"; reactionUpdate: TReactionUpdate }
165
160
  | {
166
161
  kind: "callback";
167
- query: TelegramCallbackQueryLike;
162
+ query: TCallbackQuery;
168
163
  authorization: TelegramAuthorizationState;
169
164
  }
170
165
  | {
171
166
  kind: "message";
172
- message: TelegramMessageLike & { from: TelegramUserLike };
167
+ message: TMessage & { from: TelegramUser };
173
168
  authorization: TelegramAuthorizationState;
174
169
  }
175
170
  | {
176
171
  kind: "edited-message";
177
- message: TelegramMessageLike & { from: TelegramUserLike };
172
+ message: TMessage & { from: TelegramUser };
178
173
  authorization: TelegramAuthorizationState;
179
174
  };
180
175
 
181
- export function buildTelegramUpdateFlowAction(
182
- update: TelegramUpdateFlowLike,
176
+ export function buildTelegramUpdateFlowAction<
177
+ TUpdate extends TelegramUpdateFlow,
178
+ >(
179
+ update: TUpdate,
183
180
  allowedUserId?: number,
184
- ): TelegramUpdateFlowAction {
181
+ ): TelegramUpdateFlowAction<
182
+ NonNullable<TUpdate["message_reaction"]>,
183
+ NonNullable<TUpdate["callback_query"]>,
184
+ NonNullable<TUpdate["message"] | TUpdate["edited_message"]>
185
+ > {
185
186
  const deletedMessageIds = extractDeletedTelegramMessageIds(update);
186
187
  if (deletedMessageIds.length > 0) {
187
188
  return { kind: "deleted", messageIds: deletedMessageIds };
@@ -193,7 +194,7 @@ export function buildTelegramUpdateFlowAction(
193
194
  if (query) {
194
195
  return {
195
196
  kind: "callback",
196
- query,
197
+ query: query as NonNullable<TUpdate["callback_query"]>,
197
198
  authorization: getTelegramAuthorizationState(
198
199
  query.from.id,
199
200
  allowedUserId,
@@ -204,7 +205,9 @@ export function buildTelegramUpdateFlowAction(
204
205
  if (message?.from) {
205
206
  return {
206
207
  kind: "message",
207
- message: message as TelegramMessageLike & { from: TelegramUserLike },
208
+ message: message as NonNullable<
209
+ TUpdate["message"] | TUpdate["edited_message"]
210
+ > & { from: TelegramUser },
208
211
  authorization: getTelegramAuthorizationState(
209
212
  message.from.id,
210
213
  allowedUserId,
@@ -215,9 +218,9 @@ export function buildTelegramUpdateFlowAction(
215
218
  if (editedMessage?.from) {
216
219
  return {
217
220
  kind: "edited-message",
218
- message: editedMessage as TelegramMessageLike & {
219
- from: TelegramUserLike;
220
- },
221
+ message: editedMessage as NonNullable<
222
+ TUpdate["message"] | TUpdate["edited_message"]
223
+ > & { from: TelegramUser },
221
224
  authorization: getTelegramAuthorizationState(
222
225
  editedMessage.from.id,
223
226
  allowedUserId,
@@ -229,36 +232,45 @@ export function buildTelegramUpdateFlowAction(
229
232
 
230
233
  // --- Execution Planning ---
231
234
 
232
- export type TelegramUpdateExecutionPlan =
235
+ export type TelegramUpdateExecutionPlan<
236
+ TReactionUpdate extends TelegramMessageReactionUpdated =
237
+ TelegramMessageReactionUpdated,
238
+ TCallbackQuery extends TelegramCallbackQuery = TelegramCallbackQuery,
239
+ TMessage extends TelegramUpdateMessage = TelegramUpdateMessage,
240
+ > =
233
241
  | { kind: "ignore" }
234
242
  | { kind: "deleted"; messageIds: number[] }
235
243
  | {
236
244
  kind: "reaction";
237
- reactionUpdate: NonNullable<TelegramUpdateFlowLike["message_reaction"]>;
245
+ reactionUpdate: TReactionUpdate;
238
246
  }
239
247
  | {
240
248
  kind: "callback";
241
- query: TelegramCallbackQueryLike;
249
+ query: TCallbackQuery;
242
250
  shouldPair: boolean;
243
251
  shouldDeny: boolean;
244
252
  }
245
253
  | {
246
254
  kind: "message";
247
- message: TelegramMessageLike & { from: TelegramUserLike };
255
+ message: TMessage & { from: TelegramUser };
248
256
  shouldPair: boolean;
249
257
  shouldNotifyPaired: boolean;
250
258
  shouldDeny: boolean;
251
259
  }
252
260
  | {
253
261
  kind: "edited-message";
254
- message: TelegramMessageLike & { from: TelegramUserLike };
262
+ message: TMessage & { from: TelegramUser };
255
263
  shouldPair: boolean;
256
264
  shouldDeny: boolean;
257
265
  };
258
266
 
259
- export function buildTelegramUpdateExecutionPlan(
260
- action: TelegramUpdateFlowAction,
261
- ): TelegramUpdateExecutionPlan {
267
+ export function buildTelegramUpdateExecutionPlan<
268
+ TReactionUpdate extends TelegramMessageReactionUpdated,
269
+ TCallbackQuery extends TelegramCallbackQuery,
270
+ TMessage extends TelegramUpdateMessage,
271
+ >(
272
+ action: TelegramUpdateFlowAction<TReactionUpdate, TCallbackQuery, TMessage>,
273
+ ): TelegramUpdateExecutionPlan<TReactionUpdate, TCallbackQuery, TMessage> {
262
274
  switch (action.kind) {
263
275
  case "ignore":
264
276
  return { kind: "ignore" };
@@ -291,10 +303,16 @@ export function buildTelegramUpdateExecutionPlan(
291
303
  }
292
304
  }
293
305
 
294
- export function buildTelegramUpdateExecutionPlanFromUpdate(
295
- update: TelegramUpdateFlowLike,
306
+ export function buildTelegramUpdateExecutionPlanFromUpdate<
307
+ TUpdate extends TelegramUpdateFlow,
308
+ >(
309
+ update: TUpdate,
296
310
  allowedUserId?: number,
297
- ): TelegramUpdateExecutionPlan {
311
+ ): TelegramUpdateExecutionPlan<
312
+ NonNullable<TUpdate["message_reaction"]>,
313
+ NonNullable<TUpdate["callback_query"]>,
314
+ NonNullable<TUpdate["message"] | TUpdate["edited_message"]>
315
+ > {
298
316
  return buildTelegramUpdateExecutionPlan(
299
317
  buildTelegramUpdateFlowAction(update, allowedUserId),
300
318
  );
@@ -302,33 +320,74 @@ export function buildTelegramUpdateExecutionPlanFromUpdate(
302
320
 
303
321
  // --- Runtime ---
304
322
 
305
- export interface TelegramUpdateRuntimeDeps {
306
- ctx: ExtensionContext;
323
+ export interface TelegramUpdateRuntimeDeps<
324
+ TContext = unknown,
325
+ TReactionUpdate extends TelegramMessageReactionUpdated =
326
+ TelegramMessageReactionUpdated,
327
+ TCallbackQuery extends TelegramCallbackQuery = TelegramCallbackQuery,
328
+ TMessage extends TelegramUpdateMessage = TelegramUpdateMessage,
329
+ > {
330
+ ctx: TContext;
307
331
  removePendingMediaGroupMessages: (messageIds: number[]) => void;
308
332
  removeQueuedTelegramTurnsByMessageIds: (
309
333
  messageIds: number[],
310
- ctx: ExtensionContext,
334
+ ctx: TContext,
311
335
  ) => number;
312
336
  handleAuthorizedTelegramReactionUpdate: (
313
- reactionUpdate: NonNullable<
314
- Extract<
315
- TelegramUpdateExecutionPlan,
316
- { kind: "reaction" }
317
- >["reactionUpdate"]
318
- >,
319
- ctx: ExtensionContext,
337
+ reactionUpdate: TReactionUpdate,
338
+ ctx: TContext,
339
+ ) => Promise<void>;
340
+ pairTelegramUserIfNeeded: (userId: number, ctx: TContext) => Promise<boolean>;
341
+ answerCallbackQuery: (
342
+ callbackQueryId: string,
343
+ text?: string,
344
+ ) => Promise<void>;
345
+ handleAuthorizedTelegramCallbackQuery: (
346
+ query: TCallbackQuery,
347
+ ctx: TContext,
348
+ ) => Promise<void>;
349
+ sendTextReply: (
350
+ chatId: number,
351
+ replyToMessageId: number,
352
+ text: string,
353
+ ) => Promise<number | undefined>;
354
+ handleAuthorizedTelegramMessage: (
355
+ message: TMessage,
356
+ ctx: TContext,
320
357
  ) => Promise<void>;
321
- pairTelegramUserIfNeeded: (
322
- userId: number,
323
- ctx: ExtensionContext,
324
- ) => Promise<boolean>;
358
+ handleAuthorizedTelegramEditedMessage: (
359
+ message: TMessage,
360
+ ctx: TContext,
361
+ ) => unknown;
362
+ }
363
+
364
+ export interface TelegramUpdateRuntimeControllerDeps<
365
+ TContext = unknown,
366
+ TCallbackQuery extends TelegramCallbackQuery = TelegramCallbackQuery,
367
+ TMessage extends TelegramUpdateMessage = TelegramUpdateMessage,
368
+ > {
369
+ getAllowedUserId: () => number | undefined;
370
+ removePendingMediaGroupMessages: (messageIds: number[]) => void;
371
+ removeQueuedTelegramTurnsByMessageIds: (
372
+ messageIds: number[],
373
+ ctx: TContext,
374
+ ) => number;
375
+ clearQueuedTelegramTurnPriorityByMessageId: (
376
+ messageId: number,
377
+ ctx: TContext,
378
+ ) => boolean;
379
+ prioritizeQueuedTelegramTurnByMessageId: (
380
+ messageId: number,
381
+ ctx: TContext,
382
+ ) => boolean;
383
+ pairTelegramUserIfNeeded: (userId: number, ctx: TContext) => Promise<boolean>;
325
384
  answerCallbackQuery: (
326
385
  callbackQueryId: string,
327
386
  text?: string,
328
387
  ) => Promise<void>;
329
388
  handleAuthorizedTelegramCallbackQuery: (
330
- query: Extract<TelegramUpdateExecutionPlan, { kind: "callback" }>["query"],
331
- ctx: ExtensionContext,
389
+ query: TCallbackQuery,
390
+ ctx: TContext,
332
391
  ) => Promise<void>;
333
392
  sendTextReply: (
334
393
  chatId: number,
@@ -336,29 +395,34 @@ export interface TelegramUpdateRuntimeDeps {
336
395
  text: string,
337
396
  ) => Promise<number | undefined>;
338
397
  handleAuthorizedTelegramMessage: (
339
- message: Extract<
340
- TelegramUpdateExecutionPlan,
341
- { kind: "message" }
342
- >["message"],
343
- ctx: ExtensionContext,
398
+ message: TMessage,
399
+ ctx: TContext,
344
400
  ) => Promise<void>;
345
401
  handleAuthorizedTelegramEditedMessage: (
346
- message: Extract<
347
- TelegramUpdateExecutionPlan,
348
- { kind: "edited-message" }
349
- >["message"],
350
- ctx: ExtensionContext,
402
+ message: TMessage,
403
+ ctx: TContext,
404
+ ) => unknown;
405
+ }
406
+
407
+ export interface TelegramUpdateRuntimeController<
408
+ TContext = unknown,
409
+ TUpdate extends TelegramUpdateFlow = TelegramUpdateFlow,
410
+ > {
411
+ handleAuthorizedReactionUpdate: (
412
+ reactionUpdate: NonNullable<TUpdate["message_reaction"]>,
413
+ ctx: TContext,
351
414
  ) => Promise<void>;
415
+ handleUpdate: (update: TUpdate, ctx: TContext) => Promise<void>;
352
416
  }
353
417
 
354
418
  function getTelegramCallbackQueryId(
355
- query: TelegramCallbackQueryLike,
419
+ query: TelegramCallbackQuery,
356
420
  ): string | undefined {
357
421
  return typeof query.id === "string" ? query.id : undefined;
358
422
  }
359
423
 
360
424
  function getTelegramMessageReplyTarget(
361
- message: TelegramMessageLike,
425
+ message: TelegramUpdateMessage,
362
426
  ): { chatId: number; messageId: number } | undefined {
363
427
  if (
364
428
  typeof message.chat.id !== "number" ||
@@ -372,10 +436,18 @@ function getTelegramMessageReplyTarget(
372
436
  };
373
437
  }
374
438
 
375
- export async function executeTelegramUpdate(
376
- update: TelegramUpdateFlowLike,
439
+ export async function executeTelegramUpdate<
440
+ TUpdate extends TelegramUpdateFlow,
441
+ TContext = unknown,
442
+ >(
443
+ update: TUpdate,
377
444
  allowedUserId: number | undefined,
378
- deps: TelegramUpdateRuntimeDeps,
445
+ deps: TelegramUpdateRuntimeDeps<
446
+ TContext,
447
+ NonNullable<TUpdate["message_reaction"]>,
448
+ NonNullable<TUpdate["callback_query"]>,
449
+ NonNullable<TUpdate["message"] | TUpdate["edited_message"]>
450
+ >,
379
451
  ): Promise<void> {
380
452
  await executeTelegramUpdatePlan(
381
453
  buildTelegramUpdateExecutionPlanFromUpdate(update, allowedUserId),
@@ -383,9 +455,168 @@ export async function executeTelegramUpdate(
383
455
  );
384
456
  }
385
457
 
386
- export async function executeTelegramUpdatePlan(
387
- plan: TelegramUpdateExecutionPlan,
388
- deps: TelegramUpdateRuntimeDeps,
458
+ export type TelegramPairedUpdateRuntimeControllerDeps<
459
+ TContext = unknown,
460
+ TUpdate extends TelegramUpdateFlow = TelegramUpdateFlow,
461
+ > = Omit<
462
+ TelegramUpdateRuntimeControllerDeps<
463
+ TContext,
464
+ NonNullable<TUpdate["callback_query"]>,
465
+ NonNullable<TUpdate["message"] | TUpdate["edited_message"]>
466
+ >,
467
+ "pairTelegramUserIfNeeded"
468
+ > &
469
+ TelegramUserPairingRuntimeDeps<TContext>;
470
+
471
+ export function createTelegramPairedUpdateRuntime<
472
+ TContext = unknown,
473
+ TUpdate extends TelegramUpdateFlow = TelegramUpdateFlow,
474
+ >(
475
+ deps: TelegramPairedUpdateRuntimeControllerDeps<TContext, TUpdate>,
476
+ ): TelegramUpdateRuntimeController<TContext, TUpdate> {
477
+ return createTelegramUpdateRuntime({
478
+ getAllowedUserId: deps.getAllowedUserId,
479
+ removePendingMediaGroupMessages: deps.removePendingMediaGroupMessages,
480
+ removeQueuedTelegramTurnsByMessageIds:
481
+ deps.removeQueuedTelegramTurnsByMessageIds,
482
+ clearQueuedTelegramTurnPriorityByMessageId:
483
+ deps.clearQueuedTelegramTurnPriorityByMessageId,
484
+ prioritizeQueuedTelegramTurnByMessageId:
485
+ deps.prioritizeQueuedTelegramTurnByMessageId,
486
+ pairTelegramUserIfNeeded: createTelegramUserPairingRuntime({
487
+ getAllowedUserId: deps.getAllowedUserId,
488
+ setAllowedUserId: deps.setAllowedUserId,
489
+ persistConfig: deps.persistConfig,
490
+ updateStatus: deps.updateStatus,
491
+ }).pairIfNeeded,
492
+ answerCallbackQuery: deps.answerCallbackQuery,
493
+ handleAuthorizedTelegramCallbackQuery:
494
+ deps.handleAuthorizedTelegramCallbackQuery,
495
+ sendTextReply: deps.sendTextReply,
496
+ handleAuthorizedTelegramMessage: deps.handleAuthorizedTelegramMessage,
497
+ handleAuthorizedTelegramEditedMessage:
498
+ deps.handleAuthorizedTelegramEditedMessage,
499
+ });
500
+ }
501
+
502
+ export function createTelegramUpdateRuntime<
503
+ TContext = unknown,
504
+ TUpdate extends TelegramUpdateFlow = TelegramUpdateFlow,
505
+ >(
506
+ deps: TelegramUpdateRuntimeControllerDeps<
507
+ TContext,
508
+ NonNullable<TUpdate["callback_query"]>,
509
+ NonNullable<TUpdate["message"] | TUpdate["edited_message"]>
510
+ >,
511
+ ): TelegramUpdateRuntimeController<TContext, TUpdate> {
512
+ const handleAuthorizedReactionUpdate = async (
513
+ reactionUpdate: NonNullable<TUpdate["message_reaction"]>,
514
+ ctx: TContext,
515
+ ): Promise<void> => {
516
+ await handleAuthorizedTelegramReactionUpdate(reactionUpdate, {
517
+ allowedUserId: deps.getAllowedUserId(),
518
+ ctx,
519
+ removePendingMediaGroupMessages: deps.removePendingMediaGroupMessages,
520
+ removeQueuedTelegramTurnsByMessageIds:
521
+ deps.removeQueuedTelegramTurnsByMessageIds,
522
+ clearQueuedTelegramTurnPriorityByMessageId:
523
+ deps.clearQueuedTelegramTurnPriorityByMessageId,
524
+ prioritizeQueuedTelegramTurnByMessageId:
525
+ deps.prioritizeQueuedTelegramTurnByMessageId,
526
+ });
527
+ };
528
+ return {
529
+ handleAuthorizedReactionUpdate,
530
+ handleUpdate: (update, ctx) =>
531
+ executeTelegramUpdate(update, deps.getAllowedUserId(), {
532
+ ctx,
533
+ removePendingMediaGroupMessages: deps.removePendingMediaGroupMessages,
534
+ removeQueuedTelegramTurnsByMessageIds:
535
+ deps.removeQueuedTelegramTurnsByMessageIds,
536
+ handleAuthorizedTelegramReactionUpdate: handleAuthorizedReactionUpdate,
537
+ pairTelegramUserIfNeeded: deps.pairTelegramUserIfNeeded,
538
+ answerCallbackQuery: deps.answerCallbackQuery,
539
+ handleAuthorizedTelegramCallbackQuery:
540
+ deps.handleAuthorizedTelegramCallbackQuery,
541
+ sendTextReply: deps.sendTextReply,
542
+ handleAuthorizedTelegramMessage: deps.handleAuthorizedTelegramMessage,
543
+ handleAuthorizedTelegramEditedMessage:
544
+ deps.handleAuthorizedTelegramEditedMessage,
545
+ }),
546
+ };
547
+ }
548
+
549
+ export interface AuthorizedTelegramReactionUpdateDeps<TContext> {
550
+ allowedUserId?: number;
551
+ ctx: TContext;
552
+ removePendingMediaGroupMessages: (messageIds: number[]) => void;
553
+ removeQueuedTelegramTurnsByMessageIds: (
554
+ messageIds: number[],
555
+ ctx: TContext,
556
+ ) => number;
557
+ clearQueuedTelegramTurnPriorityByMessageId: (
558
+ messageId: number,
559
+ ctx: TContext,
560
+ ) => boolean;
561
+ prioritizeQueuedTelegramTurnByMessageId: (
562
+ messageId: number,
563
+ ctx: TContext,
564
+ ) => boolean;
565
+ }
566
+
567
+ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
568
+ reactionUpdate: TelegramMessageReactionUpdated,
569
+ deps: AuthorizedTelegramReactionUpdateDeps<TContext>,
570
+ ): Promise<void> {
571
+ const reactionUser = reactionUpdate.user;
572
+ if (
573
+ reactionUpdate.chat.type !== "private" ||
574
+ !reactionUser ||
575
+ reactionUser.is_bot ||
576
+ reactionUser.id !== deps.allowedUserId
577
+ ) {
578
+ return;
579
+ }
580
+ const oldEmojis = collectTelegramReactionEmojis(reactionUpdate.old_reaction);
581
+ const newEmojis = collectTelegramReactionEmojis(reactionUpdate.new_reaction);
582
+ const dislikeAdded = !oldEmojis.has("👎") && newEmojis.has("👎");
583
+ if (dislikeAdded) {
584
+ deps.removePendingMediaGroupMessages([reactionUpdate.message_id]);
585
+ deps.removeQueuedTelegramTurnsByMessageIds(
586
+ [reactionUpdate.message_id],
587
+ deps.ctx,
588
+ );
589
+ return;
590
+ }
591
+ const likeRemoved = oldEmojis.has("👍") && !newEmojis.has("👍");
592
+ if (likeRemoved) {
593
+ deps.clearQueuedTelegramTurnPriorityByMessageId(
594
+ reactionUpdate.message_id,
595
+ deps.ctx,
596
+ );
597
+ }
598
+ const likeAdded = !oldEmojis.has("👍") && newEmojis.has("👍");
599
+ if (!likeAdded) return;
600
+ deps.prioritizeQueuedTelegramTurnByMessageId(
601
+ reactionUpdate.message_id,
602
+ deps.ctx,
603
+ );
604
+ }
605
+
606
+ export async function executeTelegramUpdatePlan<
607
+ TContext = unknown,
608
+ TReactionUpdate extends TelegramMessageReactionUpdated =
609
+ TelegramMessageReactionUpdated,
610
+ TCallbackQuery extends TelegramCallbackQuery = TelegramCallbackQuery,
611
+ TMessage extends TelegramUpdateMessage = TelegramUpdateMessage,
612
+ >(
613
+ plan: TelegramUpdateExecutionPlan<TReactionUpdate, TCallbackQuery, TMessage>,
614
+ deps: TelegramUpdateRuntimeDeps<
615
+ TContext,
616
+ TReactionUpdate,
617
+ TCallbackQuery,
618
+ TMessage
619
+ >,
389
620
  ): Promise<void> {
390
621
  if (plan.kind === "ignore") return;
391
622
  if (plan.kind === "deleted") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.2.10",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "Better Telegram DM bridge extension for pi",
6
6
  "type": "module",
@@ -21,8 +21,19 @@
21
21
  "url": "https://github.com/llblab/pi-telegram/issues"
22
22
  },
23
23
  "scripts": {
24
- "test": "node --experimental-strip-types --test tests/*.test.ts"
24
+ "test": "node --experimental-strip-types --test tests/*.test.ts",
25
+ "typecheck": "tsc --noEmit",
26
+ "audit": "npm audit",
27
+ "pack:check": "npm pack --dry-run",
28
+ "validate": "npm run typecheck && npm test && npm run audit && npm run pack:check"
25
29
  },
30
+ "files": [
31
+ "index.ts",
32
+ "lib/",
33
+ "README.md",
34
+ "docs/",
35
+ "screenshot.png"
36
+ ],
26
37
  "publishConfig": {
27
38
  "access": "public"
28
39
  },
@@ -33,9 +44,13 @@
33
44
  "image": "https://github.com/llblab/pi-telegram/raw/main/screenshot.png"
34
45
  },
35
46
  "peerDependencies": {
36
- "@mariozechner/pi-ai": "*",
37
47
  "@mariozechner/pi-agent-core": "*",
48
+ "@mariozechner/pi-ai": "*",
38
49
  "@mariozechner/pi-coding-agent": "*",
39
50
  "@sinclair/typebox": "*"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "latest",
54
+ "typescript": "latest"
40
55
  }
41
56
  }