@imessaging/telegram-mtproto 0.7.1 → 0.7.2

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.1",
3
+ "version": "0.7.2",
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.1",
34
+ "@imessaging/core": "^0.7.2",
35
35
  "big-integer": "^1.6.52"
36
36
  },
37
37
  "peerDependencies": {
package/src/incoming.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { IncomingAttachment, IncomingMessage } from "@imessaging/core";
1
+ import type { IncomingAttachment, IncomingLink, IncomingMessage } from "@imessaging/core";
2
2
 
3
3
  /** Пир MTProto в той части, по которой определяется чат. Приходит из `Api.TypePeer`. */
4
4
  export type MtprotoPeerLike = {
@@ -17,6 +17,15 @@ export type MtprotoMessageLike = {
17
17
  peerId?: MtprotoPeerLike;
18
18
  fromId?: MtprotoPeerLike;
19
19
  media?: MtprotoMediaLike;
20
+ /**
21
+ * Разметка текста: жирное, ссылки, упоминания. Отсюда берутся адреса, которых В ТЕКСТЕ НЕТ.
22
+ *
23
+ * Смещения Telegram считает в кодовых единицах UTF-16 — тех же, которыми режет строку JS, так
24
+ * что `slice` по ним точен. Считать в символах значило бы промахиваться на каждом эмодзи.
25
+ */
26
+ entities?: MtprotoEntityLike[];
27
+ /** Кнопки под сообщением. У поста в канале здесь `ReplyInlineMarkup`, у остальных пусто. */
28
+ replyMarkup?: { rows?: { buttons?: MtprotoButtonLike[] }[] };
20
29
  replyTo?: { replyToMsgId?: number | string };
21
30
  /** Заголовок форварда. Пусто — сообщение своё, не пересланное. */
22
31
  fwdFrom?: {
@@ -52,6 +61,23 @@ export type MtprotoMediaLike = {
52
61
  attributes?: { className?: string; fileName?: string }[];
53
62
  };
54
63
  photo?: unknown;
64
+ /** Предпросмотр страницы. Вложением НЕ считается: скачивать нечего, адрес есть. */
65
+ webpage?: { className?: string; url?: string; displayUrl?: string; title?: string };
66
+ };
67
+
68
+ /** Сущность разметки. `url` есть только у скрытой гиперссылки (`MessageEntityTextUrl`). */
69
+ export type MtprotoEntityLike = {
70
+ className?: string;
71
+ offset?: number;
72
+ length?: number;
73
+ url?: string;
74
+ };
75
+
76
+ /** Кнопка под сообщением. Адрес несёт только `KeyboardButtonUrl`, у остальных его нет. */
77
+ export type MtprotoButtonLike = {
78
+ className?: string;
79
+ text?: string;
80
+ url?: string;
55
81
  };
56
82
 
57
83
  /**
@@ -96,9 +122,7 @@ export function unmarkPeerId(chatId: string): string {
96
122
  * Пир источника МОЖЕТ отсутствовать: автор форварда вправе скрыть его. Тогда остаётся имя, а
97
123
  * иногда и оно пустое — и это честнее выдумывания, чем подстановка чата, в котором сообщение лежит.
98
124
  */
99
- function toForwardedFrom(
100
- fwdFrom: MtprotoMessageLike["fwdFrom"],
101
- ): IncomingMessage["forwardedFrom"] {
125
+ function toForwardedFrom(fwdFrom: MtprotoMessageLike["fwdFrom"]): IncomingMessage["forwardedFrom"] {
102
126
  if (!fwdFrom) return undefined;
103
127
  const peer = fwdFrom.fromId ? peerToChat(fwdFrom.fromId) : null;
104
128
  return {
@@ -146,6 +170,47 @@ export function collectAttachments(
146
170
  return [];
147
171
  }
148
172
 
173
+ /**
174
+ * Адреса из сообщения: текст, скрытые гиперссылки, кнопки, предпросмотр.
175
+ *
176
+ * Разбирается ЗДЕСЬ, а не у потребителя, по одной причине: половины этих адресов в тексте нет.
177
+ * Слово «откликнуться» со скрытой ссылкой, надпись на кнопке и предпросмотр живут в разметке, и
178
+ * поиск регулярным выражением по `text` не находит их ВООБЩЕ — а выглядит это не промахом
179
+ * разбора, а вакансией без ссылки. Замер 16.09.2026 по боевой базе italent: у всех 379 вакансий,
180
+ * приехавших шлюзом, поле адреса пустое, тогда как у 8190 прежних оно заполнено.
181
+ *
182
+ * Повторы снимаются по адресу: один и тот же URL часто и в тексте, и в предпросмотре, и
183
+ * потребитель, считающий их разными, показал бы человеку две одинаковые ссылки.
184
+ */
185
+ export function collectLinks(message: MtprotoMessageLike): IncomingLink[] {
186
+ const text = message.message ?? "";
187
+ const links: IncomingLink[] = [];
188
+ const seen = new Set<string>();
189
+ const add = (url: string | undefined, origin: IncomingLink["origin"], label?: string): void => {
190
+ const trimmed = url?.trim();
191
+ if (!trimmed || seen.has(trimmed)) return;
192
+ seen.add(trimmed);
193
+ links.push(label ? { url: trimmed, label, origin } : { url: trimmed, origin });
194
+ };
195
+
196
+ // Порядок — как в сообщении: смещения приходят по возрастанию, но полагаться на это не надо.
197
+ const entities = [...(message.entities ?? [])].sort((a, b) => (a.offset ?? 0) - (b.offset ?? 0));
198
+ for (const entity of entities) {
199
+ const shown = text.slice(entity.offset ?? 0, (entity.offset ?? 0) + (entity.length ?? 0));
200
+ // Голый адрес: Telegram размечает его сам, и сам адрес — это и есть показанный текст.
201
+ if (entity.className === "MessageEntityUrl") add(shown, "text");
202
+ // Скрытая гиперссылка: адрес в поле, а показанное слово — подпись к нему.
203
+ if (entity.className === "MessageEntityTextUrl") add(entity.url, "markup", shown || undefined);
204
+ }
205
+
206
+ for (const row of message.replyMarkup?.rows ?? []) {
207
+ for (const button of row.buttons ?? []) add(button.url, "button", button.text);
208
+ }
209
+
210
+ add(message.media?.webpage?.url, "preview", message.media?.webpage?.title);
211
+ return links;
212
+ }
213
+
149
214
  /**
150
215
  * Сообщение живой учётной записи → общий вид.
151
216
  *
@@ -178,6 +243,7 @@ export function toIncomingMessage(
178
243
  messageId: String(message.id),
179
244
  text: message.message ?? "",
180
245
  attachments: collectAttachments(message, chatId),
246
+ links: collectLinks(message),
181
247
  replyToMessageId:
182
248
  message.replyTo?.replyToMsgId === undefined
183
249
  ? undefined
package/src/index.ts CHANGED
@@ -4,12 +4,15 @@ export {
4
4
  } from "./telegram-mtproto-transport";
5
5
  export {
6
6
  collectAttachments,
7
+ collectLinks,
7
8
  downloadFromMessage,
8
9
  IncomingDispatcher,
9
10
  peerToChat,
10
11
  unmarkPeerId,
11
12
  toIncomingMessage,
12
13
  type DownloadingClient,
14
+ type MtprotoButtonLike,
15
+ type MtprotoEntityLike,
13
16
  type MtprotoMediaLike,
14
17
  type MtprotoMessageLike,
15
18
  type MtprotoPeerLike,