@imessaging/telegram-mtproto 0.8.11 → 0.8.13

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.11",
3
+ "version": "0.8.13",
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.9",
34
+ "@imessaging/core": "^0.8.11",
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),
@@ -1,4 +1,6 @@
1
1
  import type {
2
+ ChannelCommand,
3
+ CommandMenuTransport,
2
4
  ContactCard,
3
5
  ContactTransport,
4
6
  PhoneLookupOptions,
@@ -9,6 +11,8 @@ import type {
9
11
  OutgoingObserverTransport,
10
12
  OutgoingReadReceipt,
11
13
  OutgoingReadReceiptTransport,
14
+ PeerInfo,
15
+ PeerInfoTransport,
12
16
  ButtonPress,
13
17
  ButtonPressTransport,
14
18
  IncomingAttachment,
@@ -106,6 +110,18 @@ const OWN_MEMORY_MS = 10 * 60 * 1000;
106
110
  /** Номер не занят Telegram или не принят в этой форме — «не найдено», а не отказ. */
107
111
  const PHONE_NOT_FOUND_ERRORS = new Set(["PHONE_NOT_OCCUPIED", "PHONE_NUMBER_INVALID"]);
108
112
 
113
+ /** @username не занят или записан неверно — «не найдено», а не отказ. */
114
+ const USERNAME_NOT_FOUND_ERRORS = new Set(["USERNAME_NOT_OCCUPIED", "USERNAME_INVALID"]);
115
+
116
+ function toPeerInfo(user: Api.User): PeerInfo {
117
+ return {
118
+ id: user.id.toString(),
119
+ username: user.username ?? undefined,
120
+ contact: Boolean(user.contact),
121
+ mutualContact: Boolean(user.mutualContact),
122
+ };
123
+ }
124
+
109
125
  /** Номер чата входного пира в том же маркированном виде, что chatId во входящем (peerToChat). */
110
126
  function inputPeerChatId(peer: Api.TypeInputPeer): string {
111
127
  if (peer instanceof Api.InputPeerChannel) return peerToChat({ channelId: peer.channelId }).chatId;
@@ -114,6 +130,14 @@ function inputPeerChatId(peer: Api.TypeInputPeer): string {
114
130
  throw new Error(`Unsupported input peer: ${peer.className}`);
115
131
  }
116
132
 
133
+ /** Разметка отправки: кнопки, а без них — снятие постоянной клавиатуры по `removeKeyboard`. */
134
+ function sendMarkup(message: OutboundMessage) {
135
+ return (
136
+ toGramJsButtons(message.buttons) ??
137
+ (message.removeKeyboard ? new Api.ReplyKeyboardHide({}) : undefined)
138
+ );
139
+ }
140
+
117
141
  function toGramJsButtons(buttons?: OutboundButton[][]) {
118
142
  if (!buttons || buttons.length === 0) return undefined;
119
143
  return buttons.map((row) =>
@@ -127,6 +151,7 @@ function toGramJsButtons(buttons?: OutboundButton[][]) {
127
151
 
128
152
  export class TelegramMtprotoTransport
129
153
  implements
154
+ CommandMenuTransport,
130
155
  ContactTransport,
131
156
  DraftTransport,
132
157
  EditableMessageTransport,
@@ -135,6 +160,7 @@ export class TelegramMtprotoTransport
135
160
  IncomingMessageTransport,
136
161
  OutgoingObserverTransport,
137
162
  OutgoingReadReceiptTransport,
163
+ PeerInfoTransport,
138
164
  PhoneLookupTransport,
139
165
  ReactionTransport,
140
166
  ReadReceiptTransport,
@@ -707,6 +733,74 @@ export class TelegramMtprotoTransport
707
733
  this.lastActivityAt = new Date();
708
734
  }
709
735
 
736
+ /**
737
+ * Контакт ли собеседник и взаимно ли. Пир берётся из хранилища и кэша клиента, иначе по @username;
738
+ * по телефону не ищет — это findByPhone.
739
+ */
740
+ async peerInfo(recipient: MessageRecipient): Promise<PeerInfo | null> {
741
+ this.assertConnected();
742
+ if (recipient.type !== "user" && recipient.type !== "username") return null;
743
+ const cached = await this.peerStore.get(this.options.accountId, recipient);
744
+ if (cached && cached.type !== "user") return null;
745
+ const peer = cached ? this.toInputPeer(cached) : await this.knownInputPeer(recipient);
746
+ if (peer instanceof Api.InputPeerUser) {
747
+ const [user] = await this.client.invoke(
748
+ new Api.users.GetUsers({
749
+ id: [new Api.InputUser({ userId: peer.userId, accessHash: peer.accessHash })],
750
+ }),
751
+ );
752
+ return user && isUser(user) ? toPeerInfo(user) : null;
753
+ }
754
+ if (peer) return null;
755
+
756
+ const username = recipient.username?.replace(/^@/, "");
757
+ if (!username) return null;
758
+ let result: Api.contacts.ResolvedPeer;
759
+ try {
760
+ result = await this.client.invoke(new Api.contacts.ResolveUsername({ username }));
761
+ } catch (error: unknown) {
762
+ if (USERNAME_NOT_FOUND_ERRORS.has((error as TelegramRpcError).errorMessage ?? ""))
763
+ return null;
764
+ throw error;
765
+ }
766
+ const resolved = result.peer;
767
+ if (!isPeerUser(resolved)) return null;
768
+ const user = result.users.find(
769
+ (candidate): candidate is Api.User =>
770
+ isUser(candidate) && candidate.id.equals(resolved.userId),
771
+ );
772
+ if (!user) return null;
773
+ if (user.accessHash) {
774
+ await this.peerStore.set(
775
+ this.options.accountId,
776
+ recipient,
777
+ {
778
+ type: "user",
779
+ id: user.id.toString(),
780
+ accessHash: user.accessHash.toString(),
781
+ username,
782
+ resolvedAt: new Date().toISOString(),
783
+ },
784
+ this.options.peerTtlSeconds,
785
+ );
786
+ }
787
+ return toPeerInfo(user);
788
+ }
789
+
790
+ /** Меню команд бота. Сессия обычной учётки получит отказ Telegram: меню бывает только у бота. */
791
+ async setCommands(commands: ChannelCommand[]): Promise<void> {
792
+ this.assertConnected();
793
+ await this.client.invoke(
794
+ new Api.bots.SetBotCommands({
795
+ scope: new Api.BotCommandScopeDefault(),
796
+ langCode: "",
797
+ commands: commands.map(
798
+ (c) => new Api.BotCommand({ command: c.command, description: c.description }),
799
+ ),
800
+ }),
801
+ );
802
+ }
803
+
710
804
  async disconnect(): Promise<void> {
711
805
  await this.client.disconnect();
712
806
  this.connected = false;
@@ -869,7 +963,7 @@ export class TelegramMtprotoTransport
869
963
  parseMode,
870
964
  formattingEntities,
871
965
  replyTo: message.replyToMessageId ?? message.threadId,
872
- buttons: toGramJsButtons(message.buttons),
966
+ buttons: sendMarkup(message),
873
967
  });
874
968
  this.rememberOwn(this.ownSent, peer, sent.id);
875
969
  for (const document of message.documents ?? []) {
@@ -882,10 +976,13 @@ export class TelegramMtprotoTransport
882
976
  this.rememberOwn(this.ownSent, peer, file.id);
883
977
  }
884
978
  this.lastActivityAt = new Date();
979
+ // Получатель есть, только если ответ принёс users: короткий ответ лички (UpdateShortSentMessage) их не несёт.
980
+ const chat = sent.chat;
885
981
  return {
886
982
  transportId: this.id,
887
983
  messageId: String(sent.id),
888
984
  recipientId: this.getPeerId(peer),
985
+ ...(chat instanceof Api.User ? { mutualContact: Boolean(chat.mutualContact) } : {}),
889
986
  };
890
987
  } catch (error: unknown) {
891
988
  if (retried || !this.isInvalidPeerError(error)) throw error;