@imessaging/telegram-mtproto 0.7.2 → 0.7.4
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 +2 -2
- package/src/incoming.ts +56 -6
- package/src/index.ts +1 -0
- package/src/telegram-mtproto-transport.ts +91 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imessaging/telegram-mtproto",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
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.
|
|
34
|
+
"@imessaging/core": "^0.7.4",
|
|
35
35
|
"big-integer": "^1.6.52"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
package/src/incoming.ts
CHANGED
|
@@ -14,6 +14,9 @@ export type MtprotoMessageLike = {
|
|
|
14
14
|
message?: string;
|
|
15
15
|
/** `true` — сообщение отправлено САМОЙ учётной записью. */
|
|
16
16
|
out?: boolean;
|
|
17
|
+
date?: number;
|
|
18
|
+
editDate?: number;
|
|
19
|
+
groupedId?: { toString(): string };
|
|
17
20
|
peerId?: MtprotoPeerLike;
|
|
18
21
|
fromId?: MtprotoPeerLike;
|
|
19
22
|
media?: MtprotoMediaLike;
|
|
@@ -26,7 +29,11 @@ export type MtprotoMessageLike = {
|
|
|
26
29
|
entities?: MtprotoEntityLike[];
|
|
27
30
|
/** Кнопки под сообщением. У поста в канале здесь `ReplyInlineMarkup`, у остальных пусто. */
|
|
28
31
|
replyMarkup?: { rows?: { buttons?: MtprotoButtonLike[] }[] };
|
|
29
|
-
replyTo?: {
|
|
32
|
+
replyTo?: {
|
|
33
|
+
replyToMsgId?: number | string;
|
|
34
|
+
replyToTopId?: number | string;
|
|
35
|
+
forumTopic?: boolean;
|
|
36
|
+
};
|
|
30
37
|
/** Заголовок форварда. Пусто — сообщение своё, не пересланное. */
|
|
31
38
|
fwdFrom?: {
|
|
32
39
|
fromId?: MtprotoPeerLike;
|
|
@@ -39,7 +46,20 @@ export type MtprotoMessageLike = {
|
|
|
39
46
|
* Нужен не ради имени: по одному номеру ответить нельзя, пока собеседник клиенту неизвестен, а
|
|
40
47
|
* `accessHash` живёт только здесь. Поле необязательное: в обновлении его может не быть.
|
|
41
48
|
*/
|
|
42
|
-
sender?: {
|
|
49
|
+
sender?: {
|
|
50
|
+
username?: string;
|
|
51
|
+
firstName?: string;
|
|
52
|
+
lastName?: string;
|
|
53
|
+
title?: string;
|
|
54
|
+
accessHash?: { toString(): string } | string | number;
|
|
55
|
+
};
|
|
56
|
+
/** Сущность САМОГО чата из обновления: канал, группа или собеседник. Может не прийти. */
|
|
57
|
+
chat?: {
|
|
58
|
+
username?: string;
|
|
59
|
+
firstName?: string;
|
|
60
|
+
lastName?: string;
|
|
61
|
+
title?: string;
|
|
62
|
+
};
|
|
43
63
|
/**
|
|
44
64
|
* Пир отправителя ВМЕСТЕ с `accessHash` — так его отдаёт само обновление.
|
|
45
65
|
*
|
|
@@ -231,6 +251,32 @@ export function toIncomingMessage(
|
|
|
231
251
|
? chatId
|
|
232
252
|
: null;
|
|
233
253
|
|
|
254
|
+
let senderName: string | undefined;
|
|
255
|
+
if (message.sender) {
|
|
256
|
+
const parts = [message.sender.firstName, message.sender.lastName].filter(Boolean) as string[];
|
|
257
|
+
if (parts.length > 0) senderName = parts.join(" ").trim();
|
|
258
|
+
else if (message.sender.title) senderName = message.sender.title;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const chatName = [message.chat?.firstName, message.chat?.lastName]
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
.join(" ")
|
|
264
|
+
.trim();
|
|
265
|
+
const chatTitle = message.chat?.title || chatName || undefined;
|
|
266
|
+
|
|
267
|
+
let threadId: number | undefined;
|
|
268
|
+
let replyToMessageId: string | undefined;
|
|
269
|
+
if (message.replyTo?.forumTopic === true) {
|
|
270
|
+
threadId = Number(message.replyTo.replyToTopId ?? message.replyTo.replyToMsgId);
|
|
271
|
+
replyToMessageId =
|
|
272
|
+
message.replyTo.replyToTopId === undefined ? undefined : String(message.replyTo.replyToMsgId);
|
|
273
|
+
} else {
|
|
274
|
+
replyToMessageId =
|
|
275
|
+
message.replyTo?.replyToMsgId === undefined
|
|
276
|
+
? undefined
|
|
277
|
+
: String(message.replyTo.replyToMsgId);
|
|
278
|
+
}
|
|
279
|
+
|
|
234
280
|
return {
|
|
235
281
|
transportId,
|
|
236
282
|
accountId,
|
|
@@ -240,14 +286,18 @@ export function toIncomingMessage(
|
|
|
240
286
|
// Имя отправителя: по нему отвечают, когда номер клиенту неизвестен, и по нему же продукты
|
|
241
287
|
// связывают человека со своей учётной записью.
|
|
242
288
|
senderUsername: message.sender?.username,
|
|
289
|
+
senderName,
|
|
290
|
+
chatTitle,
|
|
291
|
+
chatUsername: message.chat?.username ?? undefined,
|
|
243
292
|
messageId: String(message.id),
|
|
293
|
+
albumId: message.groupedId?.toString(),
|
|
244
294
|
text: message.message ?? "",
|
|
295
|
+
sentAt: message.date !== undefined ? new Date(message.date * 1000) : undefined,
|
|
296
|
+
editedAt: message.editDate !== undefined ? new Date(message.editDate * 1000) : undefined,
|
|
245
297
|
attachments: collectAttachments(message, chatId),
|
|
246
298
|
links: collectLinks(message),
|
|
247
|
-
replyToMessageId
|
|
248
|
-
|
|
249
|
-
? undefined
|
|
250
|
-
: String(message.replyTo.replyToMsgId),
|
|
299
|
+
replyToMessageId,
|
|
300
|
+
threadId,
|
|
251
301
|
forwardedFrom: toForwardedFrom(message.fwdFrom),
|
|
252
302
|
};
|
|
253
303
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ResolvedPeer,
|
|
14
14
|
SendResult,
|
|
15
15
|
TelegramPeerStore,
|
|
16
|
+
TelegramRecipient,
|
|
16
17
|
TransportStatus,
|
|
17
18
|
TypingTransport,
|
|
18
19
|
} from "@imessaging/core";
|
|
@@ -29,10 +30,20 @@ import { StringSession } from "telegram/sessions";
|
|
|
29
30
|
import {
|
|
30
31
|
downloadFromMessage,
|
|
31
32
|
IncomingDispatcher,
|
|
33
|
+
toIncomingMessage,
|
|
32
34
|
type DownloadingClient,
|
|
33
35
|
type MtprotoMessageLike,
|
|
36
|
+
type MtprotoPeerLike,
|
|
34
37
|
} from "./incoming";
|
|
35
38
|
|
|
39
|
+
export type TelegramChatSummary = {
|
|
40
|
+
chatId: string;
|
|
41
|
+
chatType: IncomingMessage["chatType"];
|
|
42
|
+
title: string;
|
|
43
|
+
username?: string;
|
|
44
|
+
unreadCount: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
36
47
|
export type TelegramMtprotoTransportOptions = {
|
|
37
48
|
accountId: string;
|
|
38
49
|
apiId: number;
|
|
@@ -348,6 +359,86 @@ export class TelegramMtprotoTransport
|
|
|
348
359
|
};
|
|
349
360
|
}
|
|
350
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Диалоги учётки; `query` — подстрока названия без учёта регистра.
|
|
364
|
+
* Пир каждого диалога запоминается: следующий `history` по chatId не обходит диалоги заново.
|
|
365
|
+
*/
|
|
366
|
+
async listChats(options?: { query?: string; limit?: number }): Promise<TelegramChatSummary[]> {
|
|
367
|
+
this.assertConnected();
|
|
368
|
+
const limit = options?.limit ?? 100;
|
|
369
|
+
const query = options?.query?.toLowerCase();
|
|
370
|
+
// С запросом обходим всё: совпадение может лежать в самом конце списка. Без него — ровно limit.
|
|
371
|
+
const dialogs = await this.client.getDialogs(query === undefined ? { limit } : {});
|
|
372
|
+
const summaries: TelegramChatSummary[] = [];
|
|
373
|
+
|
|
374
|
+
for (const dialog of dialogs) {
|
|
375
|
+
if (!dialog.entity) continue;
|
|
376
|
+
const title = dialog.title ?? "";
|
|
377
|
+
if (query !== undefined && !title.toLowerCase().includes(query)) continue;
|
|
378
|
+
|
|
379
|
+
// Супергруппа — тоже канал (isChannel и isGroup сразу), поэтому канал проверяется первым.
|
|
380
|
+
const entityId = dialog.entity.id.toString();
|
|
381
|
+
const peerLike: MtprotoPeerLike | undefined = dialog.isChannel
|
|
382
|
+
? { channelId: entityId }
|
|
383
|
+
: dialog.isGroup
|
|
384
|
+
? { chatId: entityId }
|
|
385
|
+
: dialog.isUser
|
|
386
|
+
? { userId: entityId }
|
|
387
|
+
: undefined;
|
|
388
|
+
if (!peerLike) continue;
|
|
389
|
+
const { chatId, chatType } = peerToChat(peerLike);
|
|
390
|
+
const entity = dialog.entity as { accessHash?: { toString(): string }; username?: string };
|
|
391
|
+
const accessHash = entity.accessHash?.toString();
|
|
392
|
+
const username = entity.username ?? undefined;
|
|
393
|
+
|
|
394
|
+
// У обычной группы access hash не бывает — ей хватает номера, и запомнить её тоже нужно.
|
|
395
|
+
if (accessHash || chatType === "group") {
|
|
396
|
+
await this.peerStore.set(
|
|
397
|
+
this.options.accountId,
|
|
398
|
+
{ type: chatType, id: chatId },
|
|
399
|
+
{
|
|
400
|
+
type: chatType,
|
|
401
|
+
id: chatId,
|
|
402
|
+
accessHash,
|
|
403
|
+
username,
|
|
404
|
+
resolvedAt: new Date().toISOString(),
|
|
405
|
+
},
|
|
406
|
+
this.options.peerTtlSeconds,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
summaries.push({ chatId, chatType, title, username, unreadCount: dialog.unreadCount });
|
|
411
|
+
if (summaries.length >= limit) break;
|
|
412
|
+
}
|
|
413
|
+
return summaries;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Последние `limit` сообщений чата, от старых к новым, ВКЛЮЧАЯ свои — это чтение истории, а не приём.
|
|
418
|
+
* Служебные события (вход в группу, закреп) пропускаются: текста и вложений у них нет.
|
|
419
|
+
*/
|
|
420
|
+
async history(
|
|
421
|
+
recipient: TelegramRecipient,
|
|
422
|
+
options?: { limit?: number; threadId?: number },
|
|
423
|
+
): Promise<IncomingMessage[]> {
|
|
424
|
+
this.assertConnected();
|
|
425
|
+
const peer = await this.getInputPeer(recipient);
|
|
426
|
+
const messages = await this.client.getMessages(peer, {
|
|
427
|
+
limit: options?.limit ?? 30,
|
|
428
|
+
replyTo: options?.threadId,
|
|
429
|
+
});
|
|
430
|
+
return [...messages]
|
|
431
|
+
.reverse()
|
|
432
|
+
.filter((message) => message.className === "Message")
|
|
433
|
+
.map((message) =>
|
|
434
|
+
toIncomingMessage(
|
|
435
|
+
message as unknown as MtprotoMessageLike,
|
|
436
|
+
this.id,
|
|
437
|
+
this.options.accountId,
|
|
438
|
+
),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
351
442
|
private async sendWithRetry(message: OutboundMessage, retried: boolean): Promise<SendResult> {
|
|
352
443
|
try {
|
|
353
444
|
const peer = await this.getInputPeer(message.recipient);
|