@imessaging/telegram-mtproto 0.7.0 → 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.0",
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.0",
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,7 +17,22 @@ 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 };
30
+ /** Заголовок форварда. Пусто — сообщение своё, не пересланное. */
31
+ fwdFrom?: {
32
+ fromId?: MtprotoPeerLike;
33
+ fromName?: string;
34
+ channelPost?: number;
35
+ };
21
36
  /**
22
37
  * Отправитель целиком — его приносит обновление вместе с `accessHash`.
23
38
  *
@@ -46,6 +61,23 @@ export type MtprotoMediaLike = {
46
61
  attributes?: { className?: string; fileName?: string }[];
47
62
  };
48
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;
49
81
  };
50
82
 
51
83
  /**
@@ -84,6 +116,23 @@ export function unmarkPeerId(chatId: string): string {
84
116
  return chatId;
85
117
  }
86
118
 
119
+ /**
120
+ * Источник форварда → общий вид.
121
+ *
122
+ * Пир источника МОЖЕТ отсутствовать: автор форварда вправе скрыть его. Тогда остаётся имя, а
123
+ * иногда и оно пустое — и это честнее выдумывания, чем подстановка чата, в котором сообщение лежит.
124
+ */
125
+ function toForwardedFrom(fwdFrom: MtprotoMessageLike["fwdFrom"]): IncomingMessage["forwardedFrom"] {
126
+ if (!fwdFrom) return undefined;
127
+ const peer = fwdFrom.fromId ? peerToChat(fwdFrom.fromId) : null;
128
+ return {
129
+ chatId: peer?.chatId,
130
+ chatType: peer?.chatType,
131
+ title: fwdFrom.fromName,
132
+ messageId: fwdFrom.channelPost === undefined ? undefined : String(fwdFrom.channelPost),
133
+ };
134
+ }
135
+
87
136
  /**
88
137
  * Что приехало файлом.
89
138
  *
@@ -121,6 +170,47 @@ export function collectAttachments(
121
170
  return [];
122
171
  }
123
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
+
124
214
  /**
125
215
  * Сообщение живой учётной записи → общий вид.
126
216
  *
@@ -153,10 +243,12 @@ export function toIncomingMessage(
153
243
  messageId: String(message.id),
154
244
  text: message.message ?? "",
155
245
  attachments: collectAttachments(message, chatId),
246
+ links: collectLinks(message),
156
247
  replyToMessageId:
157
248
  message.replyTo?.replyToMsgId === undefined
158
249
  ? undefined
159
250
  : String(message.replyTo.replyToMsgId),
251
+ forwardedFrom: toForwardedFrom(message.fwdFrom),
160
252
  };
161
253
  }
162
254
 
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,