@imessaging/telegram-mtproto 0.8.14 → 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.14",
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.12",
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,