@zervonex/baileys 0.0.0-stage → 3.0.2

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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3206 -3
  3. package/WAProto/WAProtoCompile.js +291 -0
  4. package/WAProto/fetcher.js +69 -0
  5. package/WAProto/index.js +88 -0
  6. package/WAProto/parser.js +313 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Builders/AIRich.js +643 -0
  9. package/lib/Builders/base.js +130 -0
  10. package/lib/Builders/builders.js +449 -0
  11. package/lib/Builders/index.js +3 -0
  12. package/lib/Defaults/index.js +225 -0
  13. package/lib/Defaults/phonenumber-mcc.json +223 -0
  14. package/lib/Signal/index.js +2 -0
  15. package/lib/Signal/libsignal.js +523 -0
  16. package/lib/Signal/lid-mapping.js +283 -0
  17. package/lib/Socket/Client/index.js +2 -0
  18. package/lib/Socket/Client/types.js +10 -0
  19. package/lib/Socket/Client/websocket.js +84 -0
  20. package/lib/Socket/business.js +152 -0
  21. package/lib/Socket/chats.js +855 -0
  22. package/lib/Socket/communities.js +147 -0
  23. package/lib/Socket/groups.js +294 -0
  24. package/lib/Socket/index.js +15 -0
  25. package/lib/Socket/messages-recv.js +981 -0
  26. package/lib/Socket/messages-send.js +708 -0
  27. package/lib/Socket/mex.js +34 -0
  28. package/lib/Socket/newsletter.js +781 -0
  29. package/lib/Socket/registration.js +155 -0
  30. package/lib/Socket/socket.js +797 -0
  31. package/lib/Socket/spam-report.js +65 -0
  32. package/lib/Socket/username.js +82 -0
  33. package/lib/Socket/zervonex-handler.js +55 -0
  34. package/lib/Store/index.js +6 -0
  35. package/lib/Store/make-cache-manager-store.js +81 -0
  36. package/lib/Store/make-in-memory-store.js +425 -0
  37. package/lib/Store/make-ordered-dictionary.js +82 -0
  38. package/lib/Store/object-repository.js +31 -0
  39. package/lib/Store/reporting-info-store.js +38 -0
  40. package/lib/Types/Auth.js +2 -0
  41. package/lib/Types/Bussines.js +2 -0
  42. package/lib/Types/Call.js +2 -0
  43. package/lib/Types/Chat.js +8 -0
  44. package/lib/Types/Contact.js +2 -0
  45. package/lib/Types/Events.js +2 -0
  46. package/lib/Types/GroupMetadata.js +2 -0
  47. package/lib/Types/Label.js +25 -0
  48. package/lib/Types/LabelAssociation.js +7 -0
  49. package/lib/Types/Message.js +15 -0
  50. package/lib/Types/Newsletter.js +100 -0
  51. package/lib/Types/Product.js +2 -0
  52. package/lib/Types/Signal.js +2 -0
  53. package/lib/Types/Socket.js +14 -0
  54. package/lib/Types/State.js +26 -0
  55. package/lib/Types/USync.js +2 -0
  56. package/lib/Types/index.js +30 -0
  57. package/lib/Utils/auth-utils.js +142 -0
  58. package/lib/Utils/baileys-event-stream.js +33 -0
  59. package/lib/Utils/bot-capabilities.js +40 -0
  60. package/lib/Utils/bot-planning-replay.js +44 -0
  61. package/lib/Utils/browser-utils.js +26 -0
  62. package/lib/Utils/business.js +75 -0
  63. package/lib/Utils/chat-utils.js +317 -0
  64. package/lib/Utils/companion-reg-client-utils.js +20 -0
  65. package/lib/Utils/consumer-application.js +38 -0
  66. package/lib/Utils/crypto.js +52 -0
  67. package/lib/Utils/decode-wa-message.js +197 -0
  68. package/lib/Utils/event-buffer.js +273 -0
  69. package/lib/Utils/extras.js +186 -0
  70. package/lib/Utils/generics.js +195 -0
  71. package/lib/Utils/history.js +51 -0
  72. package/lib/Utils/identity-change-handler.js +19 -0
  73. package/lib/Utils/index.js +38 -0
  74. package/lib/Utils/link-preview.js +51 -0
  75. package/lib/Utils/logger.js +2 -0
  76. package/lib/Utils/lt-hash.js +3 -0
  77. package/lib/Utils/make-mutex.js +16 -0
  78. package/lib/Utils/message-composer.js +59 -0
  79. package/lib/Utils/message-retry-manager.js +48 -0
  80. package/lib/Utils/messages-media.js +520 -0
  81. package/lib/Utils/messages.js +752 -0
  82. package/lib/Utils/meta-ai-msmsg.js +122 -0
  83. package/lib/Utils/meta-compositing.js +75 -0
  84. package/lib/Utils/modern-messages.js +235 -0
  85. package/lib/Utils/native-flow.js +55 -0
  86. package/lib/Utils/noise-handler.js +161 -0
  87. package/lib/Utils/pre-key-manager.js +76 -0
  88. package/lib/Utils/process-message.js +399 -0
  89. package/lib/Utils/reporting-utils.js +171 -0
  90. package/lib/Utils/signal.js +167 -0
  91. package/lib/Utils/spam-report-utils.js +135 -0
  92. package/lib/Utils/sync-action-utils.js +33 -0
  93. package/lib/Utils/tc-token-utils.js +154 -0
  94. package/lib/Utils/use-multi-file-auth-state.js +307 -0
  95. package/lib/Utils/validate-connection.js +181 -0
  96. package/lib/WABinary/constants.js +1321 -0
  97. package/lib/WABinary/decode.js +152 -0
  98. package/lib/WABinary/encode.js +140 -0
  99. package/lib/WABinary/generic-utils.js +179 -0
  100. package/lib/WABinary/index.js +6 -0
  101. package/lib/WABinary/jid-utils.js +235 -0
  102. package/lib/WABinary/types.js +4 -0
  103. package/lib/WAM/BinaryInfo.js +10 -0
  104. package/lib/WAM/constants.js +22853 -0
  105. package/lib/WAM/encode.js +150 -0
  106. package/lib/WAM/index.js +3 -0
  107. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
  108. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
  109. package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
  110. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
  111. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
  112. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
  113. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
  114. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
  115. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
  116. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
  117. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
  118. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
  119. package/lib/WAUSync/Protocols/index.js +12 -0
  120. package/lib/WAUSync/USyncQuery.js +100 -0
  121. package/lib/WAUSync/USyncUser.js +20 -0
  122. package/lib/WAUSync/index.js +3 -0
  123. package/lib/index.js +67 -0
  124. package/package.json +220 -5
@@ -0,0 +1,855 @@
1
+ import NodeCache from '@cacheable/node-cache'
2
+ import { Boom } from '@hapi/boom'
3
+ import { proto } from '../../WAProto/index.js'
4
+ import {
5
+ DEFAULT_CACHE_TTLS, HISTORY_SYNC_PAUSED_TIMEOUT_MS,
6
+ PROCESSABLE_HISTORY_TYPES, PRIVACY_MEX_IDS
7
+ } from '../Defaults/index.js'
8
+ import { ALL_WA_PATCH_NAMES, SyncState } from '../Types/index.js'
9
+ import {
10
+ chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch,
11
+ ensureLTHashStateVersion, extractSyncdPatches, generateProfilePicture, getHistoryMsg,
12
+ isAppStateSyncIrrecoverable, isMissingKeyError, MAX_SYNC_ATTEMPTS, newLTHashState,
13
+ processSyncAction, makeMutex, processMessage, buildTcTokenFromJid
14
+ } from '../Utils/index.js'
15
+ import {
16
+ getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser,
17
+ isLidUser, isPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary,
18
+ S_WHATSAPP_NET
19
+ } from '../WABinary/index.js'
20
+ import { USyncQuery, USyncUser } from '../WAUSync/index.js'
21
+ import { makeSocket } from './socket.js'
22
+ import { executeWMexQuery } from './mex.js'
23
+
24
+ const APP_STATE_RESYNC_COOLDOWN_MS = 60_000
25
+
26
+ export const makeChatsSocket = (config) => {
27
+ const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config
28
+
29
+ const sock = makeSocket(config)
30
+ const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession, registerSocketEndHandler } = sock
31
+
32
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
33
+
34
+ let privacySettings
35
+ const serverProps = {
36
+ privacyTokenOn1to1: true,
37
+ profilePicPrivacyToken: true,
38
+ lidTrustedTokenIssueToLid: false
39
+ }
40
+
41
+ let syncState = SyncState.Connecting
42
+ let awaitingSyncTimeout
43
+ let historySyncPausedTimeout
44
+
45
+ const historySyncStatus = { initialBootstrapComplete: false, recentSyncComplete: false }
46
+ const blockedCollections = new Set()
47
+ const appStateResyncCooldown = new Map()
48
+
49
+ const processingMutex = makeMutex()
50
+ const messageMutex = makeMutex()
51
+ const receiptMutex = makeMutex()
52
+ const appStatePatchMutex = makeMutex()
53
+ const notificationMutex = makeMutex()
54
+
55
+ const placeholderResendCache = config.placeholderResendCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, useClones: false })
56
+ if (!config.placeholderResendCache) config.placeholderResendCache = placeholderResendCache
57
+
58
+ const profilePictureUrlCache = config.profilePictureUrlCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.PROFILE_PIC, useClones: false })
59
+ if (!config.profilePictureUrlCache) config.profilePictureUrlCache = profilePictureUrlCache
60
+
61
+ const inFlightProfilePictureUrl = new Map()
62
+
63
+ // ─── App state key ───────────────────────────────────────────────────────────
64
+
65
+ const getAppStateSyncKey = async (keyId) => {
66
+ const { [keyId]: key } = await authState.keys.get('app-state-sync-key', [keyId])
67
+ return key
68
+ }
69
+
70
+ const interactiveQuery = async (userNodes, queryNode) => {
71
+ const result = await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'usync' }, content: [{ tag: 'usync', attrs: { sid: generateMessageTag(), mode: 'query', last: 'true', index: '0', context: 'interactive' }, content: [{ tag: 'query', attrs: {}, content: [queryNode] }, { tag: 'list', attrs: {}, content: userNodes }] }] })
72
+ const usyncNode = getBinaryNodeChild(result, 'usync')
73
+ const listNode = getBinaryNodeChild(usyncNode, 'list')
74
+ return getBinaryNodeChildren(listNode, 'user')
75
+ }
76
+
77
+ // ─── Privacy ──────────────────────────────────────────────────────────────
78
+
79
+ const fetchPrivacySettings = async (force = false) => {
80
+ if (!privacySettings || force) {
81
+ const { content } = await query({ tag: 'iq', attrs: { xmlns: 'privacy', to: S_WHATSAPP_NET, type: 'get' }, content: [{ tag: 'privacy', attrs: {} }] })
82
+ privacySettings = reduceBinaryNodeToDictionary(content?.[0], 'category')
83
+ }
84
+ return privacySettings
85
+ }
86
+
87
+ const privacyQuery = async (name, value) => {
88
+ await query({ tag: 'iq', attrs: { xmlns: 'privacy', to: S_WHATSAPP_NET, type: 'set' }, content: [{ tag: 'privacy', attrs: {}, content: [{ tag: 'category', attrs: { name, value } }] }] })
89
+ }
90
+
91
+ const updateMessagesPrivacy = (value) => privacyQuery('messages', value)
92
+ const updateCallPrivacy = (value) => privacyQuery('calladd', value)
93
+ const updateLastSeenPrivacy = (value) => privacyQuery('last', value)
94
+ const updateOnlinePrivacy = (value) => privacyQuery('online', value)
95
+ const updateProfilePicturePrivacy = (value) => privacyQuery('profile', value)
96
+ const updateStatusPrivacy = (value) => privacyQuery('status', value)
97
+ const updateReadReceiptsPrivacy = (value) => privacyQuery('readreceipts', value)
98
+ const updateGroupsAddPrivacy = (value) => privacyQuery('groupadd', value)
99
+
100
+ const updateDefaultDisappearingMode = async (duration) => {
101
+ await query({ tag: 'iq', attrs: { xmlns: 'disappearing_mode', to: S_WHATSAPP_NET, type: 'set' }, content: [{ tag: 'disappearing_mode', attrs: { duration: duration.toString() } }] })
102
+ }
103
+
104
+ // ─── Queries ──────────────────────────────────────────────────────────────
105
+
106
+ const getBotListV2 = async () => {
107
+ const resp = await query({ tag: 'iq', attrs: { xmlns: 'bot', to: S_WHATSAPP_NET, type: 'get' }, content: [{ tag: 'bot', attrs: { v: '2' } }] })
108
+ const botNode = getBinaryNodeChild(resp, 'bot')
109
+ const botList = []
110
+ for (const section of getBinaryNodeChildren(botNode, 'section')) {
111
+ if (section.attrs.type === 'all') {
112
+ for (const bot of getBinaryNodeChildren(section, 'bot')) {
113
+ botList.push({ jid: bot.attrs.jid, personaId: bot.attrs['persona_id'] })
114
+ }
115
+ }
116
+ }
117
+ return botList
118
+ }
119
+
120
+ const fetchStatus = async (...jids) => {
121
+ const usyncQuery = new USyncQuery().withStatusProtocol()
122
+ for (const jid of jids) usyncQuery.withUser(new USyncUser().withId(jid))
123
+ const result = await sock.executeUSyncQuery(usyncQuery)
124
+ return result?.list
125
+ }
126
+
127
+ const fetchDisappearingDuration = async (...jids) => {
128
+ const usyncQuery = new USyncQuery().withDisappearingModeProtocol()
129
+ for (const jid of jids) usyncQuery.withUser(new USyncUser().withId(jid))
130
+ const result = await sock.executeUSyncQuery(usyncQuery)
131
+ return result?.list
132
+ }
133
+
134
+ const onWhatsApp = async (...jids) => {
135
+ const usyncQuery = new USyncQuery()
136
+ let contactEnabled = false
137
+ for (let jid of jids) {
138
+ if (isLidUser(jid)) {
139
+ const pn = await signalRepository.lidMapping.getPNForLID(jid)
140
+ if (pn) {
141
+ jid = pn
142
+ } else {
143
+ if (!contactEnabled) { contactEnabled = true; usyncQuery.withContactProtocol().withLIDProtocol() }
144
+ usyncQuery.withUser(new USyncUser().withId(jid))
145
+ continue
146
+ }
147
+ }
148
+ if (!contactEnabled) { contactEnabled = true; usyncQuery.withContactProtocol().withLIDProtocol() }
149
+ const phone = `+${jid.replace('+', '').split('@')[0].split(':')[0]}`
150
+ usyncQuery.withUser(new USyncUser().withPhone(phone))
151
+ }
152
+ if (usyncQuery.users.length === 0) return []
153
+ const results = await sock.executeUSyncQuery(usyncQuery)
154
+ if (!results) return []
155
+ return Promise.all(
156
+ results.list
157
+ .filter(a => a.contact === true && a.id && a.id !== 'undefined')
158
+ .map(async ({ id, lid }) => {
159
+ try {
160
+ const businessProfile = await getBusinessProfile(id)
161
+ const isBusiness = businessProfile && Object.keys(businessProfile).length > 0
162
+ if (isBusiness) {
163
+ const { wid, ...businessInfo } = businessProfile
164
+ return { jid: id, exists: true, lid: lid || id, status: 'business', businessInfo }
165
+ }
166
+ return { jid: id, exists: true, lid: lid || id, status: 'regular' }
167
+ } catch (error) {
168
+ return { jid: id, exists: true, lid: lid || id, status: 'error', error: error?.message }
169
+ }
170
+ })
171
+ )
172
+ }
173
+
174
+ // ─── Profile ──────────────────────────────────────────────────────────────
175
+
176
+ const updateProfilePicture = async (jid, content, dimensions) => {
177
+ if (!jid) throw new Boom('Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update')
178
+ const targetJid = jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id) ? jidNormalizedUser(jid) : undefined
179
+ const { img } = await generateProfilePicture(content, dimensions)
180
+ await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'w:profile:picture', ...(targetJid ? { target: targetJid } : {}) }, content: [{ tag: 'picture', attrs: { type: 'image' }, content: img }] })
181
+ }
182
+
183
+ const removeProfilePicture = async (jid) => {
184
+ if (!jid) throw new Boom('Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update')
185
+ const targetJid = jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id) ? jidNormalizedUser(jid) : undefined
186
+ await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'w:profile:picture', ...(targetJid ? { target: targetJid } : {}) } })
187
+ }
188
+
189
+ const updateProfileStatus = async (status) => {
190
+ await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'status' }, content: [{ tag: 'status', attrs: {}, content: Buffer.from(status, 'utf-8') }] })
191
+ }
192
+
193
+ const updateProfileName = (name) => chatModify({ pushNameSetting: name }, '')
194
+
195
+ const profilePictureUrl = async (jid, type = 'preview', timeoutMs) => {
196
+ const normalizedJid = jidNormalizedUser(jid)
197
+ const cacheKey = `${normalizedJid}:${type}`
198
+ const cached = profilePictureUrlCache.get(cacheKey)
199
+ if (typeof cached !== 'undefined') return cached || undefined
200
+ const inFlight = inFlightProfilePictureUrl.get(cacheKey)
201
+ if (inFlight) return inFlight
202
+ const fetchPromise = (async () => {
203
+ const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid)
204
+ const me = authState.creds.me
205
+ const isSelf = me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)))
206
+ let tcTokenContent
207
+ if (serverProps.profilePicPrivacyToken && isUserJid && !isSelf) {
208
+ tcTokenContent = await buildTcTokenFromJid({ authState, jid: normalizedJid, getLIDForPN })
209
+ }
210
+ const picture = { tag: 'picture', attrs: { type, query: 'url' }, ...(tcTokenContent?.length ? { content: tcTokenContent } : {}) }
211
+ const result = await query({ tag: 'iq', attrs: { target: normalizedJid, to: S_WHATSAPP_NET, type: 'get', xmlns: 'w:profile:picture' }, content: [picture] }, timeoutMs)
212
+ const child = getBinaryNodeChild(result, 'picture')
213
+ const url = child?.attrs?.url
214
+ profilePictureUrlCache.set(cacheKey, url || null)
215
+ return url
216
+ })()
217
+ inFlightProfilePictureUrl.set(cacheKey, fetchPromise)
218
+ try {
219
+ return await fetchPromise
220
+ } finally {
221
+ inFlightProfilePictureUrl.delete(cacheKey)
222
+ }
223
+ }
224
+
225
+ // ─── Blocklist ────────────────────────────────────────────────────────────
226
+
227
+ const fetchBlocklist = async () => {
228
+ const result = await query({ tag: 'iq', attrs: { xmlns: 'blocklist', to: S_WHATSAPP_NET, type: 'get' } })
229
+ const listNode = getBinaryNodeChild(result, 'list')
230
+ return getBinaryNodeChildren(listNode, 'item').map(n => n.attrs.jid)
231
+ }
232
+
233
+ const updateBlockStatus = async (jid, action) => {
234
+ const normalizedJid = jidNormalizedUser(jid)
235
+ let lid
236
+ let pn_jid
237
+ if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
238
+ lid = normalizedJid
239
+ if (action === 'block') {
240
+ const pn = await signalRepository.lidMapping.getPNForLID(normalizedJid)
241
+ if (!pn) throw new Boom(`Unable to resolve PN JID for LID: ${jid}`, { statusCode: 400 })
242
+ pn_jid = jidNormalizedUser(pn)
243
+ }
244
+ } else if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
245
+ const mapped = await signalRepository.lidMapping.getLIDForPN(normalizedJid)
246
+ if (!mapped) throw new Boom(`Unable to resolve LID for PN JID: ${jid}`, { statusCode: 400 })
247
+ lid = mapped
248
+ if (action === 'block') pn_jid = jidNormalizedUser(normalizedJid)
249
+ } else {
250
+ throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 })
251
+ }
252
+ const itemAttrs = { action, jid: lid }
253
+ if (action === 'block') {
254
+ if (!pn_jid) throw new Boom(`pn_jid required for block: ${jid}`, { statusCode: 400 })
255
+ itemAttrs.pn_jid = pn_jid
256
+ }
257
+ await query({ tag: 'iq', attrs: { xmlns: 'blocklist', to: S_WHATSAPP_NET, type: 'set' }, content: [{ tag: 'item', attrs: itemAttrs }] })
258
+ }
259
+
260
+ // ─── Privacy MEX queries ──────────────────────────────────────────────────
261
+
262
+ const privacyMexQuery = (variables, queryId, dataPath) => executeWMexQuery(variables, queryId, dataPath, query, generateMessageTag)
263
+
264
+ const updateTextStatus = (text, emoji = null) => {
265
+ const input = { text }
266
+ if (emoji) input.emoji = { content: emoji }
267
+ return privacyMexQuery({ text_status_input: input }, PRIVACY_MEX_IDS.UPDATE_TEXT_STATUS, 'xwa2_text_status_update')
268
+ }
269
+ const getTextStatusList = (jids, lastUpdateTime = null) => privacyMexQuery({ input: jids.map(jid => ({ jid, last_update_time: lastUpdateTime })) }, PRIVACY_MEX_IDS.GET_TEXT_STATUS_LIST, 'xwa2_text_status_list')
270
+ const fetchUserPictureInfo = jid => privacyMexQuery({ jid }, PRIVACY_MEX_IDS.FETCH_USER_PICTURE, 'xwa2_fetch_user_picture_info')
271
+ const setProfilePictureMex = (imageBase64, type = 'image') => privacyMexQuery({ input: { image: imageBase64, type } }, PRIVACY_MEX_IDS.PROFILE_PICTURE_MUT, 'xwa2_profile_picture_mutation')
272
+ const accountLogin = phoneNumber => privacyMexQuery({ input: { phone_number: phoneNumber } }, PRIVACY_MEX_IDS.ACCOUNT_LOGIN, 'xwa2_account_login')
273
+ const accountLogout = (phoneNumber, enabledBiometric = false) => privacyMexQuery({ input: { phone_number: phoneNumber, enabled_biometric: enabledBiometric } }, PRIVACY_MEX_IDS.ACCOUNT_LOGOUT, 'xwa2_account_logout')
274
+ const addMultiAccountLink = phoneNumber => privacyMexQuery({ input: { phone_number: phoneNumber } }, PRIVACY_MEX_IDS.ADD_MULTI_ACCOUNT, 'xwa2_add_multi_account_link')
275
+ const revokeMultiAccount = accountJid => privacyMexQuery({ account_jid: accountJid }, PRIVACY_MEX_IDS.MULTI_ACCOUNT_REVOKE, 'xwa2_multi_account_revoke')
276
+ const addTrustedDevice = (deviceId, deviceName) => privacyMexQuery({ device_id: deviceId, device_name: deviceName }, PRIVACY_MEX_IDS.ADD_TRUSTED_DEVICE, 'xwa2_add_trusted_device')
277
+ const getTrustedDevices = () => privacyMexQuery({}, PRIVACY_MEX_IDS.GET_TRUSTED_DEVICES, 'xwa2_get_trusted_devices')
278
+ const untrustTrustedDevice = (deviceId, reason = 'USER_INITIATED') => privacyMexQuery({ device_id: deviceId, reason }, PRIVACY_MEX_IDS.UNTRUST_DEVICE, 'xwa2_untrust_trusted_device')
279
+ const deleteTrustedDevice = deviceId => privacyMexQuery({ device_id: deviceId }, PRIVACY_MEX_IDS.DELETE_TRUSTED_DEVICE, 'xwa2_delete_trusted_device')
280
+ const fetchMobileConfig = (apiVersion = 0, epRefreshId = 0, flags = '') => privacyMexQuery({ api_version: apiVersion, ep_refresh_id: epRefreshId, flags }, PRIVACY_MEX_IDS.MOBILE_CONFIG_FETCH, 'xwa2_mobile_config_fetch')
281
+ const notifyPushName = (groupJid, participants) => privacyMexQuery({ input: { group_jid: groupJid, participants: participants.map(({ jid, pushName }) => ({ jid, push_name: pushName })) } }, PRIVACY_MEX_IDS.NOTIFY_PUSH_NAME, 'xwa2_notify_push_name')
282
+ const contactIntegrityQuery = (jids, useCase = 'START_CHAT_CONTEXT') => privacyMexQuery({ users: jids.map(jid => ({ jid })), use_case: useCase }, PRIVACY_MEX_IDS.CONTACT_INTEGRITY, 'xwa2_fetch_wa_users')
283
+ const bizIntegrityQuery = jids => privacyMexQuery({ users: jids.map(jid => ({ jid })) }, PRIVACY_MEX_IDS.BIZ_INTEGRITY, 'xwa2_fetch_wa_users')
284
+ const linkedProfilesSet = profiles => privacyMexQuery({ profiles: profiles.map(p => ({ type: p.type, ...(p.vid ? { vid: p.vid } : p.username ? { username: p.username } : {}) })) }, PRIVACY_MEX_IDS.LINKED_PROFILES_SET, 'xwa2_linked_profiles_set')
285
+ const linkedProfilesRemove = types => privacyMexQuery({ profiles: types.map(type => ({ type })) }, PRIVACY_MEX_IDS.LINKED_PROFILES_REMOVE, 'xwa2_linked_profiles_remove')
286
+ const linkedProfilesUpdate = profiles => privacyMexQuery({ profiles: profiles.map(({ type, showOnProfile }) => ({ type, show_on_profile: showOnProfile })) }, PRIVACY_MEX_IDS.LINKED_PROFILES_UPDATE, 'xwa2_linked_profiles_update')
287
+ const migrateBlocklistLid = (jids, dhash = '', dirtyAck = true) => privacyMexQuery({ input: { blocklist: jids.map(jid => ({ jid })), dhash, dirty_ack: dirtyAck } }, PRIVACY_MEX_IDS.MIGRATE_BLOCKLIST_LID, 'xwa2_migrate_blocklist_lid')
288
+ const qrCodeScan = qrData => privacyMexQuery({ qr_data: qrData }, PRIVACY_MEX_IDS.QR_CODE_SCAN, 'xwa2_qr_code_scan')
289
+
290
+ // ─── Business ─────────────────────────────────────────────────────────────
291
+
292
+ const getBusinessProfile = async (jid) => {
293
+ const results = await query({ tag: 'iq', attrs: { to: 's.whatsapp.net', xmlns: 'w:biz', type: 'get' }, content: [{ tag: 'business_profile', attrs: { v: '244' }, content: [{ tag: 'profile', attrs: { jid } }] }] })
294
+ const profileNode = getBinaryNodeChild(results, 'business_profile')
295
+ const profiles = getBinaryNodeChild(profileNode, 'profile')
296
+ if (profiles) {
297
+ const address = getBinaryNodeChild(profiles, 'address')
298
+ const description = getBinaryNodeChild(profiles, 'description')
299
+ const website = getBinaryNodeChild(profiles, 'website')
300
+ const email = getBinaryNodeChild(profiles, 'email')
301
+ const category = getBinaryNodeChild(getBinaryNodeChild(profiles, 'categories'), 'category')
302
+ const businessHours = getBinaryNodeChild(profiles, 'business_hours')
303
+ const businessHoursConfig = businessHours ? getBinaryNodeChildren(businessHours, 'business_hours_config') : undefined
304
+ const websiteStr = website?.content?.toString()
305
+ return {
306
+ wid: profiles.attrs?.jid,
307
+ address: address?.content?.toString(),
308
+ description: description?.content?.toString() || '',
309
+ website: websiteStr ? [websiteStr] : [],
310
+ email: email?.content?.toString(),
311
+ category: category?.content?.toString(),
312
+ business_hours: { timezone: businessHours?.attrs?.timezone, business_config: businessHoursConfig?.map(({ attrs }) => attrs) }
313
+ }
314
+ }
315
+ }
316
+
317
+ // ─── App state ────────────────────────────────────────────────────────────
318
+
319
+ const cleanDirtyBits = async (type, fromTimestamp) => {
320
+ logger.info({ fromTimestamp }, 'clean dirty bits ' + type)
321
+ await sendNode({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'urn:xmpp:whatsapp:dirty', id: generateMessageTag() }, content: [{ tag: 'clean', attrs: { type, ...(fromTimestamp ? { timestamp: fromTimestamp.toString() } : null) } }] })
322
+ }
323
+
324
+ const newAppStateChunkHandler = (isInitialSync) => ({
325
+ onMutation(mutation) {
326
+ processSyncAction(mutation, ev, authState.creds.me, isInitialSync ? { accountSettings: authState.creds.accountSettings } : undefined, logger)
327
+ }
328
+ })
329
+
330
+ const resyncAppState = ev.createBufferedFunction(async (collections, isInitialSync) => {
331
+ const now = Date.now()
332
+ const collectionsToSync = collections.filter(name => (appStateResyncCooldown.get(name) || 0) <= now)
333
+ if (!collectionsToSync.length) return
334
+
335
+ const appStateSyncKeyCache = new Map()
336
+ const getCachedAppStateSyncKey = async (keyId) => {
337
+ if (appStateSyncKeyCache.has(keyId)) return appStateSyncKeyCache.get(keyId) ?? undefined
338
+ const key = await getAppStateSyncKey(keyId)
339
+ appStateSyncKeyCache.set(keyId, key ?? null)
340
+ return key
341
+ }
342
+
343
+ const initialVersionMap = {}
344
+ const globalMutationMap = {}
345
+
346
+ await authState.keys.transaction(async () => {
347
+ const collectionsToHandle = new Set(collectionsToSync)
348
+ const attemptsMap = {}
349
+ const forceSnapshotCollections = new Set()
350
+
351
+ while (collectionsToHandle.size) {
352
+ const states = {}
353
+ const nodes = []
354
+
355
+ for (const name of collectionsToHandle) {
356
+ const result = await authState.keys.get('app-state-sync-version', [name])
357
+ let state = result[name]
358
+ if (state) {
359
+ state = ensureLTHashStateVersion(state)
360
+ if (typeof initialVersionMap[name] === 'undefined') initialVersionMap[name] = state.version
361
+ } else {
362
+ state = newLTHashState()
363
+ }
364
+ states[name] = state
365
+ const shouldForceSnapshot = forceSnapshotCollections.has(name)
366
+ if (shouldForceSnapshot) forceSnapshotCollections.delete(name)
367
+ logger.info(`resyncing ${name} from v${state.version}${shouldForceSnapshot ? ' (forcing snapshot)' : ''}`)
368
+ nodes.push({ tag: 'collection', attrs: { name, version: state.version.toString(), return_snapshot: (shouldForceSnapshot || !state.version).toString() } })
369
+ }
370
+
371
+ const result = await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, xmlns: 'w:sync:app:state', type: 'set' }, content: [{ tag: 'sync', attrs: {}, content: nodes }] })
372
+ const decoded = await extractSyncdPatches(result, config?.options)
373
+
374
+ for (const key in decoded) {
375
+ const name = key
376
+ const { patches, hasMorePatches, snapshot } = decoded[name]
377
+ try {
378
+ if (snapshot) {
379
+ const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot, logger)
380
+ states[name] = newState
381
+ Object.assign(globalMutationMap, mutationMap)
382
+ logger.info(`restored state of ${name} from snapshot to v${newState.version} with mutations`)
383
+ await authState.keys.set({ 'app-state-sync-version': { [name]: newState } })
384
+ }
385
+ if (patches.length) {
386
+ const { state: newState, mutationMap } = await decodePatches(name, patches, states[name], getCachedAppStateSyncKey, config.options, initialVersionMap[name], logger, appStateMacVerification.patch)
387
+ await authState.keys.set({ 'app-state-sync-version': { [name]: newState } })
388
+ logger.info(`synced ${name} to v${newState.version}`)
389
+ initialVersionMap[name] = newState.version
390
+ Object.assign(globalMutationMap, mutationMap)
391
+ }
392
+ if (hasMorePatches) {
393
+ logger.info(`${name} has more patches...`)
394
+ } else {
395
+ collectionsToHandle.delete(name)
396
+ }
397
+ } catch (error) {
398
+ attemptsMap[name] = (attemptsMap[name] || 0) + 1
399
+ const logData = { name, attempt: attemptsMap[name], version: states[name].version, statusCode: error.output?.statusCode, errorType: error.name, error: error.stack }
400
+ if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
401
+ logger.warn(logData, `${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`)
402
+ blockedCollections.add(name)
403
+ collectionsToHandle.delete(name)
404
+ appStateResyncCooldown.set(name, Date.now() + APP_STATE_RESYNC_COOLDOWN_MS)
405
+ } else if (isMissingKeyError(error)) {
406
+ logger.info(logData, `${name} blocked on missing key from v${states[name].version}, retrying with snapshot`)
407
+ forceSnapshotCollections.add(name)
408
+ } else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
409
+ logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`)
410
+ collectionsToHandle.delete(name)
411
+ } else {
412
+ logger.info(logData, `failed to sync ${name} from v${states[name].version}, forcing snapshot retry`)
413
+ forceSnapshotCollections.add(name)
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }, authState?.creds?.me?.id || 'resync-app-state')
419
+
420
+ const { onMutation } = newAppStateChunkHandler(isInitialSync)
421
+ for (const key in globalMutationMap) onMutation(globalMutationMap[key])
422
+ })
423
+
424
+ // ─── Presence ─────────────────────────────────────────────────────────────
425
+
426
+ const sendPresenceUpdate = async (type, toJid) => {
427
+ const me = authState.creds.me
428
+ const isAvailableType = type === 'available'
429
+ if (isAvailableType || type === 'unavailable') {
430
+ if (!me.name) { logger.warn('no name present, ignoring presence update request...'); return }
431
+ ev.emit('connection.update', { isOnline: isAvailableType })
432
+ if (isAvailableType) void sendUnifiedSession()
433
+ await sendNode({ tag: 'presence', attrs: { name: me.name.replace(/@/g, ''), type } })
434
+ } else {
435
+ const { server } = jidDecode(toJid)
436
+ const isLid = server === 'lid'
437
+ await sendNode({ tag: 'chatstate', attrs: { from: isLid ? me.lid : me.id, to: toJid }, content: [{ tag: type === 'recording' ? 'composing' : type, attrs: type === 'recording' ? { media: 'audio' } : {} }] })
438
+ }
439
+ }
440
+
441
+ const presenceSubscribe = async (toJid) => {
442
+ const normalizedToJid = jidNormalizedUser(toJid)
443
+ const isUserJid = isPnUser(normalizedToJid) || isLidUser(normalizedToJid)
444
+ const tcTokenContent = isUserJid ? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN }) : undefined
445
+ return sendNode({ tag: 'presence', attrs: { to: toJid, id: generateMessageTag(), type: 'subscribe' }, content: tcTokenContent })
446
+ }
447
+
448
+ const handlePresenceUpdate = ({ tag, attrs, content }) => {
449
+ let presence
450
+ const jid = attrs.from
451
+ const participant = attrs.participant || attrs.from
452
+ if (shouldIgnoreJid(jid) && jid !== S_WHATSAPP_NET) return
453
+ if (tag === 'presence') {
454
+ presence = { lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available', lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined, groupOnlineCount: attrs.count ? +attrs.count : undefined }
455
+ } else if (Array.isArray(content)) {
456
+ const [firstChild] = content
457
+ let type = firstChild.tag
458
+ if (type === 'paused') type = 'available'
459
+ if (firstChild.attrs?.media === 'audio') type = 'recording'
460
+ presence = { lastKnownPresence: type }
461
+ } else {
462
+ logger.error({ tag, attrs, content }, 'recv invalid presence node')
463
+ }
464
+ if (presence) ev.emit('presence.update', { id: jid, presences: { [participant]: presence } })
465
+ }
466
+
467
+ // ─── App patch ────────────────────────────────────────────────────────────
468
+
469
+ const appPatch = async (patchCreate) => {
470
+ const name = patchCreate.type
471
+ const myAppStateKeyId = authState.creds.myAppStateKeyId
472
+ if (!myAppStateKeyId) throw new Boom('App state key not present!', { statusCode: 400 })
473
+
474
+ let initial
475
+ let encodeResult
476
+
477
+ await appStatePatchMutex.mutex(async () => {
478
+ await authState.keys.transaction(async () => {
479
+ logger.debug({ patch: patchCreate }, 'applying app patch')
480
+ await resyncAppState([name], false)
481
+ const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name])
482
+ initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState()
483
+ encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey)
484
+ const { patch, state } = encodeResult
485
+ await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'w:sync:app:state' }, content: [{ tag: 'sync', attrs: {}, content: [{ tag: 'collection', attrs: { name, version: (state.version - 1).toString(), return_snapshot: 'false' }, content: [{ tag: 'patch', attrs: {}, content: proto.SyncdPatch.encode(patch).finish() }] }] }] })
486
+ await authState.keys.set({ 'app-state-sync-version': { [name]: state } })
487
+ }, authState?.creds?.me?.id || 'app-patch')
488
+ })
489
+
490
+ if (config.emitOwnEvents) {
491
+ const { onMutation } = newAppStateChunkHandler(false)
492
+ const { mutationMap } = await decodePatches(name, [{ ...encodeResult.patch, version: { version: encodeResult.state.version } }], initial, getAppStateSyncKey, config.options, undefined, logger)
493
+ for (const key in mutationMap) onMutation(mutationMap[key])
494
+ }
495
+ }
496
+
497
+ // ─── AB props ─────────────────────────────────────────────────────────────
498
+
499
+ const fetchProps = async () => {
500
+ const resultNode = await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, xmlns: 'abt', type: 'get' }, content: [{ tag: 'props', attrs: { protocol: '1', ...(authState?.creds?.lastPropHash ? { hash: authState.creds.lastPropHash } : {}) } }] })
501
+ const propsNode = getBinaryNodeChild(resultNode, 'props')
502
+ let props = {}
503
+ if (propsNode) {
504
+ if (propsNode.attrs?.hash) {
505
+ authState.creds.lastPropHash = propsNode?.attrs?.hash
506
+ ev.emit('creds.update', authState.creds)
507
+ }
508
+ props = reduceBinaryNodeToDictionary(propsNode, 'prop')
509
+ }
510
+ const privacyTokenProp = props['10518'] ?? props['privacy_token_sending_on_all_1_on_1_messages']
511
+ if (privacyTokenProp !== undefined) serverProps.privacyTokenOn1to1 = privacyTokenProp === 'true' || privacyTokenProp === '1'
512
+ const profilePicProp = props['9666'] ?? props['profile_scraping_privacy_token_in_photo_iq']
513
+ if (profilePicProp !== undefined) serverProps.profilePicPrivacyToken = profilePicProp === 'true' || profilePicProp === '1'
514
+ const lidIssueProp = props['14303'] ?? props['lid_trusted_token_issue_to_lid']
515
+ if (lidIssueProp !== undefined) serverProps.lidTrustedTokenIssueToLid = lidIssueProp === 'true' || lidIssueProp === '1'
516
+ logger.debug({ serverProps }, 'fetched props')
517
+ return props
518
+ }
519
+
520
+ // ─── Chat modification ────────────────────────────────────────────────────
521
+
522
+ const chatModify = (mod, jid) => appPatch(chatModificationToAppPatch(mod, jid))
523
+ const updateDisableLinkPreviewsPrivacy = (isPreviewsDisabled) => chatModify({ disableLinkPreviews: { isPreviewsDisabled } }, '')
524
+ const star = (jid, messages, star) => chatModify({ star: { messages, star } }, jid)
525
+ const addOrEditContact = (jid, contact) => chatModify({ contact }, jid)
526
+ const removeContact = (jid) => chatModify({ contact: null }, jid)
527
+ const addLabel = (jid, labels) => chatModify({ addLabel: { ...labels } }, jid)
528
+ const addChatLabel = (jid, labelId) => chatModify({ addChatLabel: { labelId } }, jid)
529
+ const removeChatLabel = (jid, labelId) => chatModify({ removeChatLabel: { labelId } }, jid)
530
+ const addMessageLabel = (jid, messageId, labelId) => chatModify({ addMessageLabel: { messageId, labelId } }, jid)
531
+ const removeMessageLabel = (jid, messageId, labelId) => chatModify({ removeMessageLabel: { messageId, labelId } }, jid)
532
+ const addOrEditQuickReply = (quickReply) => chatModify({ quickReply }, '')
533
+ const removeQuickReply = (timestamp) => chatModify({ quickReply: { timestamp, deleted: true } }, '')
534
+
535
+ // ─── Call link ────────────────────────────────────────────────────────────
536
+
537
+ const createCallLink = async (type, event, timeoutMs) => {
538
+ const result = await query({ tag: 'call', attrs: { id: generateMessageTag(), to: '@call' }, content: [{ tag: 'link_create', attrs: { media: type }, content: event ? [{ tag: 'event', attrs: { start_time: String(event.startTime) } }] : undefined }] }, timeoutMs)
539
+ return getBinaryNodeChild(result, 'link_create')?.attrs?.token
540
+ }
541
+
542
+ // ─── Init ─────────────────────────────────────────────────────────────────
543
+
544
+ const executeInitQueries = () => Promise.all([fetchProps(), fetchBlocklist(), fetchPrivacySettings()])
545
+
546
+ // ─── Message upsert ───────────────────────────────────────────────────────
547
+
548
+ const upsertMessage = ev.createBufferedFunction(async (msg, type) => {
549
+ ev.emit('messages.upsert', { messages: [msg], type })
550
+
551
+ if (msg.pushName) {
552
+ let jid = msg.key.fromMe ? authState.creds.me.id : msg.key.participant || msg.key.remoteJid
553
+ jid = jidNormalizedUser(jid)
554
+ if (!msg.key.fromMe) ev.emit('contacts.update', [{ id: jid, notify: msg.pushName, verifiedName: msg.verifiedBizName }])
555
+ if (msg.key.fromMe && msg.pushName && authState.creds.me?.name !== msg.pushName) ev.emit('creds.update', { me: { ...authState.creds.me, name: msg.pushName } })
556
+ }
557
+
558
+ const historyMsg = getHistoryMsg(msg.message)
559
+ const shouldProcessHistoryMsg = historyMsg
560
+ ? !!historyMsg.mediaKey?.length && shouldSyncHistoryMessage(historyMsg) && PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
561
+ : false
562
+
563
+ if (historyMsg && shouldProcessHistoryMsg) {
564
+ const syncType = historyMsg.syncType
565
+ if (syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP && !historySyncStatus.initialBootstrapComplete) {
566
+ historySyncStatus.initialBootstrapComplete = true
567
+ ev.emit('messaging-history.status', { syncType, status: 'complete', explicit: true })
568
+ }
569
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT && historyMsg.progress === 100 && !historySyncStatus.recentSyncComplete) {
570
+ historySyncStatus.recentSyncComplete = true
571
+ clearTimeout(historySyncPausedTimeout)
572
+ historySyncPausedTimeout = undefined
573
+ ev.emit('messaging-history.status', { syncType, status: 'complete', explicit: true })
574
+ }
575
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
576
+ clearTimeout(historySyncPausedTimeout)
577
+ historySyncPausedTimeout = setTimeout(() => {
578
+ if (!historySyncStatus.recentSyncComplete) {
579
+ historySyncStatus.recentSyncComplete = true
580
+ ev.emit('messaging-history.status', { syncType: proto.HistorySync.HistorySyncType.RECENT, status: 'paused', explicit: false })
581
+ }
582
+ historySyncPausedTimeout = undefined
583
+ }, HISTORY_SYNC_PAUSED_TIMEOUT_MS)
584
+ }
585
+ }
586
+
587
+ if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
588
+ if (awaitingSyncTimeout) { clearTimeout(awaitingSyncTimeout); awaitingSyncTimeout = undefined }
589
+ if (shouldProcessHistoryMsg) {
590
+ syncState = SyncState.Syncing
591
+ logger.info('Transitioned to Syncing state')
592
+ } else {
593
+ syncState = SyncState.Online
594
+ logger.info('History sync skipped, transitioning to Online state and flushing buffer')
595
+ ev.flush()
596
+ }
597
+ }
598
+
599
+ const doAppStateSync = async () => {
600
+ if (syncState === SyncState.Syncing) {
601
+ blockedCollections.clear()
602
+ logger.info('Doing app state sync')
603
+ await resyncAppState(ALL_WA_PATCH_NAMES, true)
604
+ syncState = SyncState.Online
605
+ logger.info('App state sync complete, transitioning to Online state and flushing buffer')
606
+ ev.flush()
607
+ const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1
608
+ ev.emit('creds.update', { accountSyncCounter })
609
+ }
610
+ }
611
+
612
+ await Promise.all([
613
+ (async () => { if (shouldProcessHistoryMsg) await doAppStateSync() })(),
614
+ processMessage(msg, { signalRepository, shouldProcessHistoryMsg, placeholderResendCache, ev, creds: authState.creds, keyStore: authState.keys, logger, options: config.options, getMessage })
615
+ ])
616
+
617
+ if (msg.message?.protocolMessage?.appStateSyncKeyShare) {
618
+ if (blockedCollections.size > 0) {
619
+ const collections = [...blockedCollections]
620
+ blockedCollections.clear()
621
+ logger.info({ collections }, 'app state sync key arrived via protocol message, re-syncing blocked collections')
622
+ resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync on key share'))
623
+ } else if (syncState === SyncState.Syncing) {
624
+ logger.info('App state sync key arrived, triggering app state sync')
625
+ await doAppStateSync()
626
+ }
627
+ }
628
+ })
629
+
630
+ // ─── WS handlers ──────────────────────────────────────────────────────────
631
+
632
+ ws.on('CB:presence', handlePresenceUpdate)
633
+ ws.on('CB:chatstate', handlePresenceUpdate)
634
+
635
+ ws.on('CB:ib,,dirty', async (node) => {
636
+ const { attrs } = getBinaryNodeChild(node, 'dirty')
637
+ const type = attrs.type
638
+ switch (type) {
639
+ case 'account_sync':
640
+ if (attrs.timestamp) {
641
+ let { lastAccountSyncTimestamp } = authState.creds
642
+ if (lastAccountSyncTimestamp) await cleanDirtyBits('account_sync', lastAccountSyncTimestamp)
643
+ lastAccountSyncTimestamp = +attrs.timestamp
644
+ ev.emit('creds.update', { lastAccountSyncTimestamp })
645
+ }
646
+ break
647
+ case 'groups':
648
+ break
649
+ default:
650
+ logger.info({ node }, 'received unknown sync')
651
+ break
652
+ }
653
+ })
654
+
655
+ // ─── Event listeners ──────────────────────────────────────────────────────
656
+
657
+ ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
658
+ if (connection === 'close') {
659
+ blockedCollections.clear()
660
+ clearTimeout(historySyncPausedTimeout)
661
+ historySyncPausedTimeout = undefined
662
+ }
663
+
664
+ if (connection === 'open') {
665
+ if (fireInitQueries) executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'))
666
+ sendPresenceUpdate(markOnlineOnConnect ? 'available' : 'unavailable').catch(error => onUnexpectedError(error, 'presence update requests'))
667
+ }
668
+
669
+ if (!receivedPendingNotifications || syncState !== SyncState.Connecting) return
670
+
671
+ historySyncStatus.initialBootstrapComplete = false
672
+ historySyncStatus.recentSyncComplete = false
673
+ clearTimeout(historySyncPausedTimeout)
674
+ historySyncPausedTimeout = undefined
675
+
676
+ syncState = SyncState.AwaitingInitialSync
677
+ logger.info('Connection is now AwaitingInitialSync, buffering events')
678
+ ev.buffer()
679
+
680
+ const willSyncHistory = shouldSyncHistoryMessage(proto.Message.HistorySyncNotification.create({ syncType: proto.HistorySync.HistorySyncType.RECENT }))
681
+
682
+ if (!willSyncHistory) {
683
+ logger.info('History sync is disabled by config, not waiting for notification. Transitioning to Online.')
684
+ syncState = SyncState.Online
685
+ setTimeout(() => ev.flush(), 0)
686
+ return
687
+ }
688
+
689
+ if (authState.creds.accountSyncCounter > 0) {
690
+ logger.info('Reconnection with existing sync data, skipping history sync wait. Transitioning to Online.')
691
+ syncState = SyncState.Online
692
+ setTimeout(() => ev.flush(), 0)
693
+ return
694
+ }
695
+
696
+ logger.info('First connection, awaiting history sync notification with a 20s timeout.')
697
+ if (awaitingSyncTimeout) clearTimeout(awaitingSyncTimeout)
698
+ awaitingSyncTimeout = setTimeout(() => {
699
+ if (syncState === SyncState.AwaitingInitialSync) {
700
+ logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer')
701
+ syncState = SyncState.Online
702
+ ev.flush()
703
+ const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1
704
+ ev.emit('creds.update', { accountSyncCounter })
705
+ }
706
+ }, 20_000)
707
+ })
708
+
709
+ ev.on('creds.update', ({ myAppStateKeyId }) => {
710
+ if (!myAppStateKeyId || blockedCollections.size === 0) return
711
+ if (syncState === SyncState.Syncing) { blockedCollections.clear(); return }
712
+ const collections = [...blockedCollections]
713
+ blockedCollections.clear()
714
+ logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections')
715
+ resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync'))
716
+ })
717
+
718
+ ev.on('lid-mapping.update', async ({ lid, pn }) => {
719
+ try {
720
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }])
721
+ } catch (error) {
722
+ logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping')
723
+ }
724
+ })
725
+
726
+ registerSocketEndHandler(() => {
727
+ if (awaitingSyncTimeout) { clearTimeout(awaitingSyncTimeout); awaitingSyncTimeout = undefined }
728
+ if (!config.placeholderResendCache && placeholderResendCache.close) placeholderResendCache.close()
729
+ syncState = SyncState.Connecting
730
+ privacySettings = undefined
731
+ })
732
+
733
+ return {
734
+ ...sock,
735
+ serverProps,
736
+ createCallLink,
737
+ getBotListV2,
738
+ processingMutex,
739
+ messageMutex,
740
+ receiptMutex,
741
+ appStatePatchMutex,
742
+ notificationMutex,
743
+ fetchPrivacySettings,
744
+ upsertMessage,
745
+ appPatch,
746
+ sendPresenceUpdate,
747
+ presenceSubscribe,
748
+ profilePictureUrl,
749
+ fetchBlocklist,
750
+ fetchStatus,
751
+ fetchDisappearingDuration,
752
+ updateProfilePicture,
753
+ removeProfilePicture,
754
+ updateProfileStatus,
755
+ updateProfileName,
756
+ updateBlockStatus,
757
+ updateDisableLinkPreviewsPrivacy,
758
+ updateCallPrivacy,
759
+ updateMessagesPrivacy,
760
+ updateLastSeenPrivacy,
761
+ updateOnlinePrivacy,
762
+ updateProfilePicturePrivacy,
763
+ updateStatusPrivacy,
764
+ updateReadReceiptsPrivacy,
765
+ updateGroupsAddPrivacy,
766
+ updateDefaultDisappearingMode,
767
+ getBusinessProfile,
768
+ resyncAppState,
769
+ chatModify,
770
+ cleanDirtyBits,
771
+ addOrEditContact,
772
+ removeContact,
773
+ placeholderResendCache,
774
+ addLabel,
775
+ onWhatsApp,
776
+ addChatLabel,
777
+ removeChatLabel,
778
+ addMessageLabel,
779
+ removeMessageLabel,
780
+ star,
781
+ addOrEditQuickReply,
782
+ removeQuickReply,
783
+ updateTextStatus,
784
+ getTextStatusList,
785
+ fetchUserPictureInfo,
786
+ setProfilePictureMex,
787
+ accountLogin,
788
+ accountLogout,
789
+ addMultiAccountLink,
790
+ revokeMultiAccount,
791
+ addTrustedDevice,
792
+ getTrustedDevices,
793
+ untrustTrustedDevice,
794
+ deleteTrustedDevice,
795
+ fetchMobileConfig,
796
+ notifyPushName,
797
+ contactIntegrityQuery,
798
+ bizIntegrityQuery,
799
+ linkedProfilesSet,
800
+ linkedProfilesRemove,
801
+ linkedProfilesUpdate,
802
+ migrateBlocklistLid,
803
+ qrCodeScan
804
+ }
805
+ }
806
+
807
+ export const checkStatusWA = async (phoneNumber) => {
808
+ if (!phoneNumber) throw new Error('Please provide a phone number')
809
+
810
+ const formattedNumber = (() => {
811
+ let num = phoneNumber
812
+ if (num.includes('@')) num = num.split('@')[0]
813
+ if (num.includes(':')) num = num.split(':')[0]
814
+ if (!num.startsWith('+')) num = '+' + num
815
+ return num
816
+ })()
817
+
818
+ const { parsePhoneNumberWithError } = await import('libphonenumber-js')
819
+ const { countryCallingCode: countryCode, nationalNumber } = parsePhoneNumberWithError(formattedNumber)
820
+ const { mobileRegisterExists, getBanDetails } = await import('./registration.js')
821
+ const { initAuthCreds } = await import('../Utils/index.js')
822
+
823
+ const state = {
824
+ creds: initAuthCreds(),
825
+ keys: { get: async () => ({}), set: async () => { }, transaction: async (fn) => fn() }
826
+ }
827
+
828
+ const build = (status, isBanned, isNeedOfficialWa, banInfo = null) => ({ number: formattedNumber, status, isBanned, isNeedOfficialWa, banInfo })
829
+
830
+ try {
831
+ await mobileRegisterExists({ ...state.creds, phoneNumberCountryCode: countryCode, phoneNumberNationalNumber: nationalNumber })
832
+ return build('active', false, false)
833
+ } catch (err) {
834
+ if (err?.appeal_token) {
835
+ const banDetails = await getBanDetails(err.appeal_token)
836
+ const appealStatus = banDetails?.status || null
837
+ const banType = appealStatus === 'BANNED' ? 'permanent' : 'temporary'
838
+ return build('banned', true, false, {
839
+ banType,
840
+ violationType: err.violation_type || null,
841
+ violationReason: err.violation_type ? `Type ${err.violation_type}` : 'Unknown',
842
+ canAppeal: banType !== 'permanent',
843
+ appealToken: err.appeal_token,
844
+ banTime: banDetails?.ban_time || null,
845
+ banDate: banDetails?.ban_time ? new Date(banDetails.ban_time * 1000).toISOString() : null,
846
+ appealStatus,
847
+ appealCreatedAt: banDetails?.appeal_creation_time ? new Date(banDetails.appeal_creation_time * 1000).toISOString() : null
848
+ })
849
+ }
850
+ if (err?.custom_block_screen) return build('blocked', false, true)
851
+ if (err?.reason === 'incorrect') return build('active', false, false)
852
+ if (err?.reason === 'temporarily_unavailable') return build('rate_limited', false, false)
853
+ return build('error', false, false)
854
+ }
855
+ }