@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 +3 -3
- package/src/incoming.ts +91 -5
- package/src/index.ts +2 -0
- package/src/telegram-mtproto-transport.ts +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imessaging/telegram-mtproto",
|
|
3
|
-
"version": "0.8.
|
|
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.
|
|
34
|
+
"@imessaging/core": "^0.8.13",
|
|
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
|
@@ -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.
|