@imessaging/telegram-mtproto 0.8.13 → 0.8.15

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.13",
3
+ "version": "0.8.15",
4
4
  "description": "Telegram MTProto user-account transport for imessaging",
5
5
  "keywords": [
6
6
  "messaging",
@@ -31,10 +31,10 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@imessaging/core": "^0.8.11",
34
+ "@imessaging/core": "^0.8.13",
35
35
  "big-integer": "^1.6.52"
36
36
  },
37
37
  "peerDependencies": {
38
- "telegram": ">=1.224.1"
38
+ "telegram": ">=1.229.1"
39
39
  }
40
40
  }
package/src/incoming.ts CHANGED
@@ -2,9 +2,12 @@ import type {
2
2
  IncomingAttachment,
3
3
  IncomingLink,
4
4
  IncomingMessage,
5
+ IncomingMessageKind,
5
6
  TextEntity,
6
7
  TextEntityType,
7
8
  } from "@imessaging/core";
9
+ import type { Api } from "telegram";
10
+ import { renderRichText, toPlainText } from "telegram/richMessage";
8
11
 
9
12
  /** Пир MTProto в той части, по которой определяется чат. Приходит из `Api.TypePeer`. */
10
13
  export type MtprotoPeerLike = {
@@ -27,7 +30,12 @@ export type MtprotoMessageLike = {
27
30
  mentioned?: boolean | null;
28
31
  peerId?: MtprotoPeerLike;
29
32
  fromId?: MtprotoPeerLike;
30
- media?: MtprotoMediaLike;
33
+ media?: MtprotoMediaLike | null;
34
+ /**
35
+ * «Богатое» сообщение (слой 228+): текст лежит деревом блоков, а `message` пуст.
36
+ * Слой 224 отдавал такое как MessageMediaUnsupported — текст терялся (замер 16.10.2026).
37
+ */
38
+ richMessage?: Api.TypeRichMessage | null;
31
39
  /**
32
40
  * Разметка текста: жирное, ссылки, упоминания. Отсюда берутся адреса, которых В ТЕКСТЕ НЕТ.
33
41
  *
@@ -88,7 +96,14 @@ export type MtprotoMediaLike = {
88
96
  document?: {
89
97
  mimeType?: string;
90
98
  size?: { toString(): string } | number;
91
- attributes?: { className?: string; fileName?: string }[];
99
+ attributes?: {
100
+ className?: string;
101
+ fileName?: string;
102
+ /** DocumentAttributeAudio: голосовое, а не музыка. */
103
+ voice?: boolean;
104
+ /** DocumentAttributeVideo: кружок, а не видео. */
105
+ roundMessage?: boolean;
106
+ }[];
92
107
  };
93
108
  photo?: unknown;
94
109
  /** Предпросмотр страницы. Вложением НЕ считается: скачивать нечего, адрес есть. */
@@ -104,11 +119,15 @@ export type MtprotoEntityLike = {
104
119
  language?: string;
105
120
  };
106
121
 
107
- /** Кнопка под сообщением. Адрес несёт только `KeyboardButtonUrl`, у остальных его нет. */
122
+ /**
123
+ * Кнопка под сообщением. До слоя 229 адрес нёс `KeyboardButtonUrl.url`, с 229 —
124
+ * `KeyboardInlineButton.type` вида `InlineButtonTypeUrl`.
125
+ */
108
126
  export type MtprotoButtonLike = {
109
127
  className?: string;
110
128
  text?: string;
111
129
  url?: string;
130
+ type?: { className?: string; url?: string };
112
131
  };
113
132
 
114
133
  /**
@@ -235,13 +254,79 @@ export function collectLinks(message: MtprotoMessageLike): IncomingLink[] {
235
254
  }
236
255
 
237
256
  for (const row of message.replyMarkup?.rows ?? []) {
238
- for (const button of row.buttons ?? []) add(button.url, "button", button.text);
257
+ for (const button of row.buttons ?? [])
258
+ add(button.url ?? button.type?.url, "button", button.text);
239
259
  }
240
260
 
261
+ if (message.richMessage) collectRichLinks(message.richMessage, add);
241
262
  add(message.media?.webpage?.url, "preview", message.media?.webpage?.title);
242
263
  return links;
243
264
  }
244
265
 
266
+ /** Адреса из дерева richMessage: TextUrl (адрес в поле) и TextAutoUrl (адрес — сам текст). */
267
+ function collectRichLinks(
268
+ node: unknown,
269
+ add: (url: string | undefined, origin: IncomingLink["origin"], label?: string) => void,
270
+ ): void {
271
+ if (Array.isArray(node)) {
272
+ for (const item of node) collectRichLinks(item, add);
273
+ return;
274
+ }
275
+ if (!node || typeof node !== "object") return;
276
+ const rich = node as { className?: string; url?: string; text?: Api.TypeRichText };
277
+ if (rich.className === "TextUrl") {
278
+ add(rich.url, "markup", renderRichText(rich.text) || undefined);
279
+ return;
280
+ }
281
+ if (rich.className === "TextAutoUrl") {
282
+ add(renderRichText(rich.text), "text");
283
+ return;
284
+ }
285
+ for (const [key, value] of Object.entries(node)) {
286
+ // Фото и документы richMessage — медиа, не текст: в них не спускаемся.
287
+ if (key === "photos" || key === "documents" || key === "originalArgs") continue;
288
+ if (value && typeof value === "object") collectRichLinks(value, add);
289
+ }
290
+ }
291
+
292
+ /** Текст сообщения: обычный `message`, а если он пуст — плоский текст блоков richMessage. */
293
+ export function messageText(message: MtprotoMessageLike): string {
294
+ if (message.message) return message.message;
295
+ if (message.richMessage?.className === "RichMessage") return toPlainText(message.richMessage);
296
+ return "";
297
+ }
298
+
299
+ /** Вид содержимого по media. Голосовое и кружок различаются только атрибутами документа. */
300
+ export function messageKind(message: MtprotoMessageLike): IncomingMessageKind {
301
+ const media = message.media;
302
+ if (!media || media.className === "MessageMediaWebPage") return "text";
303
+ switch (media.className) {
304
+ case "MessageMediaPhoto":
305
+ return "photo";
306
+ case "MessageMediaPoll":
307
+ return "poll";
308
+ case "MessageMediaContact":
309
+ return "contact";
310
+ case "MessageMediaGeo":
311
+ case "MessageMediaGeoLive":
312
+ case "MessageMediaVenue":
313
+ return "geo";
314
+ case "MessageMediaUnsupported":
315
+ return "unsupported";
316
+ }
317
+ if (media.document) {
318
+ const attributes = media.document.attributes ?? [];
319
+ const has = (name: string) => attributes.find((attribute) => attribute.className === name);
320
+ if (has("DocumentAttributeAudio")?.voice) return "voice";
321
+ if (has("DocumentAttributeSticker")) return "sticker";
322
+ const video = has("DocumentAttributeVideo");
323
+ if (video?.roundMessage) return "video_note";
324
+ if (video) return "video";
325
+ return "document";
326
+ }
327
+ return "other";
328
+ }
329
+
245
330
  const FORMATTING: Record<string, TextEntityType> = {
246
331
  MessageEntityBold: "bold",
247
332
  MessageEntityItalic: "italic",
@@ -333,7 +418,8 @@ export function toIncomingMessage(
333
418
  chatUsername: message.chat?.username ?? undefined,
334
419
  messageId: String(message.id),
335
420
  albumId: message.groupedId?.toString(),
336
- text: message.message ?? "",
421
+ text: messageText(message),
422
+ kind: messageKind(message),
337
423
  sentAt: message.date ? new Date(message.date * 1000) : undefined,
338
424
  // Непоправленное сообщение приходит с `editDate: null` — это не правка в 1970 году.
339
425
  editedAt: message.editDate ? new Date(message.editDate * 1000) : undefined,
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@ export {
6
6
  export {
7
7
  collectAttachments,
8
8
  collectLinks,
9
+ messageKind,
10
+ messageText,
9
11
  downloadFromMessage,
10
12
  formattingEntities,
11
13
  IncomingDispatcher,
@@ -29,6 +29,8 @@ import type {
29
29
  OutboundMessage,
30
30
  ReactionTransport,
31
31
  ResolvedPeer,
32
+ SelfAccount,
33
+ SelfAwareTransport,
32
34
  SendResult,
33
35
  TelegramPeerStore,
34
36
  TelegramRecipient,
@@ -163,6 +165,7 @@ export class TelegramMtprotoTransport
163
165
  PeerInfoTransport,
164
166
  PhoneLookupTransport,
165
167
  ReactionTransport,
168
+ SelfAwareTransport,
166
169
  ReadReceiptTransport,
167
170
  TypingTransport
168
171
  {
@@ -191,6 +194,8 @@ export class TelegramMtprotoTransport
191
194
  private readonly ownEdited = new Map<string, number>();
192
195
  /** Идущие send/edit: обновление о своём сообщении может прийти раньше, чем вызов вернёт его номер. */
193
196
  private readonly ownPending = new Set<Promise<unknown>>();
197
+ /** Своя учётка: не меняется за жизнь сессии, спрашивается один раз. */
198
+ private selfAccount?: Promise<SelfAccount>;
194
199
  /** Отметка жизни подписки: срабатывает на любом обновлении, включая служебные. */
195
200
  private activityListener: ((event: unknown) => void) | null = null;
196
201
 
@@ -733,6 +738,26 @@ export class TelegramMtprotoTransport
733
738
  this.lastActivityAt = new Date();
734
739
  }
735
740
 
741
+ /** Своя учётка — users.GetUsers(InputUserSelf); удачный ответ кэшируется, отказ — нет. */
742
+ async self(): Promise<SelfAccount> {
743
+ this.assertConnected();
744
+ this.selfAccount ??= this.client
745
+ .invoke(new Api.users.GetUsers({ id: [new Api.InputUserSelf()] }))
746
+ .then(([user]) => {
747
+ if (!user || !isUser(user)) throw new Error("users.GetUsers(self) returned no user");
748
+ return {
749
+ id: user.id.toString(),
750
+ ...(user.username ? { username: user.username } : {}),
751
+ isBot: user.bot === true,
752
+ };
753
+ })
754
+ .catch((error: unknown) => {
755
+ this.selfAccount = undefined;
756
+ throw error;
757
+ });
758
+ return this.selfAccount;
759
+ }
760
+
736
761
  /**
737
762
  * Контакт ли собеседник и взаимно ли. Пир берётся из хранилища и кэша клиента, иначе по @username;
738
763
  * по телефону не ищет — это findByPhone.