@imessaging/telegram-mtproto 0.7.4 → 0.8.1

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.7.4",
3
+ "version": "0.8.1",
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.7.4",
34
+ "@imessaging/core": "^0.8.1",
35
35
  "big-integer": "^1.6.52"
36
36
  },
37
37
  "peerDependencies": {
@@ -0,0 +1,27 @@
1
+ import { Api } from "telegram";
2
+ import type { FormattedText } from "@imessaging/core";
3
+
4
+ /**
5
+ * Выделения FormattedText → сущности MTProto. Смещения у обоих в UTF-16, переводить их не нужно.
6
+ */
7
+ export function toTelegramEntities(ft: FormattedText): Api.TypeMessageEntity[] {
8
+ return ft.entities.map((e) => {
9
+ const base = { offset: e.offset, length: e.length };
10
+ switch (e.type) {
11
+ case "bold":
12
+ return new Api.MessageEntityBold(base);
13
+ case "italic":
14
+ return new Api.MessageEntityItalic(base);
15
+ case "underline":
16
+ return new Api.MessageEntityUnderline(base);
17
+ case "strike":
18
+ return new Api.MessageEntityStrike(base);
19
+ case "code":
20
+ return new Api.MessageEntityCode(base);
21
+ case "pre":
22
+ return new Api.MessageEntityPre({ ...base, language: e.language ?? "" });
23
+ case "link":
24
+ return new Api.MessageEntityTextUrl({ ...base, url: e.url ?? "" });
25
+ }
26
+ });
27
+ }
package/src/incoming.ts CHANGED
@@ -14,9 +14,11 @@ export type MtprotoMessageLike = {
14
14
  message?: string;
15
15
  /** `true` — сообщение отправлено САМОЙ учётной записью. */
16
16
  out?: boolean;
17
- date?: number;
18
- editDate?: number;
19
- groupedId?: { toString(): string };
17
+ // gramjs отдаёт отсутствующее поле как `null`, а не `undefined`: проверки ниже — на оба.
18
+ date?: number | null;
19
+ editDate?: number | null;
20
+ groupedId?: { toString(): string } | null;
21
+ mentioned?: boolean | null;
20
22
  peerId?: MtprotoPeerLike;
21
23
  fromId?: MtprotoPeerLike;
22
24
  media?: MtprotoMediaLike;
@@ -31,7 +33,7 @@ export type MtprotoMessageLike = {
31
33
  replyMarkup?: { rows?: { buttons?: MtprotoButtonLike[] }[] };
32
34
  replyTo?: {
33
35
  replyToMsgId?: number | string;
34
- replyToTopId?: number | string;
36
+ replyToTopId?: number | string | null;
35
37
  forumTopic?: boolean;
36
38
  };
37
39
  /** Заголовок форварда. Пусто — сообщение своё, не пересланное. */
@@ -269,7 +271,7 @@ export function toIncomingMessage(
269
271
  if (message.replyTo?.forumTopic === true) {
270
272
  threadId = Number(message.replyTo.replyToTopId ?? message.replyTo.replyToMsgId);
271
273
  replyToMessageId =
272
- message.replyTo.replyToTopId === undefined ? undefined : String(message.replyTo.replyToMsgId);
274
+ message.replyTo.replyToTopId == null ? undefined : String(message.replyTo.replyToMsgId);
273
275
  } else {
274
276
  replyToMessageId =
275
277
  message.replyTo?.replyToMsgId === undefined
@@ -292,10 +294,12 @@ export function toIncomingMessage(
292
294
  messageId: String(message.id),
293
295
  albumId: message.groupedId?.toString(),
294
296
  text: message.message ?? "",
295
- sentAt: message.date !== undefined ? new Date(message.date * 1000) : undefined,
296
- editedAt: message.editDate !== undefined ? new Date(message.editDate * 1000) : undefined,
297
+ sentAt: message.date ? new Date(message.date * 1000) : undefined,
298
+ // Непоправленное сообщение приходит с `editDate: null` — это не правка в 1970 году.
299
+ editedAt: message.editDate ? new Date(message.editDate * 1000) : undefined,
297
300
  attachments: collectAttachments(message, chatId),
298
301
  links: collectLinks(message),
302
+ mentionsMe: typeof message.mentioned === "boolean" ? message.mentioned : undefined,
299
303
  replyToMessageId,
300
304
  threadId,
301
305
  forwardedFrom: toForwardedFrom(message.fwdFrom),
@@ -17,7 +17,8 @@ import type {
17
17
  TransportStatus,
18
18
  TypingTransport,
19
19
  } from "@imessaging/core";
20
- import { isTelegramRecipient, MemoryPeerStore } from "@imessaging/core";
20
+ import { isTelegramRecipient, MemoryPeerStore, formattedOf } from "@imessaging/core";
21
+ import { toTelegramEntities } from "./entities";
21
22
  import { peerToChat, unmarkPeerId } from "./incoming";
22
23
  import bigInt from "big-integer";
23
24
  import { Api, TelegramClient } from "telegram";
@@ -42,6 +43,7 @@ export type TelegramChatSummary = {
42
43
  title: string;
43
44
  username?: string;
44
45
  unreadCount: number;
46
+ unreadMentions: number;
45
47
  };
46
48
 
47
49
  export type TelegramMtprotoTransportOptions = {
@@ -407,7 +409,14 @@ export class TelegramMtprotoTransport
407
409
  );
408
410
  }
409
411
 
410
- summaries.push({ chatId, chatType, title, username, unreadCount: dialog.unreadCount });
412
+ summaries.push({
413
+ chatId,
414
+ chatType,
415
+ title,
416
+ username,
417
+ unreadCount: dialog.unreadCount,
418
+ unreadMentions: dialog.unreadMentionsCount ?? 0,
419
+ });
411
420
  if (summaries.length >= limit) break;
412
421
  }
413
422
  return summaries;
@@ -416,16 +425,21 @@ export class TelegramMtprotoTransport
416
425
  /**
417
426
  * Последние `limit` сообщений чата, от старых к новым, ВКЛЮЧАЯ свои — это чтение истории, а не приём.
418
427
  * Служебные события (вход в группу, закреп) пропускаются: текста и вложений у них нет.
428
+ *
429
+ * @param options.mentionsOnly Только упоминания аккаунта. Фильтр работает на уровне всего чата;
430
+ * вызывающий код должен сам отфильтровать результат по `threadId`.
419
431
  */
420
432
  async history(
421
433
  recipient: TelegramRecipient,
422
- options?: { limit?: number; threadId?: number },
434
+ options?: { limit?: number; threadId?: number; mentionsOnly?: boolean },
423
435
  ): Promise<IncomingMessage[]> {
424
436
  this.assertConnected();
425
437
  const peer = await this.getInputPeer(recipient);
438
+ // gramjs игнорирует filter, если передан replyTo, поэтому для mentionsOnly replyTo не передаём
426
439
  const messages = await this.client.getMessages(peer, {
427
440
  limit: options?.limit ?? 30,
428
- replyTo: options?.threadId,
441
+ replyTo: options?.mentionsOnly ? undefined : options?.threadId,
442
+ filter: options?.mentionsOnly ? new Api.InputMessagesFilterMyMentions() : undefined,
429
443
  });
430
444
  return [...messages]
431
445
  .reverse()
@@ -439,12 +453,40 @@ export class TelegramMtprotoTransport
439
453
  );
440
454
  }
441
455
 
456
+ /**
457
+ * Запросить конкретные сообщения по их номерам.
458
+ * Контекст для упоминаний, то есть сообщение, на которое кто-то ответил.
459
+ * Если запросить пустой список, вернёт `[]` без сетевых вызовов (но после проверки соединения).
460
+ */
461
+ async messagesByIds(recipient: TelegramRecipient, ids: number[]): Promise<IncomingMessage[]> {
462
+ this.assertConnected();
463
+ if (ids.length === 0) return [];
464
+ const peer = await this.getInputPeer(recipient);
465
+ const messages = await this.client.getMessages(peer, { ids });
466
+ return messages
467
+ .filter((message) => message && message.className === "Message")
468
+ .map((message) =>
469
+ toIncomingMessage(
470
+ message as unknown as MtprotoMessageLike,
471
+ this.id,
472
+ this.options.accountId,
473
+ ),
474
+ )
475
+ .sort((a, b) => Number(a.messageId) - Number(b.messageId));
476
+ }
477
+
442
478
  private async sendWithRetry(message: OutboundMessage, retried: boolean): Promise<SendResult> {
443
479
  try {
444
480
  const peer = await this.getInputPeer(message.recipient);
481
+ // С `format` уходят готовые выделения: gramjs тогда не разбирает текст своим markdown вовсе.
482
+ const ft = formattedOf(message);
483
+ const text = ft ? ft.text : message.text;
484
+ const parseMode = ft ? undefined : message.parseMode?.toLowerCase();
485
+ const formattingEntities = ft ? toTelegramEntities(ft) : undefined;
445
486
  const sent = await this.client.sendMessage(peer, {
446
- message: message.text,
447
- parseMode: message.parseMode?.toLowerCase(),
487
+ message: text,
488
+ parseMode,
489
+ formattingEntities,
448
490
  replyTo: message.replyToMessageId ?? message.threadId,
449
491
  });
450
492
  for (const document of message.documents ?? []) {
@@ -604,12 +646,17 @@ export class TelegramMtprotoTransport
604
646
  let peer: Api.TypeInputPeer | null = null;
605
647
  try {
606
648
  peer = await this.getInputPeer(message.recipient);
649
+ const ft = formattedOf(message);
650
+ const text = ft ? ft.text : message.text;
651
+ const parseMode = ft ? undefined : message.parseMode?.toLowerCase();
652
+ const formattingEntities = ft ? toTelegramEntities(ft) : undefined;
607
653
  // Кнопки здесь не передаются — ровно как и при отправке этим транспортом: inline-клавиатуру
608
654
  // MTProto-аккаунт не рисует. Передать их сюда значило бы обещать разметку, которой не будет.
609
655
  await this.client.editMessage(peer, {
610
656
  message: messageId,
611
- text: message.text,
612
- parseMode: message.parseMode?.toLowerCase(),
657
+ text,
658
+ parseMode,
659
+ formattingEntities,
613
660
  });
614
661
  this.lastActivityAt = new Date();
615
662
  return {