@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imessaging/telegram-mtproto",
3
- "version": "0.7.2",
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.2",
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?: { replyToMsgId?: number | string };
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?: { username?: string; accessHash?: { toString(): string } | string | number };
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
- message.replyTo?.replyToMsgId === undefined
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
@@ -1,5 +1,6 @@
1
1
  export {
2
2
  TelegramMtprotoTransport,
3
+ type TelegramChatSummary,
3
4
  type TelegramMtprotoTransportOptions,
4
5
  } from "./telegram-mtproto-transport";
5
6
  export {
@@ -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);