@nexustechpro/baileys 2.1.0 → 2.1.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.
@@ -1,82 +1,64 @@
1
1
  import NodeCache from '@cacheable/node-cache'
2
2
  import { Boom } from '@hapi/boom'
3
3
  import { proto } from '../../WAProto/index.js'
4
- import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES, PHONENUMBER_MCC, MOBILE_TOKEN, MOBILE_USERAGENT, MOBILE_REGISTRATION_ENDPOINT } from '../Defaults/index.js'
4
+ import { DEFAULT_CACHE_TTLS, HISTORY_SYNC_PAUSED_TIMEOUT_MS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js'
5
5
  import { ALL_WA_PATCH_NAMES } from '../Types/index.js'
6
6
  import { SyncState } from '../Types/State.js'
7
- import {
8
- chatModificationToAppPatch,
9
- decodePatches,
10
- decodeSyncdSnapshot,
11
- encodeSyncdPatch,
12
- ensureLTHashStateVersion,
13
- extractSyncdPatches,
14
- generateProfilePicture,
15
- getHistoryMsg,
16
- newLTHashState,
17
- processSyncAction
18
- } from '../Utils/index.js'
7
+ import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, ensureLTHashStateVersion, extractSyncdPatches, generateProfilePicture, getHistoryMsg, isAppStateSyncIrrecoverable, isMissingKeyError, MAX_SYNC_ATTEMPTS, newLTHashState, processSyncAction } from '../Utils/index.js'
19
8
  import { makeMutex } from '../Utils/make-mutex.js'
20
9
  import processMessage from '../Utils/process-message.js'
21
- import {
22
- getBinaryNodeChild,
23
- getBinaryNodeChildren,
24
- isLidUser,
25
- jidDecode,
26
- jidNormalizedUser,
27
- reduceBinaryNodeToDictionary,
28
- S_WHATSAPP_NET,
29
- VIOLATION_TYPES
30
- } from '../WABinary/index.js'
10
+ import { buildTcTokenFromJid } from '../Utils/tc-token-utils.js'
11
+ import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET, VIOLATION_TYPES } from '../WABinary/index.js'
31
12
  import { USyncQuery, USyncUser } from '../WAUSync/index.js'
32
13
  import { makeSocket } from './socket.js'
33
14
 
34
- const MAX_SYNC_ATTEMPTS = 2
35
- const HISTORY_SYNC_PAUSED_TIMEOUT_MS = 120_000
36
15
  const APP_STATE_RESYNC_COOLDOWN_MS = 60_000
37
16
 
38
17
  export const makeChatsSocket = (config) => {
39
- const {
40
- logger,
41
- markOnlineOnConnect,
42
- fireInitQueries,
43
- appStateMacVerification,
44
- shouldIgnoreJid,
45
- shouldSyncHistoryMessage,
46
- getMessage
47
- } = config
18
+ const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config
48
19
 
49
20
  const sock = makeSocket(config)
50
- const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError } = sock
21
+ const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession, registerSocketEndHandler } = sock
22
+
23
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
51
24
 
52
25
  let privacySettings
26
+ /** Server-assigned AB props for protocol behavior. */
27
+ const serverProps = {
28
+ /** AB prop 10518: gate tctoken on 1:1 messages. Default true (safe: avoids 463). */
29
+ privacyTokenOn1to1: true,
30
+ /** AB prop 9666: gate tctoken on profile picture IQs. WA Web default: true. */
31
+ profilePicPrivacyToken: true,
32
+ /** AB prop 14303: issue tctokens to LID instead of PN. WA Web default: false. */
33
+ lidTrustedTokenIssueToLid: false
34
+ }
35
+
53
36
  let syncState = SyncState.Connecting
54
37
  let awaitingSyncTimeout
55
38
  let historySyncPausedTimeout
56
39
 
57
40
  const historySyncStatus = { initialBootstrapComplete: false, recentSyncComplete: false }
58
41
  const blockedCollections = new Set()
42
+ const appStateResyncCooldown = new Map()
59
43
 
44
+ /** this mutex ensures that processing happens in order */
60
45
  const processingMutex = makeMutex()
46
+ /** this mutex ensures that messages are processed in order */
61
47
  const messageMutex = makeMutex()
48
+ /** this mutex ensures that receipts are processed in order */
62
49
  const receiptMutex = makeMutex()
50
+ /** this mutex ensures that app state patches are processed in order */
63
51
  const appStatePatchMutex = makeMutex()
52
+ /** this mutex ensures that notifications are processed in order */
64
53
  const notificationMutex = makeMutex()
65
54
 
66
- const placeholderResendCache = config.placeholderResendCache || new NodeCache({
67
- stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY,
68
- useClones: false
69
- })
55
+ const placeholderResendCache = config.placeholderResendCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, useClones: false })
70
56
  if (!config.placeholderResendCache) config.placeholderResendCache = placeholderResendCache
71
57
 
72
- const profilePictureUrlCache = config.profilePictureUrlCache || new NodeCache({
73
- stdTTL: DEFAULT_CACHE_TTLS.PROFILE_PIC,
74
- useClones: false
75
- })
58
+ const profilePictureUrlCache = config.profilePictureUrlCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.PROFILE_PIC, useClones: false })
76
59
  if (!config.profilePictureUrlCache) config.profilePictureUrlCache = profilePictureUrlCache
77
60
 
78
61
  const inFlightProfilePictureUrl = new Map()
79
- const appStateResyncCooldown = new Map()
80
62
 
81
63
  // ─── Key helpers ────────────────────────────────────────────────────────────
82
64
 
@@ -85,45 +67,26 @@ export const makeChatsSocket = (config) => {
85
67
  return key
86
68
  }
87
69
 
88
-
89
70
  const interactiveQuery = async (userNodes, queryNode) => {
90
- const result = await query({
91
- tag: 'iq',
92
- attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'usync' },
93
- content: [{
94
- tag: 'usync',
95
- attrs: { sid: generateMessageTag(), mode: 'query', last: 'true', index: '0', context: 'interactive' },
96
- content: [
97
- { tag: 'query', attrs: {}, content: [queryNode] },
98
- { tag: 'list', attrs: {}, content: userNodes }
99
- ]
100
- }]
101
- })
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 }] }] })
102
72
  const usyncNode = getBinaryNodeChild(result, 'usync')
103
73
  const listNode = getBinaryNodeChild(usyncNode, 'list')
104
74
  return getBinaryNodeChildren(listNode, 'user')
105
75
  }
106
76
 
107
- // ─── Privacy ─────────────────────────────────────────────────────────────────
77
+ // ─── Privacy ──────────────────────────────────────────────────────────────
108
78
 
109
79
  const fetchPrivacySettings = async (force = false) => {
110
80
  if (!privacySettings || force) {
111
- const { content } = await query({
112
- tag: 'iq',
113
- attrs: { xmlns: 'privacy', to: S_WHATSAPP_NET, type: 'get' },
114
- content: [{ tag: 'privacy', attrs: {} }]
115
- })
81
+ const { content } = await query({ tag: 'iq', attrs: { xmlns: 'privacy', to: S_WHATSAPP_NET, type: 'get' }, content: [{ tag: 'privacy', attrs: {} }] })
116
82
  privacySettings = reduceBinaryNodeToDictionary(content?.[0], 'category')
117
83
  }
118
84
  return privacySettings
119
85
  }
120
86
 
87
+ /** helper function to run a privacy IQ query */
121
88
  const privacyQuery = async (name, value) => {
122
- await query({
123
- tag: 'iq',
124
- attrs: { xmlns: 'privacy', to: S_WHATSAPP_NET, type: 'set' },
125
- content: [{ tag: 'privacy', attrs: {}, content: [{ tag: 'category', attrs: { name, value } }] }]
126
- })
89
+ await query({ tag: 'iq', attrs: { xmlns: 'privacy', to: S_WHATSAPP_NET, type: 'set' }, content: [{ tag: 'privacy', attrs: {}, content: [{ tag: 'category', attrs: { name, value } }] }] })
127
90
  }
128
91
 
129
92
  const updateMessagesPrivacy = (value) => privacyQuery('messages', value)
@@ -136,21 +99,13 @@ export const makeChatsSocket = (config) => {
136
99
  const updateGroupsAddPrivacy = (value) => privacyQuery('groupadd', value)
137
100
 
138
101
  const updateDefaultDisappearingMode = async (duration) => {
139
- await query({
140
- tag: 'iq',
141
- attrs: { xmlns: 'disappearing_mode', to: S_WHATSAPP_NET, type: 'set' },
142
- content: [{ tag: 'disappearing_mode', attrs: { duration: duration.toString() } }]
143
- })
102
+ await query({ tag: 'iq', attrs: { xmlns: 'disappearing_mode', to: S_WHATSAPP_NET, type: 'set' }, content: [{ tag: 'disappearing_mode', attrs: { duration: duration.toString() } }] })
144
103
  }
145
104
 
146
- // ─── Queries ─────────────────────────────────────────────────────────────────
105
+ // ─── Queries ──────────────────────────────────────────────────────────────
147
106
 
148
107
  const getBotListV2 = async () => {
149
- const resp = await query({
150
- tag: 'iq',
151
- attrs: { xmlns: 'bot', to: S_WHATSAPP_NET, type: 'get' },
152
- content: [{ tag: 'bot', attrs: { v: '2' } }]
153
- })
108
+ const resp = await query({ tag: 'iq', attrs: { xmlns: 'bot', to: S_WHATSAPP_NET, type: 'get' }, content: [{ tag: 'bot', attrs: { v: '2' } }] })
154
109
  const botNode = getBinaryNodeChild(resp, 'bot')
155
110
  const botList = []
156
111
  for (const section of getBinaryNodeChildren(botNode, 'section')) {
@@ -180,34 +135,24 @@ export const makeChatsSocket = (config) => {
180
135
  const onWhatsApp = async (...jids) => {
181
136
  const usyncQuery = new USyncQuery()
182
137
  let contactEnabled = false
183
-
184
138
  for (let jid of jids) {
185
139
  if (isLidUser(jid)) {
186
140
  const pn = await signalRepository.lidMapping.getPNForLID(jid)
187
141
  if (pn) {
188
142
  jid = pn
189
143
  } else {
190
- if (!contactEnabled) {
191
- contactEnabled = true
192
- usyncQuery.withContactProtocol().withLIDProtocol()
193
- }
144
+ if (!contactEnabled) { contactEnabled = true; usyncQuery.withContactProtocol().withLIDProtocol() }
194
145
  usyncQuery.withUser(new USyncUser().withId(jid))
195
146
  continue
196
147
  }
197
148
  }
198
- if (!contactEnabled) {
199
- contactEnabled = true
200
- usyncQuery.withContactProtocol().withLIDProtocol()
201
- }
149
+ if (!contactEnabled) { contactEnabled = true; usyncQuery.withContactProtocol().withLIDProtocol() }
202
150
  const phone = `+${jid.replace('+', '').split('@')[0].split(':')[0]}`
203
151
  usyncQuery.withUser(new USyncUser().withPhone(phone))
204
152
  }
205
-
206
153
  if (usyncQuery.users.length === 0) return []
207
-
208
154
  const results = await sock.executeUSyncQuery(usyncQuery)
209
155
  if (!results) return []
210
-
211
156
  return Promise.all(
212
157
  results.list
213
158
  .filter(a => a.contact === true && a.id && a.id !== 'undefined')
@@ -227,53 +172,47 @@ export const makeChatsSocket = (config) => {
227
172
  )
228
173
  }
229
174
 
230
- // ─── Profile ─────────────────────────────────────────────────────────────────
175
+ // ─── Profile ──────────────────────────────────────────────────────────────
231
176
 
232
- const updateProfilePicture = async (jid, content) => {
177
+ /** update the profile picture for yourself or a group */
178
+ const updateProfilePicture = async (jid, content, dimensions) => {
233
179
  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')
234
- let targetJid
235
- if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id)) targetJid = jidNormalizedUser(jid)
236
- const { img } = await generateProfilePicture(content)
237
- await query({
238
- tag: 'iq',
239
- attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'w:profile:picture', ...(targetJid ? { target: targetJid } : {}) },
240
- content: [{ tag: 'picture', attrs: { type: 'image' }, content: img }]
241
- })
180
+ const targetJid = jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id) ? jidNormalizedUser(jid) : undefined
181
+ const { img } = await generateProfilePicture(content, dimensions)
182
+ 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 }] })
242
183
  }
243
184
 
185
+ /** remove the profile picture for yourself or a group */
244
186
  const removeProfilePicture = async (jid) => {
245
187
  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')
246
- let targetJid
247
- if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id)) targetJid = jidNormalizedUser(jid)
248
- await query({
249
- tag: 'iq',
250
- attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'w:profile:picture', ...(targetJid ? { target: targetJid } : {}) }
251
- })
188
+ const targetJid = jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id) ? jidNormalizedUser(jid) : undefined
189
+ await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'w:profile:picture', ...(targetJid ? { target: targetJid } : {}) } })
252
190
  }
253
191
 
192
+ /** update the profile status for yourself */
254
193
  const updateProfileStatus = async (status) => {
255
- await query({
256
- tag: 'iq',
257
- attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'status' },
258
- content: [{ tag: 'status', attrs: {}, content: Buffer.from(status, 'utf-8') }]
259
- })
194
+ await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'status' }, content: [{ tag: 'status', attrs: {}, content: Buffer.from(status, 'utf-8') }] })
260
195
  }
261
196
 
262
197
  const updateProfileName = (name) => chatModify({ pushNameSetting: name }, '')
263
198
 
264
199
  const profilePictureUrl = async (jid, type = 'preview', timeoutMs) => {
265
- jid = jidNormalizedUser(jid)
266
- const cacheKey = `${jid}:${type}`
200
+ const normalizedJid = jidNormalizedUser(jid)
201
+ const cacheKey = `${normalizedJid}:${type}`
267
202
  const cached = profilePictureUrlCache.get(cacheKey)
268
203
  if (typeof cached !== 'undefined') return cached || undefined
269
204
  const inFlight = inFlightProfilePictureUrl.get(cacheKey)
270
205
  if (inFlight) return inFlight
271
206
  const fetchPromise = (async () => {
272
- const result = await query({
273
- tag: 'iq',
274
- attrs: { target: jid, to: S_WHATSAPP_NET, type: 'get', xmlns: 'w:profile:picture' },
275
- content: [{ tag: 'picture', attrs: { type, query: 'url' } }]
276
- }, timeoutMs)
207
+ const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }]
208
+ const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid)
209
+ const me = authState.creds.me
210
+ const isSelf = me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)))
211
+ let content = baseContent
212
+ if (serverProps.profilePicPrivacyToken && isUserJid && !isSelf) {
213
+ content = await buildTcTokenFromJid({ authState, jid: normalizedJid, baseContent, getLIDForPN })
214
+ }
215
+ const result = await query({ tag: 'iq', attrs: { target: normalizedJid, to: S_WHATSAPP_NET, type: 'get', xmlns: 'w:profile:picture' }, content }, timeoutMs)
277
216
  const child = getBinaryNodeChild(result, 'picture')
278
217
  const url = child?.attrs?.url
279
218
  profilePictureUrlCache.set(cacheKey, url || null)
@@ -287,7 +226,7 @@ export const makeChatsSocket = (config) => {
287
226
  }
288
227
  }
289
228
 
290
- // ─── Blocklist ────────────────────────────────────────────────────────────────
229
+ // ─── Blocklist ────────────────────────────────────────────────────────────
291
230
 
292
231
  const fetchBlocklist = async () => {
293
232
  const result = await query({ tag: 'iq', attrs: { xmlns: 'blocklist', to: S_WHATSAPP_NET, type: 'get' } })
@@ -296,21 +235,36 @@ export const makeChatsSocket = (config) => {
296
235
  }
297
236
 
298
237
  const updateBlockStatus = async (jid, action) => {
299
- await query({
300
- tag: 'iq',
301
- attrs: { xmlns: 'blocklist', to: S_WHATSAPP_NET, type: 'set' },
302
- content: [{ tag: 'item', attrs: { action, jid } }]
303
- })
238
+ const normalizedJid = jidNormalizedUser(jid)
239
+ let lid
240
+ let pn_jid
241
+ if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
242
+ lid = normalizedJid
243
+ if (action === 'block') {
244
+ const pn = await signalRepository.lidMapping.getPNForLID(normalizedJid)
245
+ if (!pn) throw new Boom(`Unable to resolve PN JID for LID: ${jid}`, { statusCode: 400 })
246
+ pn_jid = jidNormalizedUser(pn)
247
+ }
248
+ } else if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
249
+ const mapped = await signalRepository.lidMapping.getLIDForPN(normalizedJid)
250
+ if (!mapped) throw new Boom(`Unable to resolve LID for PN JID: ${jid}`, { statusCode: 400 })
251
+ lid = mapped
252
+ if (action === 'block') pn_jid = jidNormalizedUser(normalizedJid)
253
+ } else {
254
+ throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 })
255
+ }
256
+ const itemAttrs = { action, jid: lid }
257
+ if (action === 'block') {
258
+ if (!pn_jid) throw new Boom(`pn_jid required for block: ${jid}`, { statusCode: 400 })
259
+ itemAttrs.pn_jid = pn_jid
260
+ }
261
+ await query({ tag: 'iq', attrs: { xmlns: 'blocklist', to: S_WHATSAPP_NET, type: 'set' }, content: [{ tag: 'item', attrs: itemAttrs }] })
304
262
  }
305
263
 
306
- // ─── Business ────────────────────────────────────────────────────────────────
264
+ // ─── Business ─────────────────────────────────────────────────────────────
307
265
 
308
266
  const getBusinessProfile = async (jid) => {
309
- const results = await query({
310
- tag: 'iq',
311
- attrs: { to: 's.whatsapp.net', xmlns: 'w:biz', type: 'get' },
312
- content: [{ tag: 'business_profile', attrs: { v: '244' }, content: [{ tag: 'profile', attrs: { jid } }] }]
313
- })
267
+ 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 } }] }] })
314
268
  const profileNode = getBinaryNodeChild(results, 'business_profile')
315
269
  const profiles = getBinaryNodeChild(profileNode, 'profile')
316
270
  if (profiles) {
@@ -334,15 +288,11 @@ export const makeChatsSocket = (config) => {
334
288
  }
335
289
  }
336
290
 
337
- // ─── App state ────────────────────────────────────────────────────────────────
291
+ // ─── App state ────────────────────────────────────────────────────────────
338
292
 
339
293
  const cleanDirtyBits = async (type, fromTimestamp) => {
340
294
  logger.info({ fromTimestamp }, 'clean dirty bits ' + type)
341
- await sendNode({
342
- tag: 'iq',
343
- attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'urn:xmpp:whatsapp:dirty', id: generateMessageTag() },
344
- content: [{ tag: 'clean', attrs: { type, ...(fromTimestamp ? { timestamp: fromTimestamp.toString() } : null) } }]
345
- })
295
+ 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) } }] })
346
296
  }
347
297
 
348
298
  const newAppStateChunkHandler = (isInitialSync) => ({
@@ -356,7 +306,6 @@ export const makeChatsSocket = (config) => {
356
306
  const collectionsToSync = collections.filter(name => (appStateResyncCooldown.get(name) || 0) <= now)
357
307
  if (!collectionsToSync.length) return
358
308
 
359
- // Per-invocation cache to avoid redundant key fetches within a single resync
360
309
  const appStateSyncKeyCache = new Map()
361
310
  const getCachedAppStateSyncKey = async (keyId) => {
362
311
  if (appStateSyncKeyCache.has(keyId)) return appStateSyncKeyCache.get(keyId) ?? undefined
@@ -387,24 +336,15 @@ export const makeChatsSocket = (config) => {
387
336
  state = newLTHashState()
388
337
  }
389
338
  states[name] = state
390
-
391
339
  const shouldForceSnapshot = forceSnapshotCollections.has(name)
392
340
  if (shouldForceSnapshot) forceSnapshotCollections.delete(name)
393
-
394
341
  logger.info(`resyncing ${name} from v${state.version}${shouldForceSnapshot ? ' (forcing snapshot)' : ''}`)
395
- nodes.push({
396
- tag: 'collection',
397
- attrs: { name, version: state.version.toString(), return_snapshot: (shouldForceSnapshot || !state.version).toString() }
398
- })
342
+ nodes.push({ tag: 'collection', attrs: { name, version: state.version.toString(), return_snapshot: (shouldForceSnapshot || !state.version).toString() } })
399
343
  }
400
344
 
401
- const result = await query({
402
- tag: 'iq',
403
- attrs: { to: S_WHATSAPP_NET, xmlns: 'w:sync:app:state', type: 'set' },
404
- content: [{ tag: 'sync', attrs: {}, content: nodes }]
405
- })
406
-
345
+ const result = await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, xmlns: 'w:sync:app:state', type: 'set' }, content: [{ tag: 'sync', attrs: {}, content: nodes }] })
407
346
  const decoded = await extractSyncdPatches(result, config?.options)
347
+
408
348
  for (const key in decoded) {
409
349
  const name = key
410
350
  const { patches, hasMorePatches, snapshot } = decoded[name]
@@ -430,25 +370,20 @@ export const makeChatsSocket = (config) => {
430
370
  }
431
371
  } catch (error) {
432
372
  attemptsMap[name] = (attemptsMap[name] || 0) + 1
433
- const isMissingKey = error.output?.statusCode === 404
434
- const isIrrecoverable = attemptsMap[name] >= MAX_SYNC_ATTEMPTS || error.name === 'TypeError'
435
-
436
- logger.info({ name, error: error.stack, attempt: attemptsMap[name] }, `failed to sync ${name} from v${states[name].version}`)
437
- if (!isMissingKey) {
438
- await authState.keys.set({ 'app-state-sync-version': { [name]: null } })
439
- }
440
-
441
- if (isMissingKey) appStateResyncCooldown.set(name, Date.now() + APP_STATE_RESYNC_COOLDOWN_MS)
442
-
443
- if (isMissingKey && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
444
- logger.warn({ name }, `${name} blocked on missing key, parking until key arrives`)
373
+ const logData = { name, attempt: attemptsMap[name], version: states[name].version, statusCode: error.output?.statusCode, errorType: error.name, error: error.stack }
374
+ if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
375
+ logger.warn(logData, `${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`)
445
376
  blockedCollections.add(name)
446
377
  collectionsToHandle.delete(name)
447
- } else if (isMissingKey) {
378
+ appStateResyncCooldown.set(name, Date.now() + APP_STATE_RESYNC_COOLDOWN_MS)
379
+ } else if (isMissingKeyError(error)) {
380
+ logger.info(logData, `${name} blocked on missing key from v${states[name].version}, retrying with snapshot`)
448
381
  forceSnapshotCollections.add(name)
449
- } else if (isIrrecoverable) {
382
+ } else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
383
+ logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`)
450
384
  collectionsToHandle.delete(name)
451
385
  } else {
386
+ logger.info(logData, `failed to sync ${name} from v${states[name].version}, forcing snapshot retry`)
452
387
  forceSnapshotCollections.add(name)
453
388
  }
454
389
  }
@@ -460,33 +395,33 @@ export const makeChatsSocket = (config) => {
460
395
  for (const key in globalMutationMap) onMutation(globalMutationMap[key])
461
396
  })
462
397
 
463
- // ─── Presence ─────────────────────────────────────────────────────────────────
398
+ // ─── Presence ─────────────────────────────────────────────────────────────
464
399
 
465
400
  const sendPresenceUpdate = async (type, toJid) => {
466
401
  const me = authState.creds.me
467
- if (type === 'available' || type === 'unavailable') {
468
- if (!me.name) {
469
- logger.warn('no name present, ignoring presence update request...')
470
- return
471
- }
472
- ev.emit('connection.update', { isOnline: type === 'available' })
402
+ const isAvailableType = type === 'available'
403
+ if (isAvailableType || type === 'unavailable') {
404
+ if (!me.name) { logger.warn('no name present, ignoring presence update request...'); return }
405
+ ev.emit('connection.update', { isOnline: isAvailableType })
406
+ if (isAvailableType) void sendUnifiedSession()
473
407
  await sendNode({ tag: 'presence', attrs: { name: me.name.replace(/@/g, ''), type } })
474
408
  } else {
475
409
  const { server } = jidDecode(toJid)
476
410
  const isLid = server === 'lid'
477
- await sendNode({
478
- tag: 'chatstate',
479
- attrs: { from: isLid ? me.lid : me.id, to: toJid },
480
- content: [{ tag: type === 'recording' ? 'composing' : type, attrs: type === 'recording' ? { media: 'audio' } : {} }]
481
- })
411
+ await sendNode({ tag: 'chatstate', attrs: { from: isLid ? me.lid : me.id, to: toJid }, content: [{ tag: type === 'recording' ? 'composing' : type, attrs: type === 'recording' ? { media: 'audio' } : {} }] })
482
412
  }
483
413
  }
484
414
 
485
- const presenceSubscribe = (toJid, tcToken) => sendNode({
486
- tag: 'presence',
487
- attrs: { to: toJid, id: generateMessageTag(), type: 'subscribe' },
488
- content: tcToken ? [{ tag: 'tctoken', attrs: {}, content: tcToken }] : undefined
489
- })
415
+ /**
416
+ * @param toJid the jid to subscribe to
417
+ * @param tcToken token for subscription, use if present
418
+ */
419
+ const presenceSubscribe = async (toJid) => {
420
+ const normalizedToJid = jidNormalizedUser(toJid)
421
+ const isUserJid = isPnUser(normalizedToJid) || isLidUser(normalizedToJid)
422
+ const tcTokenContent = isUserJid ? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN }) : undefined
423
+ return sendNode({ tag: 'presence', attrs: { to: toJid, id: generateMessageTag(), type: 'subscribe' }, content: tcTokenContent })
424
+ }
490
425
 
491
426
  const handlePresenceUpdate = ({ tag, attrs, content }) => {
492
427
  let presence
@@ -494,10 +429,7 @@ export const makeChatsSocket = (config) => {
494
429
  const participant = attrs.participant || attrs.from
495
430
  if (shouldIgnoreJid(jid) && jid !== S_WHATSAPP_NET) return
496
431
  if (tag === 'presence') {
497
- presence = {
498
- lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available',
499
- lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined
500
- }
432
+ presence = { lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available', lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined, groupOnlineCount: attrs.count ? +attrs.count : undefined }
501
433
  } else if (Array.isArray(content)) {
502
434
  const [firstChild] = content
503
435
  let type = firstChild.tag
@@ -510,7 +442,7 @@ export const makeChatsSocket = (config) => {
510
442
  if (presence) ev.emit('presence.update', { id: jid, presences: { [participant]: presence } })
511
443
  }
512
444
 
513
- // ─── App patch ────────────────────────────────────────────────────────────────
445
+ // ─── App patch ────────────────────────────────────────────────────────────
514
446
 
515
447
  const appPatch = async (patchCreate) => {
516
448
  const name = patchCreate.type
@@ -528,17 +460,7 @@ export const makeChatsSocket = (config) => {
528
460
  initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState()
529
461
  encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey)
530
462
  const { patch, state } = encodeResult
531
- await query({
532
- tag: 'iq',
533
- attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'w:sync:app:state' },
534
- content: [{
535
- tag: 'sync', attrs: {}, content: [{
536
- tag: 'collection',
537
- attrs: { name, version: (state.version - 1).toString(), return_snapshot: 'false' },
538
- content: [{ tag: 'patch', attrs: {}, content: proto.SyncdPatch.encode(patch).finish() }]
539
- }]
540
- }]
541
- })
463
+ 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() }] }] }] })
542
464
  await authState.keys.set({ 'app-state-sync-version': { [name]: state } })
543
465
  }, authState?.creds?.me?.id || 'app-patch')
544
466
  })
@@ -550,14 +472,11 @@ export const makeChatsSocket = (config) => {
550
472
  }
551
473
  }
552
474
 
553
- // ─── Props ────────────────────────────────────────────────────────────────────
475
+ // ─── Props ────────────────────────────────────────────────────────────────
554
476
 
477
+ /** fetch AB props */
555
478
  const fetchProps = async () => {
556
- const resultNode = await query({
557
- tag: 'iq',
558
- attrs: { to: S_WHATSAPP_NET, xmlns: 'w', type: 'get' },
559
- content: [{ tag: 'props', attrs: { protocol: '2', hash: authState?.creds?.lastPropHash || '' } }]
560
- })
479
+ 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 } : {}) } }] })
561
480
  const propsNode = getBinaryNodeChild(resultNode, 'props')
562
481
  let props = {}
563
482
  if (propsNode) {
@@ -567,41 +486,63 @@ export const makeChatsSocket = (config) => {
567
486
  }
568
487
  props = reduceBinaryNodeToDictionary(propsNode, 'prop')
569
488
  }
570
- logger.debug('fetched props')
489
+ const privacyTokenProp = props['10518'] ?? props['privacy_token_sending_on_all_1_on_1_messages']
490
+ if (privacyTokenProp !== undefined) serverProps.privacyTokenOn1to1 = privacyTokenProp === 'true' || privacyTokenProp === '1'
491
+ const profilePicProp = props['9666'] ?? props['profile_scraping_privacy_token_in_photo_iq']
492
+ if (profilePicProp !== undefined) serverProps.profilePicPrivacyToken = profilePicProp === 'true' || profilePicProp === '1'
493
+ const lidIssueProp = props['14303'] ?? props['lid_trusted_token_issue_to_lid']
494
+ if (lidIssueProp !== undefined) serverProps.lidTrustedTokenIssueToLid = lidIssueProp === 'true' || lidIssueProp === '1'
495
+ logger.debug({ serverProps }, 'fetched props')
571
496
  return props
572
497
  }
573
498
 
574
- // ─── Chat modification helpers ────────────────────────────────────────────────
499
+ // ─── Chat modification helpers ────────────────────────────────────────────
575
500
 
501
+ /**
502
+ * modify a chat -- mark unread, read etc.
503
+ * lastMessages must be sorted in reverse chronologically
504
+ * requires the last messages till the last message received; required for archive & unread
505
+ */
576
506
  const chatModify = (mod, jid) => appPatch(chatModificationToAppPatch(mod, jid))
507
+ /** Enable/Disable link preview privacy, not related to baileys link preview generation */
577
508
  const updateDisableLinkPreviewsPrivacy = (isPreviewsDisabled) => chatModify({ disableLinkPreviews: { isPreviewsDisabled } }, '')
509
+ /** Star or Unstar a message */
578
510
  const star = (jid, messages, star) => chatModify({ star: { messages, star } }, jid)
511
+ /** Add or Edit Contact */
579
512
  const addOrEditContact = (jid, contact) => chatModify({ contact }, jid)
513
+ /** Remove Contact */
580
514
  const removeContact = (jid) => chatModify({ contact: null }, jid)
515
+ /** Adds label */
581
516
  const addLabel = (jid, labels) => chatModify({ addLabel: { ...labels } }, jid)
517
+ /** Adds label for the chats */
582
518
  const addChatLabel = (jid, labelId) => chatModify({ addChatLabel: { labelId } }, jid)
519
+ /** Removes label for the chat */
583
520
  const removeChatLabel = (jid, labelId) => chatModify({ removeChatLabel: { labelId } }, jid)
521
+ /** Adds label for the message */
584
522
  const addMessageLabel = (jid, messageId, labelId) => chatModify({ addMessageLabel: { messageId, labelId } }, jid)
523
+ /** Removes label for the message */
585
524
  const removeMessageLabel = (jid, messageId, labelId) => chatModify({ removeMessageLabel: { messageId, labelId } }, jid)
525
+ /** Add or Edit Quick Reply */
586
526
  const addOrEditQuickReply = (quickReply) => chatModify({ quickReply }, '')
527
+ /** Remove Quick Reply */
587
528
  const removeQuickReply = (timestamp) => chatModify({ quickReply: { timestamp, deleted: true } }, '')
588
529
 
589
- // ─── Call link ────────────────────────────────────────────────────────────────
530
+ // ─── Call link ────────────────────────────────────────────────────────────
590
531
 
591
532
  const createCallLink = async (type, event, timeoutMs) => {
592
- const result = await query({
593
- tag: 'call',
594
- attrs: { id: generateMessageTag(), to: '@call' },
595
- content: [{ tag: 'link_create', attrs: { media: type }, content: event ? [{ tag: 'event', attrs: { start_time: String(event.startTime) } }] : undefined }]
596
- }, timeoutMs)
533
+ 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)
597
534
  return getBinaryNodeChild(result, 'link_create')?.attrs?.token
598
535
  }
599
536
 
600
- // ─── Init ─────────────────────────────────────────────────────────────────────
537
+ // ─── Init ─────────────────────────────────────────────────────────────────
601
538
 
539
+ /**
540
+ * queries need to be fired on connection open
541
+ * help ensure parity with WA Web
542
+ */
602
543
  const executeInitQueries = () => Promise.all([fetchProps(), fetchBlocklist(), fetchPrivacySettings()])
603
544
 
604
- // ─── Message upsert ───────────────────────────────────────────────────────────
545
+ // ─── Message upsert ───────────────────────────────────────────────────────
605
546
 
606
547
  const upsertMessage = ev.createBufferedFunction(async (msg, type) => {
607
548
  ev.emit('messages.upsert', { messages: [msg], type })
@@ -609,37 +550,27 @@ export const makeChatsSocket = (config) => {
609
550
  if (msg.pushName) {
610
551
  let jid = msg.key.fromMe ? authState.creds.me.id : msg.key.participant || msg.key.remoteJid
611
552
  jid = jidNormalizedUser(jid)
612
- if (!msg.key.fromMe) {
613
- ev.emit('contacts.update', [{ id: jid, notify: msg.pushName, verifiedName: msg.verifiedBizName }])
614
- }
615
- if (msg.key.fromMe && msg.pushName && authState.creds.me?.name !== msg.pushName) {
616
- ev.emit('creds.update', { me: { ...authState.creds.me, name: msg.pushName } })
617
- }
553
+ if (!msg.key.fromMe) ev.emit('contacts.update', [{ id: jid, notify: msg.pushName, verifiedName: msg.verifiedBizName }])
554
+ if (msg.key.fromMe && msg.pushName && authState.creds.me?.name !== msg.pushName) ev.emit('creds.update', { me: { ...authState.creds.me, name: msg.pushName } })
618
555
  }
619
556
 
620
557
  const historyMsg = getHistoryMsg(msg.message)
621
558
  const shouldProcessHistoryMsg = historyMsg
622
- ? !!historyMsg.mediaKey?.length
623
- && shouldSyncHistoryMessage(historyMsg)
624
- && PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
559
+ ? !!historyMsg.mediaKey?.length && shouldSyncHistoryMessage(historyMsg) && PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
625
560
  : false
626
561
 
627
- // History sync progress tracking
628
562
  if (historyMsg && shouldProcessHistoryMsg) {
629
563
  const syncType = historyMsg.syncType
630
-
631
564
  if (syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP && !historySyncStatus.initialBootstrapComplete) {
632
565
  historySyncStatus.initialBootstrapComplete = true
633
566
  ev.emit('messaging-history.status', { syncType, status: 'complete', explicit: true })
634
567
  }
635
-
636
568
  if (syncType === proto.HistorySync.HistorySyncType.RECENT && historyMsg.progress === 100 && !historySyncStatus.recentSyncComplete) {
637
569
  historySyncStatus.recentSyncComplete = true
638
570
  clearTimeout(historySyncPausedTimeout)
639
571
  historySyncPausedTimeout = undefined
640
572
  ev.emit('messaging-history.status', { syncType, status: 'complete', explicit: true })
641
573
  }
642
-
643
574
  if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
644
575
  clearTimeout(historySyncPausedTimeout)
645
576
  historySyncPausedTimeout = setTimeout(() => {
@@ -652,12 +583,8 @@ export const makeChatsSocket = (config) => {
652
583
  }
653
584
  }
654
585
 
655
- // SyncState machine
656
586
  if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
657
- if (awaitingSyncTimeout) {
658
- clearTimeout(awaitingSyncTimeout)
659
- awaitingSyncTimeout = undefined
660
- }
587
+ if (awaitingSyncTimeout) { clearTimeout(awaitingSyncTimeout); awaitingSyncTimeout = undefined }
661
588
  if (shouldProcessHistoryMsg) {
662
589
  syncState = SyncState.Syncing
663
590
  logger.info('Transitioned to Syncing state')
@@ -683,17 +610,7 @@ export const makeChatsSocket = (config) => {
683
610
 
684
611
  await Promise.all([
685
612
  (async () => { if (shouldProcessHistoryMsg) await doAppStateSync() })(),
686
- processMessage(msg, {
687
- signalRepository,
688
- shouldProcessHistoryMsg,
689
- placeholderResendCache,
690
- ev,
691
- creds: authState.creds,
692
- keyStore: authState.keys,
693
- logger,
694
- options: config.options,
695
- getMessage
696
- })
613
+ processMessage(msg, { signalRepository, shouldProcessHistoryMsg, placeholderResendCache, ev, creds: authState.creds, keyStore: authState.keys, logger, options: config.options, getMessage })
697
614
  ])
698
615
 
699
616
  if (msg.message?.protocolMessage?.appStateSyncKeyShare) {
@@ -709,7 +626,7 @@ export const makeChatsSocket = (config) => {
709
626
  }
710
627
  })
711
628
 
712
- // ─── WS handlers ──────────────────────────────────────────────────────────────
629
+ // ─── WS handlers ──────────────────────────────────────────────────────────
713
630
 
714
631
  ws.on('CB:presence', handlePresenceUpdate)
715
632
  ws.on('CB:chatstate', handlePresenceUpdate)
@@ -734,7 +651,7 @@ export const makeChatsSocket = (config) => {
734
651
  }
735
652
  })
736
653
 
737
- // ─── Event listeners ──────────────────────────────────────────────────────────
654
+ // ─── Event listeners ──────────────────────────────────────────────────────
738
655
 
739
656
  ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
740
657
  if (connection === 'close') {
@@ -759,9 +676,7 @@ export const makeChatsSocket = (config) => {
759
676
  logger.info('Connection is now AwaitingInitialSync, buffering events')
760
677
  ev.buffer()
761
678
 
762
- const willSyncHistory = shouldSyncHistoryMessage(proto.Message.HistorySyncNotification.create({
763
- syncType: proto.HistorySync.HistorySyncType.RECENT
764
- }))
679
+ const willSyncHistory = shouldSyncHistoryMessage(proto.Message.HistorySyncNotification.create({ syncType: proto.HistorySync.HistorySyncType.RECENT }))
765
680
 
766
681
  if (!willSyncHistory) {
767
682
  logger.info('History sync is disabled by config, not waiting for notification. Transitioning to Online.')
@@ -770,7 +685,6 @@ export const makeChatsSocket = (config) => {
770
685
  return
771
686
  }
772
687
 
773
- // On reconnection the server won't push history sync again — skip the wait
774
688
  if (authState.creds.accountSyncCounter > 0) {
775
689
  logger.info('Reconnection with existing sync data, skipping history sync wait. Transitioning to Online.')
776
690
  syncState = SyncState.Online
@@ -791,13 +705,9 @@ export const makeChatsSocket = (config) => {
791
705
  }, 20_000)
792
706
  })
793
707
 
794
- // Re-sync collections that were parked due to a missing app state key
795
708
  ev.on('creds.update', ({ myAppStateKeyId }) => {
796
709
  if (!myAppStateKeyId || blockedCollections.size === 0) return
797
- if (syncState === SyncState.Syncing) {
798
- blockedCollections.clear()
799
- return
800
- }
710
+ if (syncState === SyncState.Syncing) { blockedCollections.clear(); return }
801
711
  const collections = [...blockedCollections]
802
712
  blockedCollections.clear()
803
713
  logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections')
@@ -812,10 +722,18 @@ export const makeChatsSocket = (config) => {
812
722
  }
813
723
  })
814
724
 
815
- // ─── Return ───────────────────────────────────────────────────────────────────
725
+ registerSocketEndHandler(() => {
726
+ if (awaitingSyncTimeout) { clearTimeout(awaitingSyncTimeout); awaitingSyncTimeout = undefined }
727
+ if (!config.placeholderResendCache && placeholderResendCache.close) placeholderResendCache.close()
728
+ syncState = SyncState.Connecting
729
+ privacySettings = undefined
730
+ })
731
+
732
+ // ─── Return ───────────────────────────────────────────────────────────────
816
733
 
817
734
  return {
818
735
  ...sock,
736
+ serverProps,
819
737
  createCallLink,
820
738
  getBotListV2,
821
739
  processingMutex,
@@ -853,9 +771,9 @@ export const makeChatsSocket = (config) => {
853
771
  cleanDirtyBits,
854
772
  addOrEditContact,
855
773
  removeContact,
774
+ placeholderResendCache,
856
775
  addLabel,
857
776
  onWhatsApp,
858
- checkStatusWA,
859
777
  addChatLabel,
860
778
  removeChatLabel,
861
779
  addMessageLabel,
@@ -865,6 +783,7 @@ export const makeChatsSocket = (config) => {
865
783
  removeQuickReply
866
784
  }
867
785
  }
786
+
868
787
  export const checkStatusWA = async (phoneNumber) => {
869
788
  if (!phoneNumber) throw new Error('Please provide a phone number')
870
789
 
@@ -883,40 +802,29 @@ export const checkStatusWA = async (phoneNumber) => {
883
802
 
884
803
  const state = {
885
804
  creds: initAuthCreds(),
886
- keys: {
887
- get: async () => ({}),
888
- set: async () => { },
889
- transaction: async (fn) => fn(),
890
- }
805
+ keys: { get: async () => ({}), set: async () => { }, transaction: async (fn) => fn() }
891
806
  }
892
807
 
893
- const build = (status, isBanned, isNeedOfficialWa, banInfo = null) => ({
894
- number: formattedNumber, status, isBanned, isNeedOfficialWa, banInfo
895
- })
808
+ const build = (status, isBanned, isNeedOfficialWa, banInfo = null) => ({ number: formattedNumber, status, isBanned, isNeedOfficialWa, banInfo })
896
809
 
897
810
  try {
898
- await mobileRegisterExists({
899
- ...state.creds,
900
- phoneNumberCountryCode: countryCode,
901
- phoneNumberNationalNumber: nationalNumber,
902
- })
811
+ await mobileRegisterExists({ ...state.creds, phoneNumberCountryCode: countryCode, phoneNumberNationalNumber: nationalNumber })
903
812
  return build('active', false, false)
904
813
  } catch (err) {
905
814
  if (err?.appeal_token) {
906
815
  const banDetails = await getBanDetails(err.appeal_token)
907
816
  const appealStatus = banDetails?.status || null
908
817
  const banType = appealStatus === 'BANNED' ? 'permanent' : 'temporary'
909
-
910
818
  return build('banned', true, false, {
911
819
  banType,
912
820
  violationType: err.violation_type || null,
913
821
  violationReason: err.violation_type ? `Type ${err.violation_type}` : 'Unknown',
914
- canAppeal: banType !== 'permanent', // ✅ temporary = can appeal, permanent = cannot
822
+ canAppeal: banType !== 'permanent',
915
823
  appealToken: err.appeal_token,
916
824
  banTime: banDetails?.ban_time || null,
917
825
  banDate: banDetails?.ban_time ? new Date(banDetails.ban_time * 1000).toISOString() : null,
918
826
  appealStatus,
919
- appealCreatedAt: banDetails?.appeal_creation_time ? new Date(banDetails.appeal_creation_time * 1000).toISOString() : null,
827
+ appealCreatedAt: banDetails?.appeal_creation_time ? new Date(banDetails.appeal_creation_time * 1000).toISOString() : null
920
828
  })
921
829
  }
922
830
  if (err?.custom_block_screen) return build('blocked', false, true)
@@ -924,4 +832,4 @@ export const checkStatusWA = async (phoneNumber) => {
924
832
  if (err?.reason === 'temporarily_unavailable') return build('rate_limited', false, false)
925
833
  return build('error', false, false)
926
834
  }
927
- }
835
+ }