@imessaging/telegram-mtproto 0.8.12 → 0.8.14

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.8.12",
3
+ "version": "0.8.14",
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.8.10",
34
+ "@imessaging/core": "^0.8.12",
35
35
  "big-integer": "^1.6.52"
36
36
  },
37
37
  "peerDependencies": {
package/src/incoming.ts CHANGED
@@ -60,6 +60,8 @@ export type MtprotoMessageLike = {
60
60
  lastName?: string;
61
61
  title?: string;
62
62
  accessHash?: { toString(): string } | string | number;
63
+ /** Есть только у пользователя (Api.User). */
64
+ mutualContact?: boolean;
63
65
  };
64
66
  /** Сущность САМОГО чата из обновления: канал, группа или собеседник. Может не прийти. */
65
67
  chat?: {
@@ -325,6 +327,8 @@ export function toIncomingMessage(
325
327
  // связывают человека со своей учётной записью.
326
328
  senderUsername: message.sender?.username,
327
329
  senderName,
330
+ senderMutualContact:
331
+ typeof message.sender?.mutualContact === "boolean" ? message.sender.mutualContact : undefined,
328
332
  chatTitle,
329
333
  chatUsername: message.chat?.username ?? undefined,
330
334
  messageId: String(message.id),
@@ -11,6 +11,8 @@ import type {
11
11
  OutgoingObserverTransport,
12
12
  OutgoingReadReceipt,
13
13
  OutgoingReadReceiptTransport,
14
+ PeerInfo,
15
+ PeerInfoTransport,
14
16
  ButtonPress,
15
17
  ButtonPressTransport,
16
18
  IncomingAttachment,
@@ -27,6 +29,8 @@ import type {
27
29
  OutboundMessage,
28
30
  ReactionTransport,
29
31
  ResolvedPeer,
32
+ SelfAccount,
33
+ SelfAwareTransport,
30
34
  SendResult,
31
35
  TelegramPeerStore,
32
36
  TelegramRecipient,
@@ -108,6 +112,18 @@ const OWN_MEMORY_MS = 10 * 60 * 1000;
108
112
  /** Номер не занят Telegram или не принят в этой форме — «не найдено», а не отказ. */
109
113
  const PHONE_NOT_FOUND_ERRORS = new Set(["PHONE_NOT_OCCUPIED", "PHONE_NUMBER_INVALID"]);
110
114
 
115
+ /** @username не занят или записан неверно — «не найдено», а не отказ. */
116
+ const USERNAME_NOT_FOUND_ERRORS = new Set(["USERNAME_NOT_OCCUPIED", "USERNAME_INVALID"]);
117
+
118
+ function toPeerInfo(user: Api.User): PeerInfo {
119
+ return {
120
+ id: user.id.toString(),
121
+ username: user.username ?? undefined,
122
+ contact: Boolean(user.contact),
123
+ mutualContact: Boolean(user.mutualContact),
124
+ };
125
+ }
126
+
111
127
  /** Номер чата входного пира в том же маркированном виде, что chatId во входящем (peerToChat). */
112
128
  function inputPeerChatId(peer: Api.TypeInputPeer): string {
113
129
  if (peer instanceof Api.InputPeerChannel) return peerToChat({ channelId: peer.channelId }).chatId;
@@ -146,8 +162,10 @@ export class TelegramMtprotoTransport
146
162
  IncomingMessageTransport,
147
163
  OutgoingObserverTransport,
148
164
  OutgoingReadReceiptTransport,
165
+ PeerInfoTransport,
149
166
  PhoneLookupTransport,
150
167
  ReactionTransport,
168
+ SelfAwareTransport,
151
169
  ReadReceiptTransport,
152
170
  TypingTransport
153
171
  {
@@ -176,6 +194,8 @@ export class TelegramMtprotoTransport
176
194
  private readonly ownEdited = new Map<string, number>();
177
195
  /** Идущие send/edit: обновление о своём сообщении может прийти раньше, чем вызов вернёт его номер. */
178
196
  private readonly ownPending = new Set<Promise<unknown>>();
197
+ /** Своя учётка: не меняется за жизнь сессии, спрашивается один раз. */
198
+ private selfAccount?: Promise<SelfAccount>;
179
199
  /** Отметка жизни подписки: срабатывает на любом обновлении, включая служебные. */
180
200
  private activityListener: ((event: unknown) => void) | null = null;
181
201
 
@@ -718,6 +738,80 @@ export class TelegramMtprotoTransport
718
738
  this.lastActivityAt = new Date();
719
739
  }
720
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
+
761
+ /**
762
+ * Контакт ли собеседник и взаимно ли. Пир берётся из хранилища и кэша клиента, иначе по @username;
763
+ * по телефону не ищет — это findByPhone.
764
+ */
765
+ async peerInfo(recipient: MessageRecipient): Promise<PeerInfo | null> {
766
+ this.assertConnected();
767
+ if (recipient.type !== "user" && recipient.type !== "username") return null;
768
+ const cached = await this.peerStore.get(this.options.accountId, recipient);
769
+ if (cached && cached.type !== "user") return null;
770
+ const peer = cached ? this.toInputPeer(cached) : await this.knownInputPeer(recipient);
771
+ if (peer instanceof Api.InputPeerUser) {
772
+ const [user] = await this.client.invoke(
773
+ new Api.users.GetUsers({
774
+ id: [new Api.InputUser({ userId: peer.userId, accessHash: peer.accessHash })],
775
+ }),
776
+ );
777
+ return user && isUser(user) ? toPeerInfo(user) : null;
778
+ }
779
+ if (peer) return null;
780
+
781
+ const username = recipient.username?.replace(/^@/, "");
782
+ if (!username) return null;
783
+ let result: Api.contacts.ResolvedPeer;
784
+ try {
785
+ result = await this.client.invoke(new Api.contacts.ResolveUsername({ username }));
786
+ } catch (error: unknown) {
787
+ if (USERNAME_NOT_FOUND_ERRORS.has((error as TelegramRpcError).errorMessage ?? ""))
788
+ return null;
789
+ throw error;
790
+ }
791
+ const resolved = result.peer;
792
+ if (!isPeerUser(resolved)) return null;
793
+ const user = result.users.find(
794
+ (candidate): candidate is Api.User =>
795
+ isUser(candidate) && candidate.id.equals(resolved.userId),
796
+ );
797
+ if (!user) return null;
798
+ if (user.accessHash) {
799
+ await this.peerStore.set(
800
+ this.options.accountId,
801
+ recipient,
802
+ {
803
+ type: "user",
804
+ id: user.id.toString(),
805
+ accessHash: user.accessHash.toString(),
806
+ username,
807
+ resolvedAt: new Date().toISOString(),
808
+ },
809
+ this.options.peerTtlSeconds,
810
+ );
811
+ }
812
+ return toPeerInfo(user);
813
+ }
814
+
721
815
  /** Меню команд бота. Сессия обычной учётки получит отказ Telegram: меню бывает только у бота. */
722
816
  async setCommands(commands: ChannelCommand[]): Promise<void> {
723
817
  this.assertConnected();
@@ -907,10 +1001,13 @@ export class TelegramMtprotoTransport
907
1001
  this.rememberOwn(this.ownSent, peer, file.id);
908
1002
  }
909
1003
  this.lastActivityAt = new Date();
1004
+ // Получатель есть, только если ответ принёс users: короткий ответ лички (UpdateShortSentMessage) их не несёт.
1005
+ const chat = sent.chat;
910
1006
  return {
911
1007
  transportId: this.id,
912
1008
  messageId: String(sent.id),
913
1009
  recipientId: this.getPeerId(peer),
1010
+ ...(chat instanceof Api.User ? { mutualContact: Boolean(chat.mutualContact) } : {}),
914
1011
  };
915
1012
  } catch (error: unknown) {
916
1013
  if (retried || !this.isInvalidPeerError(error)) throw error;