@imessaging/telegram-mtproto 0.7.2 → 0.7.3

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.3",
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.3",
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,13 @@ 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
+ };
43
56
  /**
44
57
  * Пир отправителя ВМЕСТЕ с `accessHash` — так его отдаёт само обновление.
45
58
  *
@@ -231,6 +244,26 @@ export function toIncomingMessage(
231
244
  ? chatId
232
245
  : null;
233
246
 
247
+ let senderName: string | undefined;
248
+ if (message.sender) {
249
+ const parts = [message.sender.firstName, message.sender.lastName].filter(Boolean) as string[];
250
+ if (parts.length > 0) senderName = parts.join(" ").trim();
251
+ else if (message.sender.title) senderName = message.sender.title;
252
+ }
253
+
254
+ let threadId: number | undefined;
255
+ let replyToMessageId: string | undefined;
256
+ if (message.replyTo?.forumTopic === true) {
257
+ threadId = Number(message.replyTo.replyToTopId ?? message.replyTo.replyToMsgId);
258
+ replyToMessageId =
259
+ message.replyTo.replyToTopId === undefined ? undefined : String(message.replyTo.replyToMsgId);
260
+ } else {
261
+ replyToMessageId =
262
+ message.replyTo?.replyToMsgId === undefined
263
+ ? undefined
264
+ : String(message.replyTo.replyToMsgId);
265
+ }
266
+
234
267
  return {
235
268
  transportId,
236
269
  accountId,
@@ -240,14 +273,16 @@ export function toIncomingMessage(
240
273
  // Имя отправителя: по нему отвечают, когда номер клиенту неизвестен, и по нему же продукты
241
274
  // связывают человека со своей учётной записью.
242
275
  senderUsername: message.sender?.username,
276
+ senderName,
243
277
  messageId: String(message.id),
278
+ albumId: message.groupedId?.toString(),
244
279
  text: message.message ?? "",
280
+ sentAt: message.date !== undefined ? new Date(message.date * 1000) : undefined,
281
+ editedAt: message.editDate !== undefined ? new Date(message.editDate * 1000) : undefined,
245
282
  attachments: collectAttachments(message, chatId),
246
283
  links: collectLinks(message),
247
- replyToMessageId:
248
- message.replyTo?.replyToMsgId === undefined
249
- ? undefined
250
- : String(message.replyTo.replyToMsgId),
284
+ replyToMessageId,
285
+ threadId,
251
286
  forwardedFrom: toForwardedFrom(message.fwdFrom),
252
287
  };
253
288
  }
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);