@imessaging/telegram-mtproto 0.8.14 → 0.8.16
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 +3 -3
- package/src/incoming.ts +91 -5
- package/src/index.ts +2 -0
- package/src/telegram-mtproto-transport.ts +14 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imessaging/telegram-mtproto",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.16",
|
|
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.
|
|
34
|
+
"@imessaging/core": "^0.8.15",
|
|
35
35
|
"big-integer": "^1.6.52"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"telegram": ">=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?: {
|
|
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
|
-
/**
|
|
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 ?? [])
|
|
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
|
|
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
|
@@ -413,6 +413,12 @@ export class TelegramMtprotoTransport
|
|
|
413
413
|
if (replyTo !== undefined && (!Number.isSafeInteger(replyTo) || replyTo <= 0)) {
|
|
414
414
|
throw new Error(`Telegram message id must be a positive integer: ${draft.replyToMessageId}`);
|
|
415
415
|
}
|
|
416
|
+
const threadId = draft.threadId;
|
|
417
|
+
if (threadId !== undefined && (!Number.isSafeInteger(threadId) || threadId <= 0)) {
|
|
418
|
+
throw new Error(`Telegram thread id must be a positive integer: ${threadId}`);
|
|
419
|
+
}
|
|
420
|
+
// Как у send: тема без ответа — ответ на её корень; ответ в теме — ещё и topMsgId.
|
|
421
|
+
const replyToMsgId = replyTo ?? threadId;
|
|
416
422
|
const ft = draft.format ? formatText(draft.text, draft.format) : undefined;
|
|
417
423
|
const save = async (peer: Api.TypeInputPeer) =>
|
|
418
424
|
this.client.invoke(
|
|
@@ -420,7 +426,14 @@ export class TelegramMtprotoTransport
|
|
|
420
426
|
peer,
|
|
421
427
|
message: ft ? ft.text : draft.text,
|
|
422
428
|
...(ft && ft.entities.length > 0 ? { entities: toTelegramEntities(ft) } : {}),
|
|
423
|
-
...(
|
|
429
|
+
...(replyToMsgId
|
|
430
|
+
? {
|
|
431
|
+
replyTo: new Api.InputReplyToMessage({
|
|
432
|
+
replyToMsgId,
|
|
433
|
+
...(replyTo && threadId ? { topMsgId: threadId } : {}),
|
|
434
|
+
}),
|
|
435
|
+
}
|
|
436
|
+
: {}),
|
|
424
437
|
}),
|
|
425
438
|
);
|
|
426
439
|
try {
|