@mrlegendbot/baileys 1.2.3 → 1.2.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.
Files changed (212) hide show
  1. package/README.upstream.md +1312 -0
  2. package/WAProto/WAProto.json +1 -0
  3. package/WAProto/WAProto.proto +6275 -0
  4. package/WAProto/compiler.js +550 -0
  5. package/WAProto/index.js +2 -97691
  6. package/lib/Defaults/index.js +89 -92
  7. package/lib/Signal/Group/ciphertext-message.js +9 -10
  8. package/lib/Signal/Group/group-session-builder.js +21 -22
  9. package/lib/Signal/Group/group_cipher.js +75 -69
  10. package/lib/Signal/Group/index.js +0 -1
  11. package/lib/Signal/Group/keyhelper.js +9 -10
  12. package/lib/Signal/Group/sender-chain-key.js +21 -22
  13. package/lib/Signal/Group/sender-key-distribution-message.js +51 -60
  14. package/lib/Signal/Group/sender-key-message.js +56 -62
  15. package/lib/Signal/Group/sender-key-name.js +38 -40
  16. package/lib/Signal/Group/sender-key-record.js +33 -34
  17. package/lib/Signal/Group/sender-key-state.js +72 -76
  18. package/lib/Signal/Group/sender-message-key.js +22 -23
  19. package/lib/Signal/libsignal.js +506 -405
  20. package/lib/Signal/lid-mapping.js +276 -258
  21. package/lib/Socket/Client/index.js +0 -1
  22. package/lib/Socket/Client/types.js +6 -7
  23. package/lib/Socket/Client/websocket.js +48 -49
  24. package/lib/Socket/business.js +349 -369
  25. package/lib/Socket/chats.js +1172 -1148
  26. package/lib/Socket/communities.js +473 -413
  27. package/lib/Socket/groups.js +387 -331
  28. package/lib/Socket/index.js +6 -8
  29. package/lib/Socket/messages-recv.js +1782 -1719
  30. package/lib/Socket/messages-send.js +1503 -2450
  31. package/lib/Socket/mex.js +40 -35
  32. package/lib/Socket/newsletter.js +239 -191
  33. package/lib/Socket/socket.js +992 -923
  34. package/lib/Store/index.js +8 -0
  35. package/lib/Store/keyed-db.js +108 -0
  36. package/lib/Store/make-cache-manager-store.js +70 -0
  37. package/lib/Store/make-in-memory-store.js +417 -0
  38. package/lib/Store/make-ordered-dictionary.js +69 -0
  39. package/lib/Store/object-repository.js +27 -0
  40. package/lib/Types/Auth.js +0 -1
  41. package/lib/Types/Bussines.js +0 -1
  42. package/lib/Types/Call.js +0 -1
  43. package/lib/Types/Chat.js +1 -8
  44. package/lib/Types/Contact.js +0 -1
  45. package/lib/Types/Events.js +1 -2
  46. package/lib/Types/GroupMetadata.js +0 -1
  47. package/lib/Types/Label.js +20 -22
  48. package/lib/Types/LabelAssociation.js +2 -4
  49. package/lib/Types/Message.js +3 -5
  50. package/lib/Types/Mex.js +26 -31
  51. package/lib/Types/Product.js +0 -1
  52. package/lib/Types/Signal.js +1 -2
  53. package/lib/Types/Socket.js +2 -3
  54. package/lib/Types/State.js +4 -52
  55. package/lib/Types/USync.js +0 -1
  56. package/lib/Types/index.js +10 -11
  57. package/lib/Utils/auth-utils.js +240 -277
  58. package/lib/Utils/browser-utils.js +20 -23
  59. package/lib/Utils/business.js +200 -204
  60. package/lib/Utils/chat-utils.js +836 -810
  61. package/lib/Utils/companion-reg-client-utils.js +23 -25
  62. package/lib/Utils/crypto.js +70 -86
  63. package/lib/Utils/decode-wa-message.js +277 -284
  64. package/lib/Utils/event-buffer.js +592 -595
  65. package/lib/Utils/generics.js +312 -335
  66. package/lib/Utils/history.js +123 -115
  67. package/lib/Utils/index.js +1 -2
  68. package/lib/Utils/link-preview.js +74 -73
  69. package/lib/Utils/logger.js +3 -2
  70. package/lib/Utils/lt-hash.js +0 -6
  71. package/lib/Utils/make-mutex.js +27 -27
  72. package/lib/Utils/message-retry-manager.js +213 -254
  73. package/lib/Utils/messages-media.js +741 -715
  74. package/lib/Utils/messages.js +791 -827
  75. package/lib/Utils/noise-handler.js +215 -187
  76. package/lib/Utils/pre-key-manager.js +69 -98
  77. package/lib/Utils/process-message.js +617 -586
  78. package/lib/Utils/reporting-utils.js +553 -234
  79. package/lib/Utils/signal.js +217 -170
  80. package/lib/Utils/stanza-ack.js +27 -35
  81. package/lib/Utils/sync-action-utils.js +37 -41
  82. package/lib/Utils/tc-token-utils.js +120 -141
  83. package/lib/Utils/use-multi-file-auth-state.js +87 -105
  84. package/lib/Utils/use-sqlite-auth-state.js +162 -0
  85. package/lib/Utils/validate-connection.js +190 -188
  86. package/lib/WABinary/constants.js +28 -1294
  87. package/lib/WABinary/decode.js +245 -249
  88. package/lib/WABinary/encode.js +201 -206
  89. package/lib/WABinary/generic-utils.js +86 -89
  90. package/lib/WABinary/index.js +0 -1
  91. package/lib/WABinary/jid-utils.js +68 -77
  92. package/lib/WABinary/types.js +0 -1
  93. package/lib/WAM/BinaryInfo.js +7 -8
  94. package/lib/WAM/constants.js +19892 -22853
  95. package/lib/WAM/encode.js +121 -132
  96. package/lib/WAM/index.js +0 -1
  97. package/lib/WAUSync/Protocols/USyncContactProtocol.js +46 -43
  98. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +47 -48
  99. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +22 -23
  100. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +30 -33
  101. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +19 -20
  102. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +53 -44
  103. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +24 -24
  104. package/lib/WAUSync/Protocols/index.js +0 -1
  105. package/lib/WAUSync/USyncQuery.js +79 -88
  106. package/lib/WAUSync/USyncUser.js +28 -29
  107. package/lib/WAUSync/index.js +0 -1
  108. package/lib/index.js +2 -2
  109. package/package.json +22 -86
  110. package/WAProto/GenerateStatics.sh +0 -3
  111. package/WAProto/fix-imports.js +0 -85
  112. package/WAProto/index.d.ts +0 -14017
  113. package/lib/Defaults/index.d.ts +0 -75
  114. package/lib/Signal/Group/ciphertext-message.d.ts +0 -10
  115. package/lib/Signal/Group/group-session-builder.d.ts +0 -15
  116. package/lib/Signal/Group/group_cipher.d.ts +0 -17
  117. package/lib/Signal/Group/index.d.ts +0 -12
  118. package/lib/Signal/Group/keyhelper.d.ts +0 -11
  119. package/lib/Signal/Group/sender-chain-key.d.ts +0 -14
  120. package/lib/Signal/Group/sender-key-distribution-message.d.ts +0 -17
  121. package/lib/Signal/Group/sender-key-message.d.ts +0 -19
  122. package/lib/Signal/Group/sender-key-name.d.ts +0 -18
  123. package/lib/Signal/Group/sender-key-record.d.ts +0 -31
  124. package/lib/Signal/Group/sender-key-state.d.ts +0 -39
  125. package/lib/Signal/Group/sender-message-key.d.ts +0 -12
  126. package/lib/Signal/libsignal.d.ts +0 -5
  127. package/lib/Signal/lid-mapping.d.ts +0 -23
  128. package/lib/Socket/Client/index.d.ts +0 -3
  129. package/lib/Socket/Client/types.d.ts +0 -16
  130. package/lib/Socket/Client/websocket.d.ts +0 -13
  131. package/lib/Socket/business.d.ts +0 -217
  132. package/lib/Socket/chats.d.ts +0 -125
  133. package/lib/Socket/communities.d.ts +0 -273
  134. package/lib/Socket/groups.d.ts +0 -161
  135. package/lib/Socket/index.d.ts +0 -260
  136. package/lib/Socket/messages-recv.d.ts +0 -203
  137. package/lib/Socket/messages-send.d.ts +0 -199
  138. package/lib/Socket/mex.d.ts +0 -3
  139. package/lib/Socket/newsletter.d.ts +0 -170
  140. package/lib/Socket/socket.d.ts +0 -59
  141. package/lib/Types/Auth.d.ts +0 -117
  142. package/lib/Types/Bussines.d.ts +0 -25
  143. package/lib/Types/Call.d.ts +0 -15
  144. package/lib/Types/Chat.d.ts +0 -124
  145. package/lib/Types/Contact.d.ts +0 -26
  146. package/lib/Types/Events.d.ts +0 -256
  147. package/lib/Types/GroupMetadata.d.ts +0 -71
  148. package/lib/Types/Label.d.ts +0 -47
  149. package/lib/Types/LabelAssociation.d.ts +0 -30
  150. package/lib/Types/Message.d.ts +0 -320
  151. package/lib/Types/Mex.d.ts +0 -141
  152. package/lib/Types/Product.d.ts +0 -79
  153. package/lib/Types/Signal.d.ts +0 -87
  154. package/lib/Types/Socket.d.ts +0 -136
  155. package/lib/Types/State.d.ts +0 -97
  156. package/lib/Types/USync.d.ts +0 -26
  157. package/lib/Types/index.d.ts +0 -66
  158. package/lib/Utils/auth-utils.d.ts +0 -24
  159. package/lib/Utils/browser-utils.d.ts +0 -4
  160. package/lib/Utils/business.d.ts +0 -23
  161. package/lib/Utils/chat-utils.d.ts +0 -100
  162. package/lib/Utils/companion-reg-client-utils.d.ts +0 -17
  163. package/lib/Utils/crypto.d.ts +0 -37
  164. package/lib/Utils/decode-wa-message.d.ts +0 -66
  165. package/lib/Utils/event-buffer.d.ts +0 -36
  166. package/lib/Utils/generics.d.ts +0 -92
  167. package/lib/Utils/history.d.ts +0 -24
  168. package/lib/Utils/identity-change-handler.d.ts +0 -44
  169. package/lib/Utils/identity-change-handler.js +0 -50
  170. package/lib/Utils/index.d.ts +0 -22
  171. package/lib/Utils/link-preview.d.ts +0 -21
  172. package/lib/Utils/logger.d.ts +0 -12
  173. package/lib/Utils/lt-hash.d.ts +0 -8
  174. package/lib/Utils/make-mutex.d.ts +0 -9
  175. package/lib/Utils/message-retry-manager.d.ts +0 -115
  176. package/lib/Utils/messages-media.d.ts +0 -133
  177. package/lib/Utils/messages.d.ts +0 -91
  178. package/lib/Utils/noise-handler.d.ts +0 -20
  179. package/lib/Utils/offline-node-processor.d.ts +0 -17
  180. package/lib/Utils/offline-node-processor.js +0 -40
  181. package/lib/Utils/pre-key-manager.d.ts +0 -28
  182. package/lib/Utils/process-message.d.ts +0 -60
  183. package/lib/Utils/reporting-utils.d.ts +0 -11
  184. package/lib/Utils/signal.d.ts +0 -47
  185. package/lib/Utils/stanza-ack.d.ts +0 -11
  186. package/lib/Utils/sync-action-utils.d.ts +0 -19
  187. package/lib/Utils/tc-token-utils.d.ts +0 -37
  188. package/lib/Utils/use-multi-file-auth-state.d.ts +0 -13
  189. package/lib/Utils/validate-connection.d.ts +0 -11
  190. package/lib/WABinary/constants.d.ts +0 -28
  191. package/lib/WABinary/decode.d.ts +0 -7
  192. package/lib/WABinary/encode.d.ts +0 -3
  193. package/lib/WABinary/generic-utils.d.ts +0 -15
  194. package/lib/WABinary/index.d.ts +0 -6
  195. package/lib/WABinary/jid-utils.d.ts +0 -48
  196. package/lib/WABinary/types.d.ts +0 -19
  197. package/lib/WAM/BinaryInfo.d.ts +0 -9
  198. package/lib/WAM/constants.d.ts +0 -40
  199. package/lib/WAM/encode.d.ts +0 -3
  200. package/lib/WAM/index.d.ts +0 -4
  201. package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts +0 -10
  202. package/lib/WAUSync/Protocols/USyncDeviceProtocol.d.ts +0 -23
  203. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +0 -13
  204. package/lib/WAUSync/Protocols/USyncStatusProtocol.d.ts +0 -13
  205. package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts +0 -10
  206. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.d.ts +0 -26
  207. package/lib/WAUSync/Protocols/UsyncLIDProtocol.d.ts +0 -10
  208. package/lib/WAUSync/Protocols/index.d.ts +0 -6
  209. package/lib/WAUSync/USyncQuery.d.ts +0 -30
  210. package/lib/WAUSync/USyncUser.d.ts +0 -17
  211. package/lib/WAUSync/index.d.ts +0 -4
  212. package/lib/index.d.ts +0 -12
@@ -2,9 +2,8 @@ import { Boom } from '@hapi/boom';
2
2
  import { randomBytes } from 'crypto';
3
3
  import { URL } from 'url';
4
4
  import { promisify } from 'util';
5
- import { proto } from '../../WAProto/index.js';
5
+ import { proto } from '../../WAProto/compiler.js';
6
6
  import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, NOISE_WA_HEADER, PROCESSABLE_HISTORY_TYPES, TimeMs, UPLOAD_TIMEOUT } from '../Defaults/index.js';
7
- import { QueryIds, ReachoutTimelockEnforcementType } from '../Types/index.js';
8
7
  import { DisconnectReason, XWAPaths } from '../Types/index.js';
9
8
  import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, buildPairingQRData, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getCompanionPlatformId, getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js';
10
9
  import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js';
@@ -12,959 +11,1029 @@ import { BinaryInfo } from '../WAM/BinaryInfo.js';
12
11
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
13
12
  import { WebSocketClient } from './Client/index.js';
14
13
  import { executeWMexQuery } from './mex.js';
15
- /**
16
- * Connects to WA servers and performs:
17
- * - simple queries (no retry mechanism, wait for connection establishment)
18
- * - listen to messages and emit events
19
- * - query phone connection
20
- */
21
- export const makeSocket = (config) => {
22
- const { waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts, qrTimeout, makeSignalRepository } = config;
23
- const publicWAMBuffer = new BinaryInfo();
24
- let serverTimeOffsetMs = 0;
25
- const uqTagId = generateMdTagPrefix();
26
- const generateMessageTag = () => `${uqTagId}${epoch++}`;
27
- if (printQRInTerminal) {
28
- logger.warn({}, '⚠️ The printQRInTerminal option has been deprecated. You will no longer receive QR codes in the terminal automatically. Please listen to the connection.update event yourself and handle the QR your way. You can remove this message by removing this opttion. This message will be removed in a future version.');
29
- }
30
- if (browser[1].toLocaleLowerCase().includes('android')) {
31
- logger.warn('⚠️ Using the Android browser is experimental and may lead to unexpected behavior. Use at your own risk.');
32
- }
33
- const syncDisabled = PROCESSABLE_HISTORY_TYPES.map(syncType => config.shouldSyncHistoryMessage({ syncType })).filter(x => x === false)
34
- .length === PROCESSABLE_HISTORY_TYPES.length;
35
- if (syncDisabled) {
36
- logger.warn('⚠️ DANGER: DISABLING ALL SYNC BY shouldSyncHistoryMsg PREVENTS BAILEYS FROM ACCESSING INITIAL LID MAPPINGS, LEADING TO INSTABILIY AND SESSION ERRORS');
37
- }
38
- const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl;
39
- if (config.mobile || url.protocol === 'tcp:') {
40
- throw new Boom('Mobile API is not supported anymore', { statusCode: DisconnectReason.loggedOut });
41
- }
42
- if (url.protocol === 'wss' && authState?.creds?.routingInfo) {
43
- url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'));
44
- }
45
- /** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
46
- const ephemeralKeyPair = Curve.generateKeyPair();
47
- /** WA noise protocol wrapper */
48
- const noise = makeNoiseHandler({
49
- keyPair: ephemeralKeyPair,
50
- NOISE_HEADER: NOISE_WA_HEADER,
51
- logger,
52
- routingInfo: authState?.creds?.routingInfo
14
+ export const makeSocket = config => {
15
+ const {
16
+ waWebSocketUrl,
17
+ connectTimeoutMs,
18
+ logger,
19
+ keepAliveIntervalMs,
20
+ browser,
21
+ auth: authState,
22
+ printQRInTerminal,
23
+ defaultQueryTimeoutMs,
24
+ transactionOpts,
25
+ qrTimeout,
26
+ makeSignalRepository
27
+ } = config;
28
+ const publicWAMBuffer = new BinaryInfo();
29
+ let serverTimeOffsetMs = 0;
30
+ const uqTagId = generateMdTagPrefix();
31
+ const generateMessageTag = () => `${uqTagId}${epoch++}`;
32
+ if (printQRInTerminal) {
33
+ logger.warn({}, '⚠️ The printQRInTerminal option has been deprecated. You will no longer receive QR codes in the terminal automatically. Please listen to the connection.update event yourself and handle the QR your way. You can remove this message by removing this opttion. This message will be removed in a future version.');
34
+ }
35
+ const syncDisabled = PROCESSABLE_HISTORY_TYPES.map(syncType => config.shouldSyncHistoryMessage({
36
+ syncType
37
+ })).filter(x => x === false).length === PROCESSABLE_HISTORY_TYPES.length;
38
+ if (syncDisabled) {
39
+ logger.warn('⚠️ DANGER: DISABLING ALL SYNC BY shouldSyncHistoryMsg PREVENTS BAILEYS FROM ACCESSING INITIAL LID MAPPINGS, LEADING TO INSTABILIY AND SESSION ERRORS');
40
+ }
41
+ const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl;
42
+ if (config.mobile || url.protocol === 'tcp:') {
43
+ throw new Boom('Mobile API is not supported anymore', {
44
+ statusCode: DisconnectReason.loggedOut
53
45
  });
54
- const ws = new WebSocketClient(url, config);
55
- ws.connect();
56
- const sendPromise = promisify(ws.send);
57
- /** send a raw buffer */
58
- const sendRawMessage = async (data) => {
59
- if (!ws.isOpen) {
60
- throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
61
- }
62
- const bytes = noise.encodeFrame(data);
63
- await promiseTimeout(connectTimeoutMs, async (resolve, reject) => {
64
- try {
65
- await sendPromise.call(ws, bytes);
66
- resolve();
67
- }
68
- catch (error) {
69
- reject(error);
70
- }
71
- });
72
- };
73
- /** send a binary node */
74
- const sendNode = (frame) => {
75
- if (logger.level === 'trace') {
76
- logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' });
77
- }
78
- const buff = encodeBinaryNode(frame);
79
- return sendRawMessage(buff);
80
- };
81
- /**
82
- * Wait for a message with a certain tag to be received
83
- * @param msgId the message tag to await
84
- * @param timeoutMs timeout after which the promise will reject
85
- */
86
- const waitForMessage = async (msgId, timeoutMs = defaultQueryTimeoutMs) => {
87
- let onRecv;
88
- let onErr;
89
- try {
90
- const result = await promiseTimeout(timeoutMs, (resolve, reject) => {
91
- onRecv = data => {
92
- resolve(data);
93
- };
94
- onErr = err => {
95
- reject(err ||
96
- new Boom('Connection Closed', {
97
- statusCode: DisconnectReason.connectionClosed
98
- }));
99
- };
100
- ws.on(`TAG:${msgId}`, onRecv);
101
- ws.on('close', onErr);
102
- ws.on('error', onErr);
103
- return () => reject(new Boom('Query Cancelled'));
104
- });
105
- return result;
106
- }
107
- catch (error) {
108
- // Catch timeout and return undefined instead of throwing
109
- if (error instanceof Boom && error.output?.statusCode === DisconnectReason.timedOut) {
110
- logger?.warn?.({ msgId }, 'timed out waiting for message');
111
- return undefined;
112
- }
113
- throw error;
114
- }
115
- finally {
116
- if (onRecv)
117
- ws.off(`TAG:${msgId}`, onRecv);
118
- if (onErr) {
119
- ws.off('close', onErr);
120
- ws.off('error', onErr);
121
- }
122
- }
123
- };
124
- /** send a query, and wait for its response. auto-generates message ID if not provided */
125
- const query = async (node, timeoutMs) => {
126
- if (!node.attrs.id) {
127
- node.attrs.id = generateMessageTag();
128
- }
129
- const msgId = node.attrs.id;
130
- const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
131
- const result = waitForMessage(msgId, timeoutMs).catch(reject);
132
- sendNode(node)
133
- .then(async () => resolve(await result))
134
- .catch(reject);
135
- });
136
- if (result && 'tag' in result) {
137
- assertNodeErrorFree(result);
138
- }
139
- return result;
140
- };
141
- // Validate current key-bundle on server; on failure, trigger pre-key upload and rethrow
142
- const digestKeyBundle = async () => {
143
- const res = await query({
144
- tag: 'iq',
145
- attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'encrypt' },
146
- content: [{ tag: 'digest', attrs: {} }]
147
- });
148
- const digestNode = getBinaryNodeChild(res, 'digest');
149
- if (!digestNode) {
150
- await uploadPreKeys();
151
- throw new Error('encrypt/get digest returned no digest node');
152
- }
153
- };
154
- // Rotate our signed pre-key on server; on failure, run digest as fallback and rethrow
155
- const rotateSignedPreKey = async () => {
156
- const newId = (creds.signedPreKey.keyId || 0) + 1;
157
- const skey = await signedKeyPair(creds.signedIdentityKey, newId);
158
- await query({
159
- tag: 'iq',
160
- attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'encrypt' },
161
- content: [
162
- {
163
- tag: 'rotate',
164
- attrs: {},
165
- content: [xmppSignedPreKey(skey)]
166
- }
167
- ]
168
- });
169
- // Persist new signed pre-key in creds
170
- ev.emit('creds.update', { signedPreKey: skey });
171
- };
172
- const executeUSyncQuery = async (usyncQuery) => {
173
- if (usyncQuery.protocols.length === 0) {
174
- throw new Boom('USyncQuery must have at least one protocol');
175
- }
176
- // todo: validate users, throw WARNING on no valid users
177
- // variable below has only validated users
178
- const validUsers = usyncQuery.users;
179
- const userNodes = validUsers.map(user => {
180
- return {
181
- tag: 'user',
182
- attrs: {
183
- jid: !user.phone ? user.id : undefined
184
- },
185
- content: usyncQuery.protocols.map(a => a.getUserElement(user)).filter(a => a !== null)
186
- };
187
- });
188
- const listNode = {
189
- tag: 'list',
190
- attrs: {},
191
- content: userNodes
192
- };
193
- const queryNode = {
194
- tag: 'query',
195
- attrs: {},
196
- content: usyncQuery.protocols.map(a => a.getQueryElement())
197
- };
198
- const iq = {
199
- tag: 'iq',
200
- attrs: {
201
- to: S_WHATSAPP_NET,
202
- type: 'get',
203
- xmlns: 'usync'
204
- },
205
- content: [
206
- {
207
- tag: 'usync',
208
- attrs: {
209
- context: usyncQuery.context,
210
- mode: usyncQuery.mode,
211
- sid: generateMessageTag(),
212
- last: 'true',
213
- index: '0'
214
- },
215
- content: [queryNode, listNode]
216
- }
217
- ]
46
+ }
47
+ if (url.protocol === 'wss' && authState?.creds?.routingInfo) {
48
+ url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'));
49
+ }
50
+ const ephemeralKeyPair = Curve.generateKeyPair();
51
+ const noise = makeNoiseHandler({
52
+ keyPair: ephemeralKeyPair,
53
+ NOISE_HEADER: NOISE_WA_HEADER,
54
+ logger,
55
+ routingInfo: authState?.creds?.routingInfo
56
+ });
57
+ const ws = new WebSocketClient(url, config);
58
+ ws.connect();
59
+ const sendPromise = promisify(ws.send);
60
+ const sendRawMessage = async data => {
61
+ if (!ws.isOpen) {
62
+ throw new Boom('Connection Closed', {
63
+ statusCode: DisconnectReason.connectionClosed
64
+ });
65
+ }
66
+ const bytes = noise.encodeFrame(data);
67
+ await promiseTimeout(connectTimeoutMs, async (resolve, reject) => {
68
+ try {
69
+ await sendPromise.call(ws, bytes);
70
+ resolve();
71
+ } catch (error) {
72
+ reject(error);
73
+ }
74
+ });
75
+ };
76
+ const sendNode = frame => {
77
+ if (logger.level === 'trace') {
78
+ logger.trace({
79
+ xml: binaryNodeToString(frame),
80
+ msg: 'xml send'
81
+ });
82
+ }
83
+ const buff = encodeBinaryNode(frame);
84
+ return sendRawMessage(buff);
85
+ };
86
+ const waitForMessage = async (msgId, timeoutMs = defaultQueryTimeoutMs) => {
87
+ let onRecv;
88
+ let onErr;
89
+ try {
90
+ const result = await promiseTimeout(timeoutMs, (resolve, reject) => {
91
+ onRecv = data => {
92
+ resolve(data);
218
93
  };
219
- const result = await query(iq);
220
- return usyncQuery.parseUSyncQueryResult(result);
221
- };
222
- const onWhatsApp = async (...phoneNumber) => {
223
- let usyncQuery = new USyncQuery();
224
- let contactEnabled = false;
225
- for (const jid of phoneNumber) {
226
- if (isLidUser(jid)) {
227
- logger?.warn('LIDs are not supported with onWhatsApp');
228
- continue;
229
- }
230
- else {
231
- if (!contactEnabled) {
232
- contactEnabled = true;
233
- usyncQuery = usyncQuery.withContactProtocol();
234
- }
235
- const phone = `+${jid.replace('+', '').split('@')[0]?.split(':')[0]}`;
236
- usyncQuery.withUser(new USyncUser().withPhone(phone));
237
- }
238
- }
239
- if (usyncQuery.users.length === 0) {
240
- return []; // return early without forcing an empty query
241
- }
242
- const results = await executeUSyncQuery(usyncQuery);
243
- if (results) {
244
- return results.list.filter(a => !!a.contact).map(({ contact, id }) => ({ jid: id, exists: contact }));
245
- }
246
- };
247
- const pnFromLIDUSync = async (jids) => {
248
- const usyncQuery = new USyncQuery().withLIDProtocol().withContext('background');
249
- for (const jid of jids) {
250
- if (isLidUser(jid)) {
251
- logger?.warn('LID user found in LID fetch call');
252
- continue;
253
- }
254
- else {
255
- usyncQuery.withUser(new USyncUser().withId(jid));
256
- }
257
- }
258
- if (usyncQuery.users.length === 0) {
259
- return []; // return early without forcing an empty query
260
- }
261
- const results = await executeUSyncQuery(usyncQuery);
262
- if (results) {
263
- return results.list.filter(a => !!a.lid).map(({ lid, id }) => ({ pn: id, lid: lid }));
264
- }
265
- return [];
266
- };
267
- const ev = makeEventBuffer(logger);
268
- const { creds } = authState;
269
- // add transaction capability
270
- const keys = addTransactionCapability(authState.keys, logger, transactionOpts);
271
- const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync);
272
- let lastDateRecv;
273
- let epoch = 1;
274
- let keepAliveReq;
275
- let qrTimer;
276
- let closed = false;
277
- const socketEndHandlers = [];
278
- /** log & process any unexpected errors */
279
- const onUnexpectedError = (err, msg) => {
280
- logger.error({ err }, `unexpected error in '${msg}'`);
281
- };
282
- /** await the next incoming message */
283
- const awaitNextMessage = async (sendMsg) => {
284
- if (!ws.isOpen) {
285
- throw new Boom('Connection Closed', {
286
- statusCode: DisconnectReason.connectionClosed
287
- });
288
- }
289
- let onOpen;
290
- let onClose;
291
- const result = promiseTimeout(connectTimeoutMs, (resolve, reject) => {
292
- onOpen = resolve;
293
- onClose = mapWebSocketError(reject);
294
- ws.on('frame', onOpen);
295
- ws.on('close', onClose);
296
- ws.on('error', onClose);
297
- }).finally(() => {
298
- ws.off('frame', onOpen);
299
- ws.off('close', onClose);
300
- ws.off('error', onClose);
301
- });
302
- if (sendMsg) {
303
- sendRawMessage(sendMsg).catch(onClose);
304
- }
305
- return result;
306
- };
307
- /** connection handshake */
308
- const validateConnection = async () => {
309
- let helloMsg = {
310
- clientHello: { ephemeral: ephemeralKeyPair.public }
94
+ onErr = err => {
95
+ reject(err || new Boom('Connection Closed', {
96
+ statusCode: DisconnectReason.connectionClosed
97
+ }));
311
98
  };
312
- helloMsg = proto.HandshakeMessage.fromObject(helloMsg);
313
- logger.info({ browser, helloMsg }, 'connected to WA');
314
- const init = proto.HandshakeMessage.encode(helloMsg).finish();
315
- const result = await awaitNextMessage(init);
316
- const handshake = proto.HandshakeMessage.decode(result);
317
- logger.trace({ handshake }, 'handshake recv from WA');
318
- const keyEnc = noise.processHandshake(handshake, creds.noiseKey);
319
- let node;
320
- if (!creds.me) {
321
- node = generateRegistrationNode(creds, config);
322
- logger.info({ node }, 'not logged in, attempting registration...');
323
- }
324
- else {
325
- node = generateLoginNode(creds.me.id, config);
326
- logger.info({ node }, 'logging in...');
327
- }
328
- const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish());
329
- await sendRawMessage(proto.HandshakeMessage.encode({
330
- clientFinish: {
331
- static: keyEnc,
332
- payload: payloadEnc
333
- }
334
- }).finish());
335
- await noise.finishInit();
336
- startKeepAliveRequest();
337
- };
338
- const getAvailablePreKeysOnServer = async () => {
339
- const result = await query({
340
- tag: 'iq',
341
- attrs: {
342
- id: generateMessageTag(),
343
- xmlns: 'encrypt',
344
- type: 'get',
345
- to: S_WHATSAPP_NET
346
- },
347
- content: [{ tag: 'count', attrs: {} }]
348
- });
349
- const countChild = getBinaryNodeChild(result, 'count');
350
- return +countChild.attrs.value;
99
+ ws.on(`TAG:${msgId}`, onRecv);
100
+ ws.on('close', onErr);
101
+ ws.on('error', onErr);
102
+ return () => reject(new Boom('Query Cancelled'));
103
+ });
104
+ return result;
105
+ } catch (error) {
106
+ if (error instanceof Boom && error.output?.statusCode === DisconnectReason.timedOut) {
107
+ logger?.warn?.({
108
+ msgId
109
+ }, 'timed out waiting for message');
110
+ return undefined;
111
+ }
112
+ throw error;
113
+ } finally {
114
+ if (onRecv) ws.off(`TAG:${msgId}`, onRecv);
115
+ if (onErr) {
116
+ ws.off('close', onErr);
117
+ ws.off('error', onErr);
118
+ }
119
+ }
120
+ };
121
+ const query = async (node, timeoutMs) => {
122
+ if (!node.attrs.id) {
123
+ node.attrs.id = generateMessageTag();
124
+ }
125
+ const msgId = node.attrs.id;
126
+ const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
127
+ const result = waitForMessage(msgId, timeoutMs).catch(reject);
128
+ sendNode(node).then(async () => resolve(await result)).catch(reject);
129
+ });
130
+ if (result && 'tag' in result) {
131
+ assertNodeErrorFree(result);
132
+ }
133
+ return result;
134
+ };
135
+ const digestKeyBundle = async () => {
136
+ const res = await query({
137
+ tag: 'iq',
138
+ attrs: {
139
+ to: S_WHATSAPP_NET,
140
+ type: 'get',
141
+ xmlns: 'encrypt'
142
+ },
143
+ content: [{
144
+ tag: 'digest',
145
+ attrs: {}
146
+ }]
147
+ });
148
+ const digestNode = getBinaryNodeChild(res, 'digest');
149
+ if (!digestNode) {
150
+ await uploadPreKeys();
151
+ throw new Error('encrypt/get digest returned no digest node');
152
+ }
153
+ };
154
+ const rotateSignedPreKey = async () => {
155
+ const newId = (creds.signedPreKey.keyId || 0) + 1;
156
+ const skey = await signedKeyPair(creds.signedIdentityKey, newId);
157
+ await query({
158
+ tag: 'iq',
159
+ attrs: {
160
+ to: S_WHATSAPP_NET,
161
+ type: 'set',
162
+ xmlns: 'encrypt'
163
+ },
164
+ content: [{
165
+ tag: 'rotate',
166
+ attrs: {},
167
+ content: [xmppSignedPreKey(skey)]
168
+ }]
169
+ });
170
+ ev.emit('creds.update', {
171
+ signedPreKey: skey
172
+ });
173
+ };
174
+ const executeUSyncQuery = async usyncQuery => {
175
+ if (usyncQuery.protocols.length === 0) {
176
+ throw new Boom('USyncQuery must have at least one protocol');
177
+ }
178
+ const validUsers = usyncQuery.users;
179
+ const userNodes = validUsers.map(user => {
180
+ return {
181
+ tag: 'user',
182
+ attrs: {
183
+ jid: !user.phone ? user.id : undefined
184
+ },
185
+ content: usyncQuery.protocols.map(a => a.getUserElement(user)).filter(a => a !== null)
186
+ };
187
+ });
188
+ const listNode = {
189
+ tag: 'list',
190
+ attrs: {},
191
+ content: userNodes
351
192
  };
352
- // WAWeb has no time throttle here; the server drives uploads via PreKeyLow notifications.
353
- let uploadPreKeysPromise = null;
354
- /** generates and uploads a set of pre-keys to the server */
355
- const uploadPreKeys = async (count = MIN_PREKEY_COUNT) => {
356
- if (uploadPreKeysPromise) {
357
- logger.debug('Pre-key upload already in progress, waiting for completion');
358
- await uploadPreKeysPromise;
359
- return;
360
- }
361
- const uploadLogic = async (retryCount) => {
362
- logger.info({ count, retryCount }, 'uploading pre-keys');
363
- // Generate and save pre-keys atomically (prevents ID collisions on retry)
364
- const node = await keys.transaction(async () => {
365
- logger.debug({ requestedCount: count }, 'generating pre-keys with requested count');
366
- const { update, node } = await getNextPreKeysNode({ creds, keys }, count);
367
- // Update credentials immediately to prevent duplicate IDs on retry
368
- ev.emit('creds.update', update);
369
- return node;
370
- }, creds?.me?.id || 'upload-pre-keys');
371
- // Upload to server (outside transaction, can fail without affecting local keys)
372
- try {
373
- await query(node);
374
- logger.info({ count }, 'uploaded pre-keys successfully');
375
- }
376
- catch (uploadError) {
377
- logger.error({ uploadError: uploadError.toString(), count }, 'Failed to upload pre-keys to server');
378
- // Recurse into uploadLogic; calling uploadPreKeys would await its own in-flight promise.
379
- if (retryCount < 3) {
380
- const backoffDelay = Math.min(1000 * Math.pow(2, retryCount), 10000);
381
- logger.info(`Retrying pre-key upload in ${backoffDelay}ms`);
382
- await new Promise(resolve => setTimeout(resolve, backoffDelay));
383
- return uploadLogic(retryCount + 1);
384
- }
385
- throw uploadError;
386
- }
387
- };
388
- // Add timeout protection
389
- uploadPreKeysPromise = Promise.race([
390
- uploadLogic(0),
391
- new Promise((_, reject) => setTimeout(() => reject(new Boom('Pre-key upload timeout', { statusCode: 408 })), UPLOAD_TIMEOUT))
392
- ]);
393
- try {
394
- await uploadPreKeysPromise;
395
- }
396
- finally {
397
- uploadPreKeysPromise = null;
398
- }
193
+ const queryNode = {
194
+ tag: 'query',
195
+ attrs: {},
196
+ content: usyncQuery.protocols.map(a => a.getQueryElement())
399
197
  };
400
- const verifyCurrentPreKeyExists = async () => {
401
- const currentPreKeyId = creds.nextPreKeyId - 1;
402
- if (currentPreKeyId <= 0) {
403
- return { exists: false, currentPreKeyId: 0 };
404
- }
405
- const preKeys = await keys.get('pre-key', [currentPreKeyId.toString()]);
406
- const exists = !!preKeys[currentPreKeyId.toString()];
407
- return { exists, currentPreKeyId };
198
+ const iq = {
199
+ tag: 'iq',
200
+ attrs: {
201
+ to: S_WHATSAPP_NET,
202
+ type: 'get',
203
+ xmlns: 'usync'
204
+ },
205
+ content: [{
206
+ tag: 'usync',
207
+ attrs: {
208
+ context: usyncQuery.context,
209
+ mode: usyncQuery.mode,
210
+ sid: generateMessageTag(),
211
+ last: 'true',
212
+ index: '0'
213
+ },
214
+ content: [queryNode, listNode]
215
+ }]
408
216
  };
409
- const uploadPreKeysToServerIfRequired = async () => {
410
- try {
411
- let count = 0;
412
- const preKeyCount = await getAvailablePreKeysOnServer();
413
- if (preKeyCount === 0)
414
- count = INITIAL_PREKEY_COUNT;
415
- else
416
- count = MIN_PREKEY_COUNT;
417
- const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists();
418
- logger.info(`${preKeyCount} pre-keys found on server`);
419
- logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`);
420
- const lowServerCount = preKeyCount <= count;
421
- const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0;
422
- const shouldUpload = lowServerCount || missingCurrentPreKey;
423
- if (shouldUpload) {
424
- const reasons = [];
425
- if (lowServerCount)
426
- reasons.push(`server count low (${preKeyCount})`);
427
- if (missingCurrentPreKey)
428
- reasons.push(`current prekey ${currentPreKeyId} missing from storage`);
429
- logger.info(`Uploading PreKeys due to: ${reasons.join(', ')}`);
430
- await uploadPreKeys(count);
431
- }
432
- else {
433
- logger.info(`PreKey validation passed - Server: ${preKeyCount}, Current prekey ${currentPreKeyId} exists`);
434
- }
435
- }
436
- catch (error) {
437
- logger.error({ error }, 'Failed to check/upload pre-keys during initialization');
438
- // Don't throw - allow connection to continue even if pre-key check fails
217
+ const result = await query(iq);
218
+ return usyncQuery.parseUSyncQueryResult(result);
219
+ };
220
+ const onWhatsApp = async (...phoneNumber) => {
221
+ let usyncQuery = new USyncQuery();
222
+ let contactEnabled = false;
223
+ for (const jid of phoneNumber) {
224
+ if (isLidUser(jid)) {
225
+ logger?.warn('LIDs are not supported with onWhatsApp');
226
+ continue;
227
+ } else {
228
+ if (!contactEnabled) {
229
+ contactEnabled = true;
230
+ usyncQuery = usyncQuery.withContactProtocol();
439
231
  }
232
+ const phone = `+${jid.replace('+', '').split('@')[0]?.split(':')[0]}`;
233
+ usyncQuery.withUser(new USyncUser().withPhone(phone));
234
+ }
235
+ }
236
+ if (usyncQuery.users.length === 0) {
237
+ return [];
238
+ }
239
+ const results = await executeUSyncQuery(usyncQuery);
240
+ if (results) {
241
+ return results.list.filter(a => !!a.contact).map(({
242
+ contact,
243
+ id
244
+ }) => ({
245
+ jid: id,
246
+ exists: contact
247
+ }));
248
+ }
249
+ };
250
+ const pnFromLIDUSync = async jids => {
251
+ const usyncQuery = new USyncQuery().withLIDProtocol().withContext('background');
252
+ for (const jid of jids) {
253
+ if (isLidUser(jid)) {
254
+ logger?.warn('LID user found in LID fetch call');
255
+ continue;
256
+ } else {
257
+ usyncQuery.withUser(new USyncUser().withId(jid));
258
+ }
259
+ }
260
+ if (usyncQuery.users.length === 0) {
261
+ return [];
262
+ }
263
+ const results = await executeUSyncQuery(usyncQuery);
264
+ if (results) {
265
+ return results.list.filter(a => !!a.lid).map(({
266
+ lid,
267
+ id
268
+ }) => ({
269
+ pn: id,
270
+ lid: lid
271
+ }));
272
+ }
273
+ return [];
274
+ };
275
+ const ev = makeEventBuffer(logger);
276
+ const {
277
+ creds
278
+ } = authState;
279
+ const keys = addTransactionCapability(authState.keys, logger, transactionOpts);
280
+ const signalRepository = makeSignalRepository({
281
+ creds,
282
+ keys
283
+ }, logger, pnFromLIDUSync);
284
+ let lastDateRecv;
285
+ let epoch = 1;
286
+ let keepAliveReq;
287
+ let qrTimer;
288
+ let closed = false;
289
+ const socketEndHandlers = [];
290
+ const onUnexpectedError = (err, msg) => {
291
+ logger.error({
292
+ err
293
+ }, `unexpected error in '${msg}'`);
294
+ };
295
+ const awaitNextMessage = async sendMsg => {
296
+ if (!ws.isOpen) {
297
+ throw new Boom('Connection Closed', {
298
+ statusCode: DisconnectReason.connectionClosed
299
+ });
300
+ }
301
+ let onOpen;
302
+ let onClose;
303
+ const result = promiseTimeout(connectTimeoutMs, (resolve, reject) => {
304
+ onOpen = resolve;
305
+ onClose = mapWebSocketError(reject);
306
+ ws.on('frame', onOpen);
307
+ ws.on('close', onClose);
308
+ ws.on('error', onClose);
309
+ }).finally(() => {
310
+ ws.off('frame', onOpen);
311
+ ws.off('close', onClose);
312
+ ws.off('error', onClose);
313
+ });
314
+ if (sendMsg) {
315
+ sendRawMessage(sendMsg).catch(onClose);
316
+ }
317
+ return result;
318
+ };
319
+ const validateConnection = async () => {
320
+ let helloMsg = {
321
+ clientHello: {
322
+ ephemeral: ephemeralKeyPair.public
323
+ }
440
324
  };
441
- const onMessageReceived = async (data) => {
442
- await noise.decodeFrame(data, frame => {
443
- // reset ping timeout
444
- lastDateRecv = new Date();
445
- let anyTriggered = false;
446
- anyTriggered = ws.emit('frame', frame);
447
- // if it's a binary node
448
- if (!(frame instanceof Uint8Array)) {
449
- const msgId = frame.attrs.id;
450
- if (logger.level === 'trace') {
451
- logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' });
452
- }
453
- /* Check if this is a response to a message we sent */
454
- anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
455
- /* Check if this is a response to a message we are expecting */
456
- const l0 = frame.tag;
457
- const l1 = frame.attrs || {};
458
- const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : '';
459
- for (const key of Object.keys(l1)) {
460
- anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered;
461
- anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered;
462
- anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered;
463
- }
464
- anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered;
465
- anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered;
466
- if (!anyTriggered && logger.level === 'debug') {
467
- logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv');
468
- }
469
- }
470
- });
471
- };
472
- const end = async (error) => {
473
- if (closed) {
474
- logger.trace({ trace: error?.stack }, 'connection already closed');
475
- return;
476
- }
477
- closed = true;
478
- logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
479
- clearInterval(keepAliveReq);
480
- clearTimeout(qrTimer);
481
- ws.removeAllListeners('close');
482
- ws.removeAllListeners('open');
483
- ws.removeAllListeners('message');
484
- signalRepository.close?.();
485
- if (!ws.isClosed && !ws.isClosing) {
486
- try {
487
- await ws.close();
488
- }
489
- catch { }
490
- }
491
- for (const handler of socketEndHandlers) {
492
- try {
493
- await handler(error);
494
- }
495
- catch (err) {
496
- logger.error({ err }, 'error in socket end handler');
497
- }
325
+ helloMsg = proto.HandshakeMessage.fromObject(helloMsg);
326
+ logger.info({
327
+ browser,
328
+ helloMsg
329
+ }, 'connected to WA');
330
+ const init = proto.HandshakeMessage.encode(helloMsg).finish();
331
+ const result = await awaitNextMessage(init);
332
+ const handshake = proto.HandshakeMessage.decode(result);
333
+ logger.trace({
334
+ handshake
335
+ }, 'handshake recv from WA');
336
+ const keyEnc = noise.processHandshake(handshake, creds.noiseKey);
337
+ let node;
338
+ if (!creds.me) {
339
+ node = generateRegistrationNode(creds, config);
340
+ logger.info({
341
+ node
342
+ }, 'not logged in, attempting registration...');
343
+ } else {
344
+ node = generateLoginNode(creds.me.id, config);
345
+ logger.info({
346
+ node
347
+ }, 'logging in...');
348
+ }
349
+ const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish());
350
+ await sendRawMessage(proto.HandshakeMessage.encode({
351
+ clientFinish: {
352
+ static: keyEnc,
353
+ payload: payloadEnc
354
+ }
355
+ }).finish());
356
+ await noise.finishInit();
357
+ startKeepAliveRequest();
358
+ };
359
+ const getAvailablePreKeysOnServer = async () => {
360
+ const result = await query({
361
+ tag: 'iq',
362
+ attrs: {
363
+ id: generateMessageTag(),
364
+ xmlns: 'encrypt',
365
+ type: 'get',
366
+ to: S_WHATSAPP_NET
367
+ },
368
+ content: [{
369
+ tag: 'count',
370
+ attrs: {}
371
+ }]
372
+ });
373
+ const countChild = getBinaryNodeChild(result, 'count');
374
+ return +countChild.attrs.value;
375
+ };
376
+ let uploadPreKeysPromise = null;
377
+ const uploadPreKeys = async (count = MIN_PREKEY_COUNT) => {
378
+ if (uploadPreKeysPromise) {
379
+ logger.debug('Pre-key upload already in progress, waiting for completion');
380
+ await uploadPreKeysPromise;
381
+ return;
382
+ }
383
+ const uploadLogic = async retryCount => {
384
+ logger.info({
385
+ count,
386
+ retryCount
387
+ }, 'uploading pre-keys');
388
+ const node = await keys.transaction(async () => {
389
+ logger.debug({
390
+ requestedCount: count
391
+ }, 'generating pre-keys with requested count');
392
+ const {
393
+ update,
394
+ node
395
+ } = await getNextPreKeysNode({
396
+ creds,
397
+ keys
398
+ }, count);
399
+ ev.emit('creds.update', update);
400
+ return node;
401
+ }, creds?.me?.id || 'upload-pre-keys');
402
+ try {
403
+ await query(node);
404
+ logger.info({
405
+ count
406
+ }, 'uploaded pre-keys successfully');
407
+ } catch (uploadError) {
408
+ logger.error({
409
+ uploadError: uploadError.toString(),
410
+ count
411
+ }, 'Failed to upload pre-keys to server');
412
+ if (retryCount < 3) {
413
+ const backoffDelay = Math.min(1000 * Math.pow(2, retryCount), 10000);
414
+ logger.info(`Retrying pre-key upload in ${backoffDelay}ms`);
415
+ await new Promise(resolve => setTimeout(resolve, backoffDelay));
416
+ return uploadLogic(retryCount + 1);
498
417
  }
499
- ev.emit('connection.update', {
500
- connection: 'close',
501
- lastDisconnect: {
502
- error,
503
- date: new Date()
504
- }
505
- });
506
- ev.removeAllListeners('connection.update');
507
- ev.destroy();
418
+ throw uploadError;
419
+ }
508
420
  };
509
- const waitForSocketOpen = async () => {
510
- if (ws.isOpen) {
511
- return;
512
- }
513
- if (ws.isClosed || ws.isClosing) {
514
- throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
515
- }
516
- let onOpen;
517
- let onClose;
518
- await new Promise((resolve, reject) => {
519
- onOpen = () => resolve(undefined);
520
- onClose = mapWebSocketError(reject);
521
- ws.on('open', onOpen);
522
- ws.on('close', onClose);
523
- ws.on('error', onClose);
524
- }).finally(() => {
525
- ws.off('open', onOpen);
526
- ws.off('close', onClose);
527
- ws.off('error', onClose);
528
- });
421
+ uploadPreKeysPromise = Promise.race([uploadLogic(0), new Promise((_, reject) => setTimeout(() => reject(new Boom('Pre-key upload timeout', {
422
+ statusCode: 408
423
+ })), UPLOAD_TIMEOUT))]);
424
+ try {
425
+ await uploadPreKeysPromise;
426
+ } finally {
427
+ uploadPreKeysPromise = null;
428
+ }
429
+ };
430
+ const verifyCurrentPreKeyExists = async () => {
431
+ const currentPreKeyId = creds.nextPreKeyId - 1;
432
+ if (currentPreKeyId <= 0) {
433
+ return {
434
+ exists: false,
435
+ currentPreKeyId: 0
436
+ };
437
+ }
438
+ const preKeys = await keys.get('pre-key', [currentPreKeyId.toString()]);
439
+ const exists = !!preKeys[currentPreKeyId.toString()];
440
+ return {
441
+ exists,
442
+ currentPreKeyId
529
443
  };
530
- const startKeepAliveRequest = () => (keepAliveReq = setInterval(() => {
531
- if (!lastDateRecv) {
532
- lastDateRecv = new Date();
533
- }
534
- const diff = Date.now() - lastDateRecv.getTime();
535
- /*
536
- check if it's been a suspicious amount of time since the server responded with our last seen
537
- it could be that the network is down
538
- */
539
- if (diff > keepAliveIntervalMs + 5000) {
540
- void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
444
+ };
445
+ const uploadPreKeysToServerIfRequired = async () => {
446
+ try {
447
+ let count = 0;
448
+ const preKeyCount = await getAvailablePreKeysOnServer();
449
+ if (preKeyCount === 0) count = INITIAL_PREKEY_COUNT;else count = MIN_PREKEY_COUNT;
450
+ const {
451
+ exists: currentPreKeyExists,
452
+ currentPreKeyId
453
+ } = await verifyCurrentPreKeyExists();
454
+ logger.info(`${preKeyCount} pre-keys found on server`);
455
+ logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`);
456
+ const lowServerCount = preKeyCount <= count;
457
+ const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0;
458
+ const shouldUpload = lowServerCount || missingCurrentPreKey;
459
+ if (shouldUpload) {
460
+ const reasons = [];
461
+ if (lowServerCount) reasons.push(`server count low (${preKeyCount})`);
462
+ if (missingCurrentPreKey) reasons.push(`current prekey ${currentPreKeyId} missing from storage`);
463
+ logger.info(`Uploading PreKeys due to: ${reasons.join(', ')}`);
464
+ await uploadPreKeys(count);
465
+ } else {
466
+ logger.info(`PreKey validation passed - Server: ${preKeyCount}, Current prekey ${currentPreKeyId} exists`);
467
+ }
468
+ } catch (error) {
469
+ logger.error({
470
+ error
471
+ }, 'Failed to check/upload pre-keys during initialization');
472
+ }
473
+ };
474
+ const onMessageReceived = async data => {
475
+ await noise.decodeFrame(data, frame => {
476
+ lastDateRecv = new Date();
477
+ let anyTriggered = false;
478
+ anyTriggered = ws.emit('frame', frame);
479
+ if (!(frame instanceof Uint8Array)) {
480
+ const msgId = frame.attrs.id;
481
+ if (logger.level === 'trace') {
482
+ logger.trace({
483
+ xml: binaryNodeToString(frame),
484
+ msg: 'recv xml'
485
+ });
541
486
  }
542
- else if (ws.isOpen) {
543
- // if its all good, send a keep alive request
544
- query({
545
- tag: 'iq',
546
- attrs: {
547
- id: generateMessageTag(),
548
- to: S_WHATSAPP_NET,
549
- type: 'get',
550
- xmlns: 'w:p'
551
- },
552
- content: [{ tag: 'ping', attrs: {} }]
553
- }).catch(err => {
554
- logger.error({ trace: err.stack }, 'error in sending keep alive');
555
- });
487
+ anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
488
+ const l0 = frame.tag;
489
+ const l1 = frame.attrs || {};
490
+ const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : '';
491
+ for (const key of Object.keys(l1)) {
492
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered;
493
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered;
494
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered;
556
495
  }
557
- else {
558
- logger.warn('keep alive called when WS not open');
496
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered;
497
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered;
498
+ if (!anyTriggered && logger.level === 'debug') {
499
+ logger.debug({
500
+ unhandled: true,
501
+ msgId,
502
+ fromMe: false,
503
+ frame
504
+ }, 'communication recv');
559
505
  }
560
- }, keepAliveIntervalMs));
561
- /** i have no idea why this exists. pls enlighten me */
562
- const sendPassiveIq = (tag) => query({
506
+ }
507
+ });
508
+ };
509
+ const end = async error => {
510
+ if (closed) {
511
+ logger.trace({
512
+ trace: error?.stack
513
+ }, 'connection already closed');
514
+ return;
515
+ }
516
+ closed = true;
517
+ logger.info({
518
+ trace: error?.stack
519
+ }, error ? 'connection errored' : 'connection closed');
520
+ clearInterval(keepAliveReq);
521
+ clearTimeout(qrTimer);
522
+ ws.removeAllListeners('close');
523
+ ws.removeAllListeners('open');
524
+ ws.removeAllListeners('message');
525
+ signalRepository.close?.();
526
+ if (!ws.isClosed && !ws.isClosing) {
527
+ try {
528
+ await ws.close();
529
+ } catch {}
530
+ }
531
+ for (const handler of socketEndHandlers) {
532
+ try {
533
+ await handler(error);
534
+ } catch (err) {
535
+ logger.error({
536
+ err
537
+ }, 'error in socket end handler');
538
+ }
539
+ }
540
+ ev.emit('connection.update', {
541
+ connection: 'close',
542
+ lastDisconnect: {
543
+ error,
544
+ date: new Date()
545
+ }
546
+ });
547
+ ev.removeAllListeners('connection.update');
548
+ ev.destroy();
549
+ };
550
+ const waitForSocketOpen = async () => {
551
+ if (ws.isOpen) {
552
+ return;
553
+ }
554
+ if (ws.isClosed || ws.isClosing) {
555
+ throw new Boom('Connection Closed', {
556
+ statusCode: DisconnectReason.connectionClosed
557
+ });
558
+ }
559
+ let onOpen;
560
+ let onClose;
561
+ await new Promise((resolve, reject) => {
562
+ onOpen = () => resolve(undefined);
563
+ onClose = mapWebSocketError(reject);
564
+ ws.on('open', onOpen);
565
+ ws.on('close', onClose);
566
+ ws.on('error', onClose);
567
+ }).finally(() => {
568
+ ws.off('open', onOpen);
569
+ ws.off('close', onClose);
570
+ ws.off('error', onClose);
571
+ });
572
+ };
573
+ const startKeepAliveRequest = () => keepAliveReq = setInterval(() => {
574
+ if (!lastDateRecv) {
575
+ lastDateRecv = new Date();
576
+ }
577
+ const diff = Date.now() - lastDateRecv.getTime();
578
+ if (diff > keepAliveIntervalMs + 5000) {
579
+ void end(new Boom('Connection was lost', {
580
+ statusCode: DisconnectReason.connectionLost
581
+ }));
582
+ } else if (ws.isOpen) {
583
+ query({
563
584
  tag: 'iq',
564
585
  attrs: {
565
- to: S_WHATSAPP_NET,
566
- xmlns: 'passive',
567
- type: 'set'
586
+ id: generateMessageTag(),
587
+ to: S_WHATSAPP_NET,
588
+ type: 'get',
589
+ xmlns: 'w:p'
568
590
  },
569
- content: [{ tag, attrs: {} }]
570
- });
571
- /** logout & invalidate connection */
572
- const logout = async (msg) => {
573
- const jid = authState.creds.me?.id;
574
- if (jid) {
575
- await sendNode({
576
- tag: 'iq',
577
- attrs: {
578
- to: S_WHATSAPP_NET,
579
- type: 'set',
580
- id: generateMessageTag(),
581
- xmlns: 'md'
582
- },
583
- content: [
584
- {
585
- tag: 'remove-companion-device',
586
- attrs: {
587
- jid,
588
- reason: 'user_initiated'
589
- }
590
- }
591
- ]
592
- });
593
- }
594
- void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
591
+ content: [{
592
+ tag: 'ping',
593
+ attrs: {}
594
+ }]
595
+ }).catch(err => {
596
+ logger.error({
597
+ trace: err.stack
598
+ }, 'error in sending keep alive');
599
+ });
600
+ } else {
601
+ logger.warn('keep alive called when WS not open');
602
+ }
603
+ }, keepAliveIntervalMs);
604
+ const sendPassiveIq = tag => query({
605
+ tag: 'iq',
606
+ attrs: {
607
+ to: S_WHATSAPP_NET,
608
+ xmlns: 'passive',
609
+ type: 'set'
610
+ },
611
+ content: [{
612
+ tag,
613
+ attrs: {}
614
+ }]
615
+ });
616
+ const logout = async msg => {
617
+ const jid = authState.creds.me?.id;
618
+ if (jid) {
619
+ await sendNode({
620
+ tag: 'iq',
621
+ attrs: {
622
+ to: S_WHATSAPP_NET,
623
+ type: 'set',
624
+ id: generateMessageTag(),
625
+ xmlns: 'md'
626
+ },
627
+ content: [{
628
+ tag: 'remove-companion-device',
629
+ attrs: {
630
+ jid,
631
+ reason: 'user_initiated'
632
+ }
633
+ }]
634
+ });
635
+ }
636
+ void end(new Boom(msg || 'Intentional Logout', {
637
+ statusCode: DisconnectReason.loggedOut
638
+ }));
639
+ };
640
+ const requestPairingCode = async (phoneNumber, customPairingCode) => {
641
+ const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5));
642
+ if (customPairingCode && customPairingCode?.length !== 8) {
643
+ throw new Error('Custom pairing code must be exactly 8 chars');
644
+ }
645
+ authState.creds.pairingCode = pairingCode;
646
+ authState.creds.me = {
647
+ id: jidEncode(phoneNumber, 's.whatsapp.net'),
648
+ name: '~'
595
649
  };
596
- const requestPairingCode = async (phoneNumber, customPairingCode) => {
597
- const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5));
598
- if (customPairingCode && customPairingCode?.length !== 8) {
599
- throw new Error('Custom pairing code must be exactly 8 chars');
600
- }
601
- authState.creds.pairingCode = pairingCode;
602
- authState.creds.me = {
603
- id: jidEncode(phoneNumber, 's.whatsapp.net'),
604
- name: '~'
605
- };
606
- ev.emit('creds.update', authState.creds);
607
- await sendNode({
608
- tag: 'iq',
609
- attrs: {
610
- to: S_WHATSAPP_NET,
611
- type: 'set',
612
- id: generateMessageTag(),
613
- xmlns: 'md'
614
- },
615
- content: [
616
- {
617
- tag: 'link_code_companion_reg',
618
- attrs: {
619
- jid: authState.creds.me.id,
620
- stage: 'companion_hello',
621
- should_show_push_notification: 'true'
622
- },
623
- content: [
624
- {
625
- tag: 'link_code_pairing_wrapped_companion_ephemeral_pub',
626
- attrs: {},
627
- content: await generatePairingKey()
628
- },
629
- {
630
- tag: 'companion_server_auth_key_pub',
631
- attrs: {},
632
- content: authState.creds.noiseKey.public
633
- },
634
- {
635
- tag: 'companion_platform_id',
636
- attrs: {},
637
- content: getCompanionPlatformId(browser)
638
- },
639
- {
640
- tag: 'companion_platform_display',
641
- attrs: {},
642
- content: `${browser[1]} (${browser[0]})`
643
- },
644
- {
645
- tag: 'link_code_pairing_nonce',
646
- attrs: {},
647
- content: '0'
648
- }
649
- ]
650
- }
651
- ]
652
- });
653
- return authState.creds.pairingCode;
650
+ ev.emit('creds.update', authState.creds);
651
+ await sendNode({
652
+ tag: 'iq',
653
+ attrs: {
654
+ to: S_WHATSAPP_NET,
655
+ type: 'set',
656
+ id: generateMessageTag(),
657
+ xmlns: 'md'
658
+ },
659
+ content: [{
660
+ tag: 'link_code_companion_reg',
661
+ attrs: {
662
+ jid: authState.creds.me.id,
663
+ stage: 'companion_hello',
664
+ should_show_push_notification: 'true'
665
+ },
666
+ content: [{
667
+ tag: 'link_code_pairing_wrapped_companion_ephemeral_pub',
668
+ attrs: {},
669
+ content: await generatePairingKey()
670
+ }, {
671
+ tag: 'companion_server_auth_key_pub',
672
+ attrs: {},
673
+ content: authState.creds.noiseKey.public
674
+ }, {
675
+ tag: 'companion_platform_id',
676
+ attrs: {},
677
+ content: getCompanionPlatformId(browser)
678
+ }, {
679
+ tag: 'companion_platform_display',
680
+ attrs: {},
681
+ content: `${browser[1]} (${browser[0]})`
682
+ }, {
683
+ tag: 'link_code_pairing_nonce',
684
+ attrs: {},
685
+ content: '0'
686
+ }]
687
+ }]
688
+ });
689
+ return authState.creds.pairingCode;
690
+ };
691
+ async function generatePairingKey() {
692
+ const salt = randomBytes(32);
693
+ const randomIv = randomBytes(16);
694
+ const key = await derivePairingCodeKey(authState.creds.pairingCode, salt);
695
+ const ciphered = aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv);
696
+ return Buffer.concat([salt, randomIv, ciphered]);
697
+ }
698
+ const sendWAMBuffer = wamBuffer => {
699
+ return query({
700
+ tag: 'iq',
701
+ attrs: {
702
+ to: S_WHATSAPP_NET,
703
+ id: generateMessageTag(),
704
+ xmlns: 'w:stats'
705
+ },
706
+ content: [{
707
+ tag: 'add',
708
+ attrs: {
709
+ t: Math.round(Date.now() / 1000) + ''
710
+ },
711
+ content: wamBuffer
712
+ }]
713
+ });
714
+ };
715
+ ws.on('message', onMessageReceived);
716
+ ws.on('open', async () => {
717
+ try {
718
+ await validateConnection();
719
+ } catch (err) {
720
+ logger.error({
721
+ err
722
+ }, 'error in validating connection');
723
+ void end(err);
724
+ }
725
+ });
726
+ ws.on('error', mapWebSocketError(end));
727
+ ws.on('close', () => void end(new Boom('Connection Terminated', {
728
+ statusCode: DisconnectReason.connectionClosed
729
+ })));
730
+ ws.on('CB:xmlstreamend', () => void end(new Boom('Connection Terminated by Server', {
731
+ statusCode: DisconnectReason.connectionClosed
732
+ })));
733
+ ws.on('CB:iq,type:set,pair-device', async stanza => {
734
+ const iq = {
735
+ tag: 'iq',
736
+ attrs: {
737
+ to: S_WHATSAPP_NET,
738
+ type: 'result',
739
+ id: stanza.attrs.id
740
+ }
654
741
  };
655
- async function generatePairingKey() {
656
- const salt = randomBytes(32);
657
- const randomIv = randomBytes(16);
658
- const key = await derivePairingCodeKey(authState.creds.pairingCode, salt);
659
- const ciphered = aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv);
660
- return Buffer.concat([salt, randomIv, ciphered]);
661
- }
662
- const sendWAMBuffer = (wamBuffer) => {
663
- return query({
664
- tag: 'iq',
665
- attrs: {
666
- to: S_WHATSAPP_NET,
667
- id: generateMessageTag(),
668
- xmlns: 'w:stats'
669
- },
670
- content: [
671
- {
672
- tag: 'add',
673
- attrs: { t: Math.round(Date.now() / 1000) + '' },
674
- content: wamBuffer
675
- }
676
- ]
677
- });
742
+ await sendNode(iq);
743
+ const pairDeviceNode = getBinaryNodeChild(stanza, 'pair-device');
744
+ const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref');
745
+ const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');
746
+ const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64');
747
+ const advB64 = creds.advSecretKey;
748
+ let qrMs = qrTimeout || 60000;
749
+ const genPairQR = () => {
750
+ if (!ws.isOpen) {
751
+ return;
752
+ }
753
+ const refNode = refNodes.shift();
754
+ if (!refNode) {
755
+ void end(new Boom('QR refs attempts ended', {
756
+ statusCode: DisconnectReason.timedOut
757
+ }));
758
+ return;
759
+ }
760
+ const ref = refNode.content.toString('utf-8');
761
+ const qr = buildPairingQRData(ref, noiseKeyB64, identityKeyB64, advB64, browser);
762
+ ev.emit('connection.update', {
763
+ qr
764
+ });
765
+ qrTimer = setTimeout(genPairQR, qrMs);
766
+ qrMs = qrTimeout || 20000;
678
767
  };
679
- ws.on('message', onMessageReceived);
680
- ws.on('open', async () => {
681
- try {
682
- await validateConnection();
683
- }
684
- catch (err) {
685
- logger.error({ err }, 'error in validating connection');
686
- void end(err);
687
- }
688
- });
689
- ws.on('error', mapWebSocketError(end));
690
- ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
691
- // the server terminated the connection
692
- ws.on('CB:xmlstreamend', () => void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
693
- // QR gen
694
- ws.on('CB:iq,type:set,pair-device', async (stanza) => {
695
- const iq = {
696
- tag: 'iq',
697
- attrs: {
698
- to: S_WHATSAPP_NET,
699
- type: 'result',
700
- id: stanza.attrs.id
701
- }
702
- };
703
- await sendNode(iq);
704
- const pairDeviceNode = getBinaryNodeChild(stanza, 'pair-device');
705
- const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref');
706
- const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');
707
- const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64');
708
- const advB64 = creds.advSecretKey;
709
- let qrMs = qrTimeout || 60000; // time to let a QR live
710
- const genPairQR = () => {
711
- if (!ws.isOpen) {
712
- return;
713
- }
714
- const refNode = refNodes.shift();
715
- if (!refNode) {
716
- void end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut }));
717
- return;
718
- }
719
- const ref = refNode.content.toString('utf-8');
720
- const qr = buildPairingQRData(ref, noiseKeyB64, identityKeyB64, advB64, browser);
721
- ev.emit('connection.update', { qr });
722
- qrTimer = setTimeout(genPairQR, qrMs);
723
- qrMs = qrTimeout || 20000; // shorter subsequent qrs
724
- };
725
- genPairQR();
768
+ genPairQR();
769
+ });
770
+ ws.on('CB:iq,,pair-success', async stanza => {
771
+ logger.debug('pair success recv');
772
+ try {
773
+ updateServerTimeOffset(stanza);
774
+ const {
775
+ reply,
776
+ creds: updatedCreds
777
+ } = configureSuccessfulPairing(stanza, creds);
778
+ logger.info({
779
+ me: updatedCreds.me,
780
+ platform: updatedCreds.platform
781
+ }, 'pairing configured successfully, expect to restart the connection...');
782
+ ev.emit('creds.update', updatedCreds);
783
+ ev.emit('connection.update', {
784
+ isNewLogin: true,
785
+ qr: undefined
786
+ });
787
+ await sendNode(reply);
788
+ void sendUnifiedSession();
789
+ } catch (error) {
790
+ logger.info({
791
+ trace: error.stack
792
+ }, 'error in pairing');
793
+ void end(error);
794
+ }
795
+ });
796
+ ws.on('CB:success', async node => {
797
+ try {
798
+ updateServerTimeOffset(node);
799
+ await uploadPreKeysToServerIfRequired();
800
+ await sendPassiveIq('active');
801
+ try {
802
+ await digestKeyBundle();
803
+ } catch (e) {
804
+ logger.warn({
805
+ e
806
+ }, 'failed to run digest after login');
807
+ }
808
+ } catch (err) {
809
+ logger.warn({
810
+ err
811
+ }, 'failed to send initial passive iq');
812
+ }
813
+ logger.info('opened connection to WA');
814
+ clearTimeout(qrTimer);
815
+ ev.emit('creds.update', {
816
+ me: {
817
+ ...authState.creds.me,
818
+ lid: node.attrs.lid
819
+ }
726
820
  });
727
- // device paired for the first time
728
- // if device pairs successfully, the server asks to restart the connection
729
- ws.on('CB:iq,,pair-success', async (stanza) => {
730
- logger.debug('pair success recv');
731
- try {
732
- updateServerTimeOffset(stanza);
733
- const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds);
734
- logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, 'pairing configured successfully, expect to restart the connection...');
735
- ev.emit('creds.update', updatedCreds);
736
- ev.emit('connection.update', { isNewLogin: true, qr: undefined });
737
- await sendNode(reply);
738
- void sendUnifiedSession();
739
- }
740
- catch (error) {
741
- logger.info({ trace: error.stack }, 'error in pairing');
742
- void end(error);
743
- }
821
+ ev.emit('connection.update', {
822
+ connection: 'open'
744
823
  });
745
- // login complete
746
- ws.on('CB:success', async (node) => {
824
+ void sendUnifiedSession();
825
+ if (node.attrs.lid && authState.creds.me?.id) {
826
+ const myLID = node.attrs.lid;
827
+ process.nextTick(async () => {
747
828
  try {
748
- updateServerTimeOffset(node);
749
- await uploadPreKeysToServerIfRequired();
750
- await sendPassiveIq('active');
751
- // After successful login, validate our key-bundle against server
752
- try {
753
- await digestKeyBundle();
829
+ const myPN = authState.creds.me.id;
830
+ await signalRepository.lidMapping.storeLIDPNMappings([{
831
+ lid: myLID,
832
+ pn: myPN
833
+ }]);
834
+ const {
835
+ user,
836
+ device
837
+ } = jidDecode(myPN);
838
+ await authState.keys.set({
839
+ 'device-list': {
840
+ [user]: [device?.toString() || '0']
754
841
  }
755
- catch (e) {
756
- logger.warn({ e }, 'failed to run digest after login');
757
- }
758
- }
759
- catch (err) {
760
- logger.warn({ err }, 'failed to send initial passive iq');
761
- }
762
- logger.info('opened connection to WA');
763
- clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try
764
- ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } });
765
- ev.emit('connection.update', { connection: 'open' });
766
- void sendUnifiedSession();
767
- if (node.attrs.lid && authState.creds.me?.id) {
768
- const myLID = node.attrs.lid;
769
- process.nextTick(async () => {
770
- try {
771
- const myPN = authState.creds.me.id;
772
- // Store our own LID-PN mapping
773
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }]);
774
- // Create device list for our own user (needed for bulk migration)
775
- const { user, device } = jidDecode(myPN);
776
- await authState.keys.set({
777
- 'device-list': {
778
- [user]: [device?.toString() || '0']
779
- }
780
- });
781
- // migrate our own session
782
- await signalRepository.migrateSession(myPN, myLID);
783
- logger.info({ myPN, myLID }, 'Own LID session created successfully');
784
- }
785
- catch (error) {
786
- logger.error({ error, lid: myLID }, 'Failed to create own LID session');
787
- }
788
- });
842
+ });
843
+ await signalRepository.migrateSession(myPN, myLID);
844
+ logger.info({
845
+ myPN,
846
+ myLID
847
+ }, 'Own LID session created successfully');
848
+ } catch (error) {
849
+ logger.error({
850
+ error,
851
+ lid: myLID
852
+ }, 'Failed to create own LID session');
789
853
  }
790
- });
791
- ws.on('CB:stream:error', (node) => {
792
- const [reasonNode] = getAllBinaryNodeChildren(node);
793
- logger.error({ reasonNode, fullErrorNode: node }, 'stream errored out');
794
- const { reason, statusCode } = getErrorCodeFromStreamError(node);
795
- void end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }));
796
- });
797
- // stream fail, possible logout
798
- ws.on('CB:failure', (node) => {
799
- const reason = +(node.attrs.reason || 500);
800
- void end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }));
801
- });
802
- ws.on('CB:ib,,downgrade_webclient', () => {
803
- void end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }));
804
- });
805
- ws.on('CB:ib,,offline_preview', async (node) => {
806
- logger.info('offline preview received', JSON.stringify(node));
807
- await sendNode({
808
- tag: 'ib',
809
- attrs: {},
810
- content: [{ tag: 'offline_batch', attrs: { count: '100' } }]
811
- });
812
- });
813
- ws.on('CB:ib,,edge_routing', (node) => {
814
- const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing');
815
- const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info');
816
- if (routingInfo?.content) {
817
- authState.creds.routingInfo = Buffer.from(routingInfo?.content);
818
- ev.emit('creds.update', authState.creds);
819
- }
820
- });
821
- let didStartBuffer = false;
822
- process.nextTick(() => {
823
- if (creds.me?.id) {
824
- // start buffering important events
825
- // if we're logged in
826
- ev.buffer();
827
- didStartBuffer = true;
854
+ });
855
+ }
856
+ });
857
+ ws.on('CB:stream:error', node => {
858
+ const [reasonNode] = getAllBinaryNodeChildren(node);
859
+ logger.error({
860
+ reasonNode,
861
+ fullErrorNode: node
862
+ }, 'stream errored out');
863
+ const {
864
+ reason,
865
+ statusCode
866
+ } = getErrorCodeFromStreamError(node);
867
+ void end(new Boom(`Stream Errored (${reason})`, {
868
+ statusCode,
869
+ data: reasonNode || node
870
+ }));
871
+ });
872
+ ws.on('CB:failure', node => {
873
+ const reason = +(node.attrs.reason || 500);
874
+ void end(new Boom('Connection Failure', {
875
+ statusCode: reason,
876
+ data: node.attrs
877
+ }));
878
+ });
879
+ ws.on('CB:ib,,downgrade_webclient', () => {
880
+ void end(new Boom('Multi-device beta not joined', {
881
+ statusCode: DisconnectReason.multideviceMismatch
882
+ }));
883
+ });
884
+ ws.on('CB:ib,,offline_preview', async node => {
885
+ logger.info('offline preview received', JSON.stringify(node));
886
+ await sendNode({
887
+ tag: 'ib',
888
+ attrs: {},
889
+ content: [{
890
+ tag: 'offline_batch',
891
+ attrs: {
892
+ count: '100'
828
893
  }
829
- ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined });
894
+ }]
830
895
  });
831
- // called when all offline notifs are handled
832
- ws.on('CB:ib,,offline', (node) => {
833
- const child = getBinaryNodeChild(node, 'offline');
834
- const offlineNotifs = +(child?.attrs.count || 0);
835
- logger.info(`handled ${offlineNotifs} offline messages/notifications`);
836
- if (didStartBuffer) {
837
- ev.flush();
838
- logger.trace('flushed events for initial buffer');
839
- }
840
- ev.emit('connection.update', { receivedPendingNotifications: true });
896
+ });
897
+ ws.on('CB:ib,,edge_routing', node => {
898
+ const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing');
899
+ const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info');
900
+ if (routingInfo?.content) {
901
+ authState.creds.routingInfo = Buffer.from(routingInfo?.content);
902
+ ev.emit('creds.update', authState.creds);
903
+ }
904
+ });
905
+ let didStartBuffer = false;
906
+ process.nextTick(() => {
907
+ if (creds.me?.id) {
908
+ ev.buffer();
909
+ didStartBuffer = true;
910
+ }
911
+ ev.emit('connection.update', {
912
+ connection: 'connecting',
913
+ receivedPendingNotifications: false,
914
+ qr: undefined
841
915
  });
842
- // update credentials when required
843
- ev.on('creds.update', update => {
844
- const name = update.me?.name;
845
- // if name has just been received
846
- if (creds.me?.name !== name) {
847
- logger.debug({ name }, 'updated pushName');
848
- sendNode({
849
- tag: 'presence',
850
- attrs: { name: name }
851
- }).catch(err => {
852
- logger.warn({ trace: err.stack }, 'error in sending presence update on name change');
853
- });
854
- }
855
- Object.assign(creds, update);
916
+ });
917
+ ws.on('CB:ib,,offline', node => {
918
+ const child = getBinaryNodeChild(node, 'offline');
919
+ const offlineNotifs = +(child?.attrs.count || 0);
920
+ logger.info(`handled ${offlineNotifs} offline messages/notifications`);
921
+ if (didStartBuffer) {
922
+ ev.flush();
923
+ logger.trace('flushed events for initial buffer');
924
+ }
925
+ ev.emit('connection.update', {
926
+ receivedPendingNotifications: true
856
927
  });
857
- const updateServerTimeOffset = ({ attrs }) => {
858
- const tValue = attrs?.t;
859
- if (!tValue) {
860
- return;
861
- }
862
- const parsed = Number(tValue);
863
- if (Number.isNaN(parsed) || parsed <= 0) {
864
- return;
865
- }
866
- const localMs = Date.now();
867
- serverTimeOffsetMs = parsed * 1000 - localMs;
868
- logger.debug({ offset: serverTimeOffsetMs }, 'calculated server time offset');
869
- };
870
- const getUnifiedSessionId = () => {
871
- const offsetMs = 3 * TimeMs.Day;
872
- const now = Date.now() + serverTimeOffsetMs;
873
- const id = (now + offsetMs) % TimeMs.Week;
874
- return id.toString();
875
- };
876
- const sendUnifiedSession = async () => {
877
- if (!ws.isOpen) {
878
- return;
879
- }
880
- const node = {
881
- tag: 'ib',
882
- attrs: {},
883
- content: [
884
- {
885
- tag: 'unified_session',
886
- attrs: {
887
- id: getUnifiedSessionId()
888
- }
889
- }
890
- ]
891
- };
892
- try {
893
- await sendNode(node);
928
+ });
929
+ ev.on('creds.update', update => {
930
+ const name = update.me?.name;
931
+ if (creds.me?.name !== name) {
932
+ logger.debug({
933
+ name
934
+ }, 'updated pushName');
935
+ sendNode({
936
+ tag: 'presence',
937
+ attrs: {
938
+ name: name
894
939
  }
895
- catch (error) {
896
- logger.debug({ error }, 'failed to send unified_session telemetry');
940
+ }).catch(err => {
941
+ logger.warn({
942
+ trace: err.stack
943
+ }, 'error in sending presence update on name change');
944
+ });
945
+ }
946
+ Object.assign(creds, update);
947
+ });
948
+ const updateServerTimeOffset = ({
949
+ attrs
950
+ }) => {
951
+ const tValue = attrs?.t;
952
+ if (!tValue) {
953
+ return;
954
+ }
955
+ const parsed = Number(tValue);
956
+ if (Number.isNaN(parsed) || parsed <= 0) {
957
+ return;
958
+ }
959
+ const localMs = Date.now();
960
+ serverTimeOffsetMs = parsed * 1000 - localMs;
961
+ logger.debug({
962
+ offset: serverTimeOffsetMs
963
+ }, 'calculated server time offset');
964
+ };
965
+ const getUnifiedSessionId = () => {
966
+ const offsetMs = 3 * TimeMs.Day;
967
+ const now = Date.now() + serverTimeOffsetMs;
968
+ const id = (now + offsetMs) % TimeMs.Week;
969
+ return id.toString();
970
+ };
971
+ const sendUnifiedSession = async () => {
972
+ if (!ws.isOpen) {
973
+ return;
974
+ }
975
+ const node = {
976
+ tag: 'ib',
977
+ attrs: {},
978
+ content: [{
979
+ tag: 'unified_session',
980
+ attrs: {
981
+ id: getUnifiedSessionId()
897
982
  }
983
+ }]
898
984
  };
899
- const registerSocketEndHandler = (handler) => {
900
- socketEndHandlers.push(handler);
901
- };
902
- /**
903
- * Fetches your account's standing when it comes to restrictions.
904
- * @returns Returns the state of the restrictions.
905
- */
906
- const fetchAccountReachoutTimelock = async () => {
907
- const queryResult = await executeWMexQuery({}, QueryIds.REACHOUT_TIMELOCK, XWAPaths.xwa2_fetch_account_reachout_timelock, query, generateMessageTag);
908
- const result = {
909
- isActive: !!queryResult?.is_active,
910
- timeEnforcementEnds: queryResult?.time_enforcement_ends && queryResult?.time_enforcement_ends !== '0'
911
- ? new Date(parseInt(queryResult.time_enforcement_ends, 10) * 1000)
912
- : undefined,
913
- enforcementType: queryResult?.enforcement_type ?? ReachoutTimelockEnforcementType.DEFAULT
914
- };
915
- ev.emit('connection.update', { reachoutTimeLock: result });
916
- return result;
917
- };
918
- /**
919
- * Fetches your account's new chat limits.
920
- * @returns Returns the quota and the usage.
921
- */
922
- const fetchNewChatMessageCap = async () => {
923
- return executeWMexQuery({ input: { type: 'INDIVIDUAL_NEW_CHAT_MSG' } }, QueryIds.MESSAGE_CAPPING_INFO, XWAPaths.xwa2_message_capping_info, query, generateMessageTag);
924
- };
925
- return {
926
- type: 'md',
927
- ws,
928
- ev,
929
- authState: { creds, keys },
930
- signalRepository,
931
- get user() {
932
- return authState.creds.me;
933
- },
934
- generateMessageTag,
935
- query,
936
- waitForMessage,
937
- waitForSocketOpen,
938
- sendRawMessage,
939
- sendNode,
940
- logout,
941
- end,
942
- registerSocketEndHandler,
943
- onUnexpectedError,
944
- uploadPreKeys,
945
- uploadPreKeysToServerIfRequired,
946
- digestKeyBundle,
947
- rotateSignedPreKey,
948
- requestPairingCode,
949
- updateServerTimeOffset,
950
- sendUnifiedSession,
951
- wamBuffer: publicWAMBuffer,
952
- /** Waits for the connection to WA to reach a state */
953
- waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
954
- sendWAMBuffer,
955
- executeUSyncQuery,
956
- onWhatsApp,
957
- fetchAccountReachoutTimelock,
958
- fetchNewChatMessageCap
959
- };
985
+ try {
986
+ await sendNode(node);
987
+ } catch (error) {
988
+ logger.debug({
989
+ error
990
+ }, 'failed to send unified_session telemetry');
991
+ }
992
+ };
993
+ const registerSocketEndHandler = handler => {
994
+ socketEndHandlers.push(handler);
995
+ };
996
+ return {
997
+ type: 'md',
998
+ ws,
999
+ ev,
1000
+ authState: {
1001
+ creds,
1002
+ keys
1003
+ },
1004
+ signalRepository,
1005
+ get user() {
1006
+ return authState.creds.me;
1007
+ },
1008
+ generateMessageTag,
1009
+ query,
1010
+ waitForMessage,
1011
+ waitForSocketOpen,
1012
+ sendRawMessage,
1013
+ sendNode,
1014
+ logout,
1015
+ end,
1016
+ registerSocketEndHandler,
1017
+ onUnexpectedError,
1018
+ uploadPreKeys,
1019
+ uploadPreKeysToServerIfRequired,
1020
+ digestKeyBundle,
1021
+ rotateSignedPreKey,
1022
+ requestPairingCode,
1023
+ updateServerTimeOffset,
1024
+ sendUnifiedSession,
1025
+ wamBuffer: publicWAMBuffer,
1026
+ waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
1027
+ sendWAMBuffer,
1028
+ executeUSyncQuery,
1029
+ onWhatsApp
1030
+ };
960
1031
  };
961
- /**
962
- * map the websocket error to the right type
963
- * so it can be retried by the caller
964
- * */
965
1032
  function mapWebSocketError(handler) {
966
- return (error) => {
967
- handler(new Boom(`WebSocket Error (${error?.message})`, { statusCode: getCodeFromWSError(error), data: error }));
968
- };
1033
+ return error => {
1034
+ handler(new Boom(`WebSocket Error (${error?.message})`, {
1035
+ statusCode: getCodeFromWSError(error),
1036
+ data: error
1037
+ }));
1038
+ };
969
1039
  }
970
- //# sourceMappingURL=socket.js.map