@imessaging/telegram-mtproto 0.8.20 → 0.8.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imessaging/telegram-mtproto",
3
- "version": "0.8.20",
3
+ "version": "0.8.21",
4
4
  "description": "Telegram MTProto user-account transport for imessaging",
5
5
  "keywords": [
6
6
  "messaging",
@@ -31,7 +31,7 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@imessaging/core": "^0.8.16",
34
+ "@imessaging/core": "^0.8.17",
35
35
  "big-integer": "^1.6.52"
36
36
  },
37
37
  "peerDependencies": {
@@ -6,6 +6,7 @@ import type {
6
6
  PhoneLookupOptions,
7
7
  PhoneLookupTransport,
8
8
  PhoneMatch,
9
+ DraftReaderTransport,
9
10
  DraftTransport,
10
11
  MessageDraft,
11
12
  OutgoingObserverTransport,
@@ -34,6 +35,7 @@ import type {
34
35
  OutboundDocument,
35
36
  SendResult,
36
37
  SentPart,
38
+ StoredDraft,
37
39
  TelegramPeerStore,
38
40
  TelegramRecipient,
39
41
  TransportStatus,
@@ -164,6 +166,7 @@ export class TelegramMtprotoTransport
164
166
  implements
165
167
  CommandMenuTransport,
166
168
  ContactTransport,
169
+ DraftReaderTransport,
167
170
  DraftTransport,
168
171
  EditableMessageTransport,
169
172
  EditedMessageTransport,
@@ -445,15 +448,60 @@ export class TelegramMtprotoTransport
445
448
  : {}),
446
449
  }),
447
450
  );
451
+ await this.withPeerRetry(draft.recipient, save);
452
+ this.lastActivityAt = new Date();
453
+ }
454
+
455
+ /**
456
+ * Черновик в поле ввода — перед saveDraft, чтобы не затереть набранное человеком. Тема форума хранит свой
457
+ * черновик в ForumTopic, общий чат — в Dialog. undefined — черновика нет.
458
+ */
459
+ async readDraft(
460
+ recipient: MessageRecipient,
461
+ options: { threadId?: number } = {},
462
+ ): Promise<StoredDraft | undefined> {
463
+ this.assertConnected();
464
+ if (!isTelegramRecipient(recipient)) {
465
+ throw new Error(`Telegram MTProto cannot read a draft for ${recipient.type}`);
466
+ }
467
+ const { threadId } = options;
468
+ if (threadId !== undefined && (!Number.isSafeInteger(threadId) || threadId <= 0)) {
469
+ throw new Error(`Telegram thread id must be a positive integer: ${threadId}`);
470
+ }
471
+ const read = async (peer: Api.TypeInputPeer): Promise<Api.TypeDraftMessage | undefined> => {
472
+ if (threadId !== undefined) {
473
+ const result = await this.client.invoke(
474
+ new Api.messages.GetForumTopicsByID({ peer, topics: [threadId] }),
475
+ );
476
+ const topic = result.topics.find(
477
+ (t): t is Api.ForumTopic => t instanceof Api.ForumTopic && t.id === threadId,
478
+ );
479
+ // Нет темы — не «пустое поле»: иначе черновик ушёл бы в General поверх набранного там.
480
+ if (!topic) throw new Error(`Telegram forum topic not found: ${threadId}`);
481
+ return topic.draft;
482
+ }
483
+ // InputPeer, а не InputDialogPeer: клиент при отправке сам оборачивает пир (_getInputDialog).
484
+ const result = await this.client.invoke(new Api.messages.GetPeerDialogs({ peers: [peer] }));
485
+ const dialog = result.dialogs.find((d): d is Api.Dialog => d instanceof Api.Dialog);
486
+ return dialog?.draft;
487
+ };
488
+ const draft = await this.withPeerRetry(recipient, read);
489
+ this.lastActivityAt = new Date();
490
+ return toStoredDraft(draft, threadId);
491
+ }
492
+
493
+ /** Как у send: устаревший пир из хранилища — забыть и разрешить заново, один раз. */
494
+ private async withPeerRetry<T>(
495
+ recipient: MessageRecipient,
496
+ call: (peer: Api.TypeInputPeer) => Promise<T>,
497
+ ): Promise<T> {
448
498
  try {
449
- await save(await this.getInputPeer(draft.recipient));
499
+ return await call(await this.getInputPeer(recipient));
450
500
  } catch (error: unknown) {
451
- // Как у send: устаревший пир из хранилища — забыть и разрешить заново, один раз.
452
501
  if (!this.isInvalidPeerError(error)) throw error;
453
- await this.peerStore.delete(this.options.accountId, draft.recipient);
454
- await save(await this.getInputPeer(draft.recipient));
502
+ await this.peerStore.delete(this.options.accountId, recipient);
503
+ return call(await this.getInputPeer(recipient));
455
504
  }
456
- this.lastActivityAt = new Date();
457
505
  }
458
506
 
459
507
  /** Свои новые исходящие — отправленное человеком с другого устройства; отправленное через send помечено sentByTransport. */
@@ -1263,6 +1311,29 @@ export class TelegramMtprotoTransport
1263
1311
  }
1264
1312
  }
1265
1313
 
1314
+ /**
1315
+ * DraftMessage → StoredDraft. В теме saveDraft без ответа ставит ответ на корень темы — это не ответ человеку.
1316
+ */
1317
+ function toStoredDraft(
1318
+ draft: Api.TypeDraftMessage | undefined,
1319
+ threadId?: number,
1320
+ ): StoredDraft | undefined {
1321
+ if (!(draft instanceof Api.DraftMessage)) return undefined;
1322
+ const r = draft.replyTo instanceof Api.InputReplyToMessage ? draft.replyTo : undefined;
1323
+ // Ответ в другой чат (replyToPeerId) — чужой id сообщения, здесь он не годится.
1324
+ const replyTo = r && !r.replyToPeerId ? r.replyToMsgId : undefined;
1325
+ const reply = replyTo !== undefined && replyTo !== threadId ? String(replyTo) : undefined;
1326
+ // Очистка в теме оставляет только ответ на корень. Что-то кроме этого (медиа, чужой ответ) — черновик есть.
1327
+ const onlyRoot =
1328
+ !draft.replyTo || (r !== undefined && !r.replyToPeerId && r.replyToMsgId === threadId);
1329
+ if (draft.message === "" && onlyRoot && !draft.media && !draft.richMessage) return undefined;
1330
+ return {
1331
+ text: draft.message,
1332
+ ...(reply !== undefined ? { replyToMessageId: reply } : {}),
1333
+ date: new Date(draft.date * 1000),
1334
+ };
1335
+ }
1336
+
1266
1337
  /** Параллельных частей при загрузке: с одной 25 МБ идут по 512 КБ за круг. */
1267
1338
  const UPLOAD_WORKERS = 4;
1268
1339