@llblab/pi-telegram 0.2.9 → 0.3.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/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,9 +104,24 @@ export function getAuthorizedTelegramCallbackQuery(
117
104
  }
118
105
 
119
106
  export function getAuthorizedTelegramMessage(
120
- update: TelegramUpdateRoutingLike,
121
- ): TelegramMessageLike | undefined {
122
- const message = update.message || update.edited_message;
107
+ update: TelegramUpdateRouting,
108
+ ): TelegramUpdateMessage | undefined {
109
+ const message = update.message;
110
+ if (
111
+ !message ||
112
+ message.chat.type !== "private" ||
113
+ !message.from ||
114
+ message.from.is_bot
115
+ ) {
116
+ return undefined;
117
+ }
118
+ return message;
119
+ }
120
+
121
+ export function getAuthorizedTelegramEditedMessage(
122
+ update: TelegramUpdateRouting,
123
+ ): TelegramUpdateMessage | undefined {
124
+ const message = update.edited_message;
123
125
  if (
124
126
  !message ||
125
127
  message.chat.type !== "private" ||
@@ -133,35 +135,54 @@ export function getAuthorizedTelegramMessage(
133
135
 
134
136
  // --- Flow ---
135
137
 
136
- export interface TelegramMessageReactionUpdatedLike {
138
+ export interface TelegramMessageReactionUpdated {
137
139
  chat: { type: string };
138
- user?: TelegramUserLike;
140
+ user?: TelegramUser;
141
+ message_id: number;
142
+ old_reaction: TelegramReactionType[];
143
+ new_reaction: TelegramReactionType[];
139
144
  }
140
145
 
141
- export interface TelegramUpdateFlowLike
142
- extends TelegramUpdateRoutingLike, TelegramUpdateLike {
143
- message_reaction?: TelegramMessageReactionUpdatedLike;
146
+ export interface TelegramUpdateFlow
147
+ extends TelegramUpdateRouting, TelegramUpdateDeletion {
148
+ message_reaction?: TelegramMessageReactionUpdated;
144
149
  }
145
150
 
146
- export type TelegramUpdateFlowAction =
151
+ export type TelegramUpdateFlowAction<
152
+ TReactionUpdate extends TelegramMessageReactionUpdated =
153
+ TelegramMessageReactionUpdated,
154
+ TCallbackQuery extends TelegramCallbackQuery = TelegramCallbackQuery,
155
+ TMessage extends TelegramUpdateMessage = TelegramUpdateMessage,
156
+ > =
147
157
  | { kind: "ignore" }
148
158
  | { kind: "deleted"; messageIds: number[] }
149
- | { kind: "reaction"; reactionUpdate: TelegramMessageReactionUpdatedLike }
159
+ | { kind: "reaction"; reactionUpdate: TReactionUpdate }
150
160
  | {
151
161
  kind: "callback";
152
- query: TelegramCallbackQueryLike;
162
+ query: TCallbackQuery;
153
163
  authorization: TelegramAuthorizationState;
154
164
  }
155
165
  | {
156
166
  kind: "message";
157
- message: TelegramMessageLike & { from: TelegramUserLike };
167
+ message: TMessage & { from: TelegramUser };
168
+ authorization: TelegramAuthorizationState;
169
+ }
170
+ | {
171
+ kind: "edited-message";
172
+ message: TMessage & { from: TelegramUser };
158
173
  authorization: TelegramAuthorizationState;
159
174
  };
160
175
 
161
- export function buildTelegramUpdateFlowAction(
162
- update: TelegramUpdateFlowLike,
176
+ export function buildTelegramUpdateFlowAction<
177
+ TUpdate extends TelegramUpdateFlow,
178
+ >(
179
+ update: TUpdate,
163
180
  allowedUserId?: number,
164
- ): TelegramUpdateFlowAction {
181
+ ): TelegramUpdateFlowAction<
182
+ NonNullable<TUpdate["message_reaction"]>,
183
+ NonNullable<TUpdate["callback_query"]>,
184
+ NonNullable<TUpdate["message"] | TUpdate["edited_message"]>
185
+ > {
165
186
  const deletedMessageIds = extractDeletedTelegramMessageIds(update);
166
187
  if (deletedMessageIds.length > 0) {
167
188
  return { kind: "deleted", messageIds: deletedMessageIds };
@@ -173,7 +194,7 @@ export function buildTelegramUpdateFlowAction(
173
194
  if (query) {
174
195
  return {
175
196
  kind: "callback",
176
- query,
197
+ query: query as NonNullable<TUpdate["callback_query"]>,
177
198
  authorization: getTelegramAuthorizationState(
178
199
  query.from.id,
179
200
  allowedUserId,
@@ -184,42 +205,72 @@ export function buildTelegramUpdateFlowAction(
184
205
  if (message?.from) {
185
206
  return {
186
207
  kind: "message",
187
- message: message as TelegramMessageLike & { from: TelegramUserLike },
208
+ message: message as NonNullable<
209
+ TUpdate["message"] | TUpdate["edited_message"]
210
+ > & { from: TelegramUser },
188
211
  authorization: getTelegramAuthorizationState(
189
212
  message.from.id,
190
213
  allowedUserId,
191
214
  ),
192
215
  };
193
216
  }
217
+ const editedMessage = getAuthorizedTelegramEditedMessage(update);
218
+ if (editedMessage?.from) {
219
+ return {
220
+ kind: "edited-message",
221
+ message: editedMessage as NonNullable<
222
+ TUpdate["message"] | TUpdate["edited_message"]
223
+ > & { from: TelegramUser },
224
+ authorization: getTelegramAuthorizationState(
225
+ editedMessage.from.id,
226
+ allowedUserId,
227
+ ),
228
+ };
229
+ }
194
230
  return { kind: "ignore" };
195
231
  }
196
232
 
197
233
  // --- Execution Planning ---
198
234
 
199
- export type TelegramUpdateExecutionPlan =
235
+ export type TelegramUpdateExecutionPlan<
236
+ TReactionUpdate extends TelegramMessageReactionUpdated =
237
+ TelegramMessageReactionUpdated,
238
+ TCallbackQuery extends TelegramCallbackQuery = TelegramCallbackQuery,
239
+ TMessage extends TelegramUpdateMessage = TelegramUpdateMessage,
240
+ > =
200
241
  | { kind: "ignore" }
201
242
  | { kind: "deleted"; messageIds: number[] }
202
243
  | {
203
244
  kind: "reaction";
204
- reactionUpdate: NonNullable<TelegramUpdateFlowLike["message_reaction"]>;
245
+ reactionUpdate: TReactionUpdate;
205
246
  }
206
247
  | {
207
248
  kind: "callback";
208
- query: TelegramCallbackQueryLike;
249
+ query: TCallbackQuery;
209
250
  shouldPair: boolean;
210
251
  shouldDeny: boolean;
211
252
  }
212
253
  | {
213
254
  kind: "message";
214
- message: TelegramMessageLike & { from: TelegramUserLike };
255
+ message: TMessage & { from: TelegramUser };
215
256
  shouldPair: boolean;
216
257
  shouldNotifyPaired: boolean;
217
258
  shouldDeny: boolean;
259
+ }
260
+ | {
261
+ kind: "edited-message";
262
+ message: TMessage & { from: TelegramUser };
263
+ shouldPair: boolean;
264
+ shouldDeny: boolean;
218
265
  };
219
266
 
220
- export function buildTelegramUpdateExecutionPlan(
221
- action: TelegramUpdateFlowAction,
222
- ): 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> {
223
274
  switch (action.kind) {
224
275
  case "ignore":
225
276
  return { kind: "ignore" };
@@ -242,13 +293,26 @@ export function buildTelegramUpdateExecutionPlan(
242
293
  shouldNotifyPaired: action.authorization.kind === "pair",
243
294
  shouldDeny: action.authorization.kind === "deny",
244
295
  };
296
+ case "edited-message":
297
+ return {
298
+ kind: "edited-message",
299
+ message: action.message,
300
+ shouldPair: action.authorization.kind === "pair",
301
+ shouldDeny: action.authorization.kind === "deny",
302
+ };
245
303
  }
246
304
  }
247
305
 
248
- export function buildTelegramUpdateExecutionPlanFromUpdate(
249
- update: TelegramUpdateFlowLike,
306
+ export function buildTelegramUpdateExecutionPlanFromUpdate<
307
+ TUpdate extends TelegramUpdateFlow,
308
+ >(
309
+ update: TUpdate,
250
310
  allowedUserId?: number,
251
- ): TelegramUpdateExecutionPlan {
311
+ ): TelegramUpdateExecutionPlan<
312
+ NonNullable<TUpdate["message_reaction"]>,
313
+ NonNullable<TUpdate["callback_query"]>,
314
+ NonNullable<TUpdate["message"] | TUpdate["edited_message"]>
315
+ > {
252
316
  return buildTelegramUpdateExecutionPlan(
253
317
  buildTelegramUpdateFlowAction(update, allowedUserId),
254
318
  );
@@ -256,33 +320,31 @@ export function buildTelegramUpdateExecutionPlanFromUpdate(
256
320
 
257
321
  // --- Runtime ---
258
322
 
259
- export interface TelegramUpdateRuntimeDeps {
260
- 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;
261
331
  removePendingMediaGroupMessages: (messageIds: number[]) => void;
262
332
  removeQueuedTelegramTurnsByMessageIds: (
263
333
  messageIds: number[],
264
- ctx: ExtensionContext,
334
+ ctx: TContext,
265
335
  ) => number;
266
336
  handleAuthorizedTelegramReactionUpdate: (
267
- reactionUpdate: NonNullable<
268
- Extract<
269
- TelegramUpdateExecutionPlan,
270
- { kind: "reaction" }
271
- >["reactionUpdate"]
272
- >,
273
- ctx: ExtensionContext,
337
+ reactionUpdate: TReactionUpdate,
338
+ ctx: TContext,
274
339
  ) => Promise<void>;
275
- pairTelegramUserIfNeeded: (
276
- userId: number,
277
- ctx: ExtensionContext,
278
- ) => Promise<boolean>;
340
+ pairTelegramUserIfNeeded: (userId: number, ctx: TContext) => Promise<boolean>;
279
341
  answerCallbackQuery: (
280
342
  callbackQueryId: string,
281
343
  text?: string,
282
344
  ) => Promise<void>;
283
345
  handleAuthorizedTelegramCallbackQuery: (
284
- query: Extract<TelegramUpdateExecutionPlan, { kind: "callback" }>["query"],
285
- ctx: ExtensionContext,
346
+ query: TCallbackQuery,
347
+ ctx: TContext,
286
348
  ) => Promise<void>;
287
349
  sendTextReply: (
288
350
  chatId: number,
@@ -290,22 +352,77 @@ export interface TelegramUpdateRuntimeDeps {
290
352
  text: string,
291
353
  ) => Promise<number | undefined>;
292
354
  handleAuthorizedTelegramMessage: (
293
- message: Extract<
294
- TelegramUpdateExecutionPlan,
295
- { kind: "message" }
296
- >["message"],
297
- ctx: ExtensionContext,
355
+ message: TMessage,
356
+ ctx: TContext,
298
357
  ) => Promise<void>;
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>;
384
+ answerCallbackQuery: (
385
+ callbackQueryId: string,
386
+ text?: string,
387
+ ) => Promise<void>;
388
+ handleAuthorizedTelegramCallbackQuery: (
389
+ query: TCallbackQuery,
390
+ ctx: TContext,
391
+ ) => Promise<void>;
392
+ sendTextReply: (
393
+ chatId: number,
394
+ replyToMessageId: number,
395
+ text: string,
396
+ ) => Promise<number | undefined>;
397
+ handleAuthorizedTelegramMessage: (
398
+ message: TMessage,
399
+ ctx: TContext,
400
+ ) => Promise<void>;
401
+ handleAuthorizedTelegramEditedMessage: (
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,
414
+ ) => Promise<void>;
415
+ handleUpdate: (update: TUpdate, ctx: TContext) => Promise<void>;
299
416
  }
300
417
 
301
418
  function getTelegramCallbackQueryId(
302
- query: TelegramCallbackQueryLike,
419
+ query: TelegramCallbackQuery,
303
420
  ): string | undefined {
304
421
  return typeof query.id === "string" ? query.id : undefined;
305
422
  }
306
423
 
307
424
  function getTelegramMessageReplyTarget(
308
- message: TelegramMessageLike,
425
+ message: TelegramUpdateMessage,
309
426
  ): { chatId: number; messageId: number } | undefined {
310
427
  if (
311
428
  typeof message.chat.id !== "number" ||
@@ -319,10 +436,18 @@ function getTelegramMessageReplyTarget(
319
436
  };
320
437
  }
321
438
 
322
- export async function executeTelegramUpdate(
323
- update: TelegramUpdateFlowLike,
439
+ export async function executeTelegramUpdate<
440
+ TUpdate extends TelegramUpdateFlow,
441
+ TContext = unknown,
442
+ >(
443
+ update: TUpdate,
324
444
  allowedUserId: number | undefined,
325
- 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
+ >,
326
451
  ): Promise<void> {
327
452
  await executeTelegramUpdatePlan(
328
453
  buildTelegramUpdateExecutionPlanFromUpdate(update, allowedUserId),
@@ -330,9 +455,168 @@ export async function executeTelegramUpdate(
330
455
  );
331
456
  }
332
457
 
333
- export async function executeTelegramUpdatePlan(
334
- plan: TelegramUpdateExecutionPlan,
335
- 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
+ >,
336
620
  ): Promise<void> {
337
621
  if (plan.kind === "ignore") return;
338
622
  if (plan.kind === "deleted") {
@@ -368,7 +652,12 @@ export async function executeTelegramUpdatePlan(
368
652
  ? await deps.pairTelegramUserIfNeeded(plan.message.from.id, deps.ctx)
369
653
  : false;
370
654
  const replyTarget = getTelegramMessageReplyTarget(plan.message);
371
- if (pairedNow && plan.shouldNotifyPaired && replyTarget) {
655
+ if (
656
+ plan.kind === "message" &&
657
+ pairedNow &&
658
+ plan.shouldNotifyPaired &&
659
+ replyTarget
660
+ ) {
372
661
  await deps.sendTextReply(
373
662
  replyTarget.chatId,
374
663
  replyTarget.messageId,
@@ -385,5 +674,9 @@ export async function executeTelegramUpdatePlan(
385
674
  }
386
675
  return;
387
676
  }
677
+ if (plan.kind === "edited-message") {
678
+ await deps.handleAuthorizedTelegramEditedMessage(plan.message, deps.ctx);
679
+ return;
680
+ }
388
681
  await deps.handleAuthorizedTelegramMessage(plan.message, deps.ctx);
389
682
  }