@badzz88/baileys 8.4.7 → 8.5.0

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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1055 -337
  3. package/WAProto/index.js +5 -5
  4. package/lib/Defaults/index.js +187 -127
  5. package/lib/Defaults/phonenumber-mcc.json +223 -0
  6. package/lib/Signal/Group/ciphertext-message.js +14 -11
  7. package/lib/Signal/Group/group-session-builder.js +91 -29
  8. package/lib/Signal/Group/group_cipher.js +88 -81
  9. package/lib/Signal/Group/index.js +136 -12
  10. package/lib/Signal/Group/keyhelper.js +70 -15
  11. package/lib/Signal/Group/sender-chain-key.js +31 -25
  12. package/lib/Signal/Group/sender-key-distribution-message.js +65 -62
  13. package/lib/Signal/Group/sender-key-message.js +68 -65
  14. package/lib/Signal/Group/sender-key-name.js +43 -41
  15. package/lib/Signal/Group/sender-key-record.js +43 -40
  16. package/lib/Signal/Group/sender-key-state.js +96 -83
  17. package/lib/Signal/Group/sender-message-key.js +29 -25
  18. package/lib/Signal/libsignal.js +483 -424
  19. package/lib/Signal/lid-mapping.js +261 -276
  20. package/lib/Socket/Client/index.js +30 -3
  21. package/lib/Socket/Client/types.js +12 -10
  22. package/lib/Socket/Client/websocket.js +61 -53
  23. package/lib/Socket/aigroups.js +240 -0
  24. package/lib/Socket/business.js +422 -379
  25. package/lib/Socket/chats.js +1816 -1193
  26. package/lib/Socket/communities.js +549 -431
  27. package/lib/Socket/graphql.js +656 -0
  28. package/lib/Socket/groups.js +764 -374
  29. package/lib/Socket/index.js +38 -11
  30. package/lib/Socket/interactive-handler.js +522 -0
  31. package/lib/Socket/interop.js +549 -0
  32. package/lib/Socket/luxu.js +6 -5
  33. package/lib/Socket/managed-account.js +183 -0
  34. package/lib/Socket/messages-recv.js +2439 -1915
  35. package/lib/Socket/messages-send.js +1642 -1242
  36. package/lib/Socket/mex.js +59 -40
  37. package/lib/Socket/newsletter.js +812 -249
  38. package/lib/Socket/privacy.js +449 -0
  39. package/lib/Socket/registration.js +427 -0
  40. package/lib/Socket/socket.js +1077 -965
  41. package/lib/Socket/text-router.js +83 -0
  42. package/lib/Socket/username.js +237 -140
  43. package/lib/Store/index.js +36 -10
  44. package/lib/Store/make-cache-manager-store.js +85 -80
  45. package/lib/Store/make-in-memory-store.js +591 -185
  46. package/lib/Store/make-ordered-dictionary.js +78 -72
  47. package/lib/Store/object-repository.js +25 -28
  48. package/lib/Types/Auth.js +38 -2
  49. package/lib/Types/Bussines.js +2 -2
  50. package/lib/Types/Call.js +2 -2
  51. package/lib/Types/Chat.js +4 -8
  52. package/lib/Types/Contact.js +2 -2
  53. package/lib/Types/Events.js +2 -2
  54. package/lib/Types/GroupMetadata.js +2 -2
  55. package/lib/Types/Label.js +26 -24
  56. package/lib/Types/LabelAssociation.js +8 -6
  57. package/lib/Types/Message.js +95 -11
  58. package/lib/Types/Mex.js +112 -37
  59. package/lib/Types/Newsletter.js +121 -0
  60. package/lib/Types/Product.js +2 -2
  61. package/lib/Types/Signal.js +2 -2
  62. package/lib/Types/Socket.js +2 -3
  63. package/lib/Types/State.js +70 -56
  64. package/lib/Types/USync.js +2 -2
  65. package/lib/Types/index.js +55 -26
  66. package/lib/Utils/auth-utils.js +292 -288
  67. package/lib/Utils/browser-utils.js +113 -48
  68. package/lib/Utils/business.js +240 -224
  69. package/lib/Utils/chat-utils.js +1205 -869
  70. package/lib/Utils/command-loader.d.ts +31 -0
  71. package/lib/Utils/command-loader.js +100 -0
  72. package/lib/Utils/companion-reg-client-utils.js +41 -34
  73. package/lib/Utils/consumer-application.js +107 -0
  74. package/lib/Utils/crypto.js +144 -107
  75. package/lib/Utils/curve25519-js.js +275 -0
  76. package/lib/Utils/decode-wa-message.js +518 -308
  77. package/lib/Utils/event-buffer.js +596 -611
  78. package/lib/Utils/generics.js +514 -354
  79. package/lib/Utils/group-history.js +60 -0
  80. package/lib/Utils/history.js +244 -134
  81. package/lib/Utils/identity-change-handler.js +51 -49
  82. package/lib/Utils/index.js +57 -23
  83. package/lib/Utils/jid-display-normalization.js +218 -0
  84. package/lib/Utils/link-preview.js +135 -77
  85. package/lib/Utils/logger.js +9 -3
  86. package/lib/Utils/lt-hash.js +37 -6
  87. package/lib/Utils/make-mutex.js +36 -33
  88. package/lib/Utils/message-composer.js +439 -233
  89. package/lib/Utils/message-retry-manager.js +265 -260
  90. package/lib/Utils/messages-media.js +855 -747
  91. package/lib/Utils/messages.js +2528 -884
  92. package/lib/Utils/meta-ai-msmsg.js +262 -0
  93. package/lib/Utils/native-bridge.js +82 -0
  94. package/lib/Utils/noise-handler.js +195 -200
  95. package/lib/Utils/offline-node-processor.js +35 -33
  96. package/lib/Utils/pre-key-manager.js +103 -102
  97. package/lib/Utils/process-message.js +978 -560
  98. package/lib/Utils/reporting-utils.js +257 -253
  99. package/lib/Utils/session-pool.d.ts +16 -0
  100. package/lib/Utils/session-pool.js +94 -0
  101. package/lib/Utils/signal.js +218 -195
  102. package/lib/Utils/stanza-ack.js +31 -29
  103. package/lib/Utils/sticker.d.ts +17 -0
  104. package/lib/Utils/sticker.js +145 -0
  105. package/lib/Utils/sync-action-utils.js +50 -45
  106. package/lib/Utils/tc-token-utils.js +137 -139
  107. package/lib/Utils/use-multi-file-auth-state.js +109 -109
  108. package/lib/Utils/validate-connection.js +218 -202
  109. package/lib/Utils/view-once-cache.d.ts +9 -0
  110. package/lib/Utils/view-once-cache.js +79 -0
  111. package/lib/Utils/voip-rekey.js +22 -0
  112. package/lib/WABinary/constants.js +1302 -1299
  113. package/lib/WABinary/decode.js +343 -262
  114. package/lib/WABinary/encode.js +12 -4
  115. package/lib/WABinary/generic-utils.js +223 -187
  116. package/lib/WABinary/index.js +33 -6
  117. package/lib/WABinary/jid-utils.js +351 -88
  118. package/lib/WABinary/types.js +2 -2
  119. package/lib/WAM/BinaryInfo.js +12 -9
  120. package/lib/WAM/constants.js +39486 -22853
  121. package/lib/WAM/encode.js +132 -140
  122. package/lib/WAM/index.js +31 -4
  123. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +55 -0
  124. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +43 -0
  125. package/lib/WAUSync/Protocols/USyncContactProtocol.js +53 -51
  126. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +56 -53
  127. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +29 -26
  128. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +59 -0
  129. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +31 -0
  130. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +32 -0
  131. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +29 -0
  132. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +38 -37
  133. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +38 -0
  134. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +26 -24
  135. package/lib/WAUSync/Protocols/index.js +39 -6
  136. package/lib/WAUSync/USyncQuery.js +132 -97
  137. package/lib/WAUSync/USyncUser.js +49 -30
  138. package/lib/WAUSync/index.js +31 -4
  139. package/lib/antiban.js +4637 -0
  140. package/lib/index.js +48 -15
  141. package/package.json +55 -92
  142. package/lib/WAUSync/Protocols/USyncNewsletterProtocol.js +0 -263
  143. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +0 -51
  144. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +0 -29
@@ -1,872 +1,1208 @@
1
- import { Boom } from '@hapi/boom';
2
- import { expandAppStateKeys } from 'whatsapp-rust-bridge';
3
- import { proto } from '../../WAProto/index.js';
4
- import { LabelAssociationType } from '../Types/LabelAssociation.js';
5
- import { getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary/index.js';
6
- import { aesDecrypt, aesEncrypt, hmacSign } from './crypto.js';
7
- import { toNumber } from './generics.js';
8
- import { LT_HASH_ANTI_TAMPERING } from './lt-hash.js';
9
- import { downloadContentFromMessage } from './messages-media.js';
10
- import { emitSyncActionResults, processContactAction } from './sync-action-utils.js';
11
- const mutationKeys = (keydata) => {
12
- const keys = expandAppStateKeys(keydata);
13
- return {
14
- indexKey: keys.indexKey,
15
- valueEncryptionKey: keys.valueEncryptionKey,
16
- valueMacKey: keys.valueMacKey,
17
- snapshotMacKey: keys.snapshotMacKey,
18
- patchMacKey: keys.patchMacKey
19
- };
20
- };
1
+ 'use strict'
2
+ Object.defineProperty(exports, '__esModule', { value: true })
3
+ exports.processSyncAction =
4
+ exports.chatModificationToAppPatch =
5
+ exports.decodePatches =
6
+ exports.decodeSyncdSnapshot =
7
+ exports.downloadExternalPatch =
8
+ exports.downloadExternalBlob =
9
+ exports.extractSyncdPatches =
10
+ exports.decodeSyncdPatch =
11
+ exports.decodeSyncdMutations =
12
+ exports.encodeSyncdPatch =
13
+ exports.newLTHashState =
14
+ void 0
15
+ const boom_1 = require('@hapi/boom')
16
+ const index_js_1 = require('../../WAProto/index.js')
17
+ const LabelAssociation_1 = require('../Types/LabelAssociation')
18
+ const WABinary_1 = require('../WABinary')
19
+ const crypto_1 = require('./crypto')
20
+ const generics_1 = require('./generics')
21
+ const lt_hash_1 = require('./lt-hash')
22
+ const messages_media_1 = require('./messages-media')
23
+ const sync_action_utils_1 = require('./sync-action-utils')
24
+ const mutationKeys = keydata => {
25
+ // app-state mutation keys are just HKDF-expanded sub-keys of a single 32-byte
26
+ // key, no elliptic-curve math involved -- this is the standard derivation
27
+ // used across the Baileys ecosystem.
28
+ const expanded = (0, crypto_1.hkdf)(keydata, 160, { info: 'WhatsApp Mutation Keys' })
29
+ return {
30
+ indexKey: expanded.subarray(0, 32),
31
+ valueEncryptionKey: expanded.subarray(32, 64),
32
+ valueMacKey: expanded.subarray(64, 96),
33
+ snapshotMacKey: expanded.subarray(96, 128),
34
+ patchMacKey: expanded.subarray(128, 160)
35
+ }
36
+ }
21
37
  const generateMac = (operation, data, keyId, key) => {
22
- const opByte = operation === proto.SyncdMutation.SyncdOperation.SET ? 0x01 : 0x02;
23
- const keyIdBuffer = typeof keyId === 'string' ? Buffer.from(keyId, 'base64') : keyId;
24
- const keyData = new Uint8Array(1 + keyIdBuffer.length);
25
- keyData[0] = opByte;
26
- keyData.set(keyIdBuffer, 1);
27
- const last = new Uint8Array(8);
28
- last[7] = keyData.length;
29
- const total = new Uint8Array(keyData.length + data.length + last.length);
30
- total.set(keyData, 0);
31
- total.set(data, keyData.length);
32
- total.set(last, keyData.length + data.length);
33
- const hmac = hmacSign(total, key, 'sha512');
34
- return hmac.subarray(0, 32);
35
- };
36
- const to64BitNetworkOrder = (e) => {
37
- const buff = Buffer.alloc(8);
38
- buff.writeUint32BE(e, 4);
39
- return buff;
40
- };
41
- export const makeLtHashGenerator = ({ indexValueMap, hash }) => {
42
- indexValueMap = { ...indexValueMap };
43
- const addBuffs = [];
44
- const subBuffs = [];
45
- return {
46
- mix: ({ indexMac, valueMac, operation }) => {
47
- const indexMacBase64 = Buffer.from(indexMac).toString('base64');
48
- const prevOp = indexValueMap[indexMacBase64];
49
- if (operation === proto.SyncdMutation.SyncdOperation.REMOVE) {
50
- if (!prevOp) {
51
- // WA Web does not throw here — it logs a warning and skips the subtract.
52
- // The missing REMOVE will cause an LTHash mismatch, which is handled
53
- // by the MAC validation layer (snapshot recovery or retry).
54
- return;
55
- }
56
- // remove from index value mac, since this mutation is erased
57
- delete indexValueMap[indexMacBase64];
58
- }
59
- else {
60
- addBuffs.push(valueMac);
61
- // add this index into the history map
62
- indexValueMap[indexMacBase64] = { valueMac };
63
- }
64
- if (prevOp) {
65
- subBuffs.push(prevOp.valueMac);
66
- }
67
- },
68
- finish: () => {
69
- const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(hash, subBuffs, addBuffs);
70
- return {
71
- hash: Buffer.from(result),
72
- indexValueMap
73
- };
74
- }
75
- };
76
- };
38
+ const opByte = operation === index_js_1.proto.SyncdMutation.SyncdOperation.SET ? 0x01 : 0x02
39
+ const keyIdBuffer = typeof keyId === 'string' ? Buffer.from(keyId, 'base64') : keyId
40
+ const keyData = new Uint8Array(1 + keyIdBuffer.length)
41
+ keyData[0] = opByte
42
+ keyData.set(keyIdBuffer, 1)
43
+ const last = new Uint8Array(8)
44
+ last[7] = keyData.length
45
+ const total = new Uint8Array(keyData.length + data.length + last.length)
46
+ total.set(keyData, 0)
47
+ total.set(data, keyData.length)
48
+ total.set(last, keyData.length + data.length)
49
+ const hmac = (0, crypto_1.hmacSign)(total, key, 'sha512')
50
+ return hmac.subarray(0, 32)
51
+ }
52
+ const to64BitNetworkOrder = e => {
53
+ const buff = Buffer.alloc(8)
54
+ buff.writeUint32BE(e, 4)
55
+ return buff
56
+ }
57
+ const makeLtHashGenerator = ({ indexValueMap, hash }) => {
58
+ indexValueMap = { ...indexValueMap }
59
+ const addBuffs = []
60
+ const subBuffs = []
61
+ return {
62
+ mix: ({ indexMac, valueMac, operation }) => {
63
+ const indexMacBase64 = Buffer.from(indexMac).toString('base64')
64
+ const prevOp = indexValueMap[indexMacBase64]
65
+ if (operation === index_js_1.proto.SyncdMutation.SyncdOperation.REMOVE) {
66
+ if (!prevOp) {
67
+ throw new boom_1.Boom('tried remove, but no previous op', { data: { indexMac, valueMac } })
68
+ }
69
+ // remove from index value mac, since this mutation is erased
70
+ delete indexValueMap[indexMacBase64]
71
+ } else {
72
+ addBuffs.push(valueMac)
73
+ // add this index into the history map
74
+ indexValueMap[indexMacBase64] = { valueMac }
75
+ }
76
+ if (prevOp) {
77
+ subBuffs.push(prevOp.valueMac)
78
+ }
79
+ },
80
+ finish: () => {
81
+ const result = lt_hash_1.LT_HASH_ANTI_TAMPERING.subtractThenAdd(hash, subBuffs, addBuffs)
82
+ return {
83
+ hash: Buffer.from(result),
84
+ indexValueMap
85
+ }
86
+ }
87
+ }
88
+ }
77
89
  const generateSnapshotMac = (lthash, version, name, key) => {
78
- const total = Buffer.concat([lthash, to64BitNetworkOrder(version), Buffer.from(name, 'utf-8')]);
79
- return hmacSign(total, key, 'sha256');
80
- };
90
+ const total = Buffer.concat([lthash, to64BitNetworkOrder(version), Buffer.from(name, 'utf-8')])
91
+ return (0, crypto_1.hmacSign)(total, key, 'sha256')
92
+ }
81
93
  const generatePatchMac = (snapshotMac, valueMacs, version, type, key) => {
82
- const total = Buffer.concat([snapshotMac, ...valueMacs, to64BitNetworkOrder(version), Buffer.from(type, 'utf-8')]);
83
- return hmacSign(total, key);
84
- };
85
- export const newLTHashState = () => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} });
86
- export const ensureLTHashStateVersion = (state) => {
87
- if (typeof state.version !== 'number' || isNaN(state.version)) {
88
- state.version = 0;
89
- }
90
- return state;
91
- };
92
- export const MAX_SYNC_ATTEMPTS = 2;
93
- /**
94
- * Check if an error is a missing app state sync key.
95
- * WA Web treats these as "Blocked" (waits for key arrival), not fatal.
96
- * In Baileys we retry with a snapshot which may use a different key.
97
- */
98
- export const isMissingKeyError = (error) => {
99
- return error?.data?.isMissingKey === true;
100
- };
101
- /**
102
- * Determines if an app state sync error is unrecoverable.
103
- * TypeError indicates a WASM crash; otherwise we give up after MAX_SYNC_ATTEMPTS.
104
- * Missing keys are NOT checked here — they are handled separately as "Blocked".
105
- */
106
- export const isAppStateSyncIrrecoverable = (error, attempts) => {
107
- return attempts >= MAX_SYNC_ATTEMPTS || error?.name === 'TypeError';
108
- };
109
- export const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, operation }, myAppStateKeyId, state, getAppStateSyncKey) => {
110
- const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined;
111
- if (!key) {
112
- throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { data: { isMissingKey: true } });
113
- }
114
- const encKeyId = Buffer.from(myAppStateKeyId, 'base64');
115
- state = { ...state, indexValueMap: { ...state.indexValueMap } };
116
- const indexBuffer = Buffer.from(JSON.stringify(index));
117
- const dataProto = proto.SyncActionData.fromObject({
118
- index: indexBuffer,
119
- value: syncAction,
120
- padding: new Uint8Array(0),
121
- version: apiVersion
122
- });
123
- const encoded = proto.SyncActionData.encode(dataProto).finish();
124
- const keyValue = mutationKeys(key.keyData);
125
- const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey);
126
- const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey);
127
- const indexMac = hmacSign(indexBuffer, keyValue.indexKey);
128
- // update LT hash
129
- const generator = makeLtHashGenerator(state);
130
- generator.mix({ indexMac, valueMac, operation });
131
- Object.assign(state, generator.finish());
132
- state.version += 1;
133
- const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey);
134
- const patch = {
135
- patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey),
136
- snapshotMac: snapshotMac,
137
- keyId: { id: encKeyId },
138
- mutations: [
139
- {
140
- operation: operation,
141
- record: {
142
- index: {
143
- blob: indexMac
144
- },
145
- value: {
146
- blob: Buffer.concat([encValue, valueMac])
147
- },
148
- keyId: { id: encKeyId }
149
- }
150
- }
151
- ]
152
- };
153
- const base64Index = indexMac.toString('base64');
154
- state.indexValueMap[base64Index] = { valueMac };
155
- return { patch, state };
156
- };
157
- export const decodeSyncdMutations = async (msgMutations, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
158
- const ltGenerator = makeLtHashGenerator(initialState);
159
- const derivedKeyCache = new Map();
160
- // indexKey used to HMAC sign record.index.blob
161
- // valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]
162
- // the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId
163
- for (const msgMutation of msgMutations) {
164
- // if it's a syncdmutation, get the operation property
165
- // otherwise, if it's only a record -- it'll be a SET mutation
166
- const operation = 'operation' in msgMutation ? msgMutation.operation : proto.SyncdMutation.SyncdOperation.SET;
167
- const record = 'record' in msgMutation && !!msgMutation.record ? msgMutation.record : msgMutation;
168
- let key;
169
- try {
170
- key = await getKey(record.keyId.id);
171
- }
172
- catch (err) {
173
- // Missing-key errors must propagate so the orchestrator can park the
174
- // collection (Blocked) and retry when APP_STATE_SYNC_KEY_SHARE arrives.
175
- // Other errors → individual record corruption, skip and keep going.
176
- if (isMissingKeyError(err))
177
- throw err;
178
- continue;
179
- }
180
- const content = record.value.blob;
181
- const encContent = content.subarray(0, -32);
182
- const ogValueMac = content.subarray(-32);
183
- if (validateMacs) {
184
- const contentHmac = generateMac(operation, encContent, record.keyId.id, key.valueMacKey);
185
- if (Buffer.compare(contentHmac, ogValueMac) !== 0) {
186
- // HMAC verification failed — skip this record
187
- continue;
188
- }
189
- }
190
- let result;
191
- try {
192
- result = aesDecrypt(encContent, key.valueEncryptionKey);
193
- }
194
- catch {
195
- // decrypt failed — skip this record instead of aborting
196
- continue;
197
- }
198
- const syncAction = proto.SyncActionData.decode(result);
199
- if (validateMacs) {
200
- const hmac = hmacSign(syncAction.index, key.indexKey);
201
- if (Buffer.compare(hmac, record.index.blob) !== 0) {
202
- throw new Boom('HMAC index verification failed');
203
- }
204
- }
205
- const indexStr = Buffer.from(syncAction.index).toString();
206
- onMutation({ syncAction, index: JSON.parse(indexStr) });
207
- ltGenerator.mix({
208
- indexMac: record.index.blob,
209
- valueMac: ogValueMac,
210
- operation: operation
211
- });
212
- }
213
- return ltGenerator.finish();
214
- async function getKey(keyId) {
215
- const base64Key = Buffer.from(keyId).toString('base64');
216
- const cached = derivedKeyCache.get(base64Key);
217
- if (cached) {
218
- return cached;
219
- }
220
- const keyEnc = await getAppStateSyncKey(base64Key);
221
- if (!keyEnc) {
222
- throw new Boom(`failed to find key "${base64Key}" to decode mutation`, {
223
- data: { isMissingKey: true, msgMutations }
224
- });
225
- }
226
- const keys = mutationKeys(keyEnc.keyData);
227
- derivedKeyCache.set(base64Key, keys);
228
- return keys;
229
- }
230
- };
231
- export const decodeSyncdPatch = async (msg, name, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
232
- if (validateMacs) {
233
- const base64Key = Buffer.from(msg.keyId.id).toString('base64');
234
- const mainKeyObj = await getAppStateSyncKey(base64Key);
235
- if (!mainKeyObj) {
236
- throw new Boom(`failed to find key "${base64Key}" to decode patch`, { data: { isMissingKey: true, msg } });
237
- }
238
- const mainKey = mutationKeys(mainKeyObj.keyData);
239
- const mutationmacs = msg.mutations.map(mutation => mutation.record.value.blob.slice(-32));
240
- const patchMac = generatePatchMac(msg.snapshotMac, mutationmacs, toNumber(msg.version.version), name, mainKey.patchMacKey);
241
- if (Buffer.compare(patchMac, msg.patchMac) !== 0) {
242
- throw new Boom('Invalid patch mac');
243
- }
244
- }
245
- const result = await decodeSyncdMutations(msg.mutations, initialState, getAppStateSyncKey, onMutation, validateMacs);
246
- return result;
247
- };
248
- export const extractSyncdPatches = async (result, options) => {
249
- const syncNode = getBinaryNodeChild(result, 'sync');
250
- const collectionNodes = getBinaryNodeChildren(syncNode, 'collection');
251
- const final = {};
252
- await Promise.all(collectionNodes.map(async (collectionNode) => {
253
- const patchesNode = getBinaryNodeChild(collectionNode, 'patches');
254
- const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch');
255
- const snapshotNode = getBinaryNodeChild(collectionNode, 'snapshot');
256
- const syncds = [];
257
- const name = collectionNode.attrs.name;
258
- const hasMorePatches = collectionNode.attrs.has_more_patches === 'true';
259
- let snapshot = undefined;
260
- if (snapshotNode && !!snapshotNode.content) {
261
- if (!Buffer.isBuffer(snapshotNode)) {
262
- snapshotNode.content = Buffer.from(Object.values(snapshotNode.content));
263
- }
264
- const blobRef = proto.ExternalBlobReference.decode(snapshotNode.content);
265
- const data = await downloadExternalBlob(blobRef, options);
266
- snapshot = proto.SyncdSnapshot.decode(data);
267
- }
268
- for (let { content } of patches) {
269
- if (content) {
270
- if (!Buffer.isBuffer(content)) {
271
- content = Buffer.from(Object.values(content));
272
- }
273
- const syncd = proto.SyncdPatch.decode(content);
274
- if (!syncd.version) {
275
- syncd.version = { version: +collectionNode.attrs.version + 1 };
276
- }
277
- syncds.push(syncd);
278
- }
279
- }
280
- final[name] = { patches: syncds, hasMorePatches, snapshot };
281
- }));
282
- return final;
283
- };
284
- export const downloadExternalBlob = async (blob, options) => {
285
- const stream = await downloadContentFromMessage(blob, 'md-app-state', { options });
286
- const bufferArray = [];
287
- for await (const chunk of stream) {
288
- bufferArray.push(chunk);
289
- }
290
- return Buffer.concat(bufferArray);
291
- };
292
- export const downloadExternalPatch = async (blob, options) => {
293
- const buffer = await downloadExternalBlob(blob, options);
294
- const syncData = proto.SyncdMutations.decode(buffer);
295
- return syncData;
296
- };
297
- export const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, minimumVersionNumber, validateMacs = true, logger) => {
298
- const newState = newLTHashState();
299
- newState.version = toNumber(snapshot.version.version);
300
- const mutationMap = {};
301
- const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber;
302
- const { hash, indexValueMap } = await decodeSyncdMutations(snapshot.records, newState, getAppStateSyncKey, areMutationsRequired
303
- ? mutation => {
304
- const index = mutation.syncAction.index?.toString();
305
- mutationMap[index] = mutation;
306
- }
307
- : () => { }, validateMacs);
308
- newState.hash = hash;
309
- newState.indexValueMap = indexValueMap;
310
- if (validateMacs) {
311
- const base64Key = Buffer.from(snapshot.keyId.id).toString('base64');
312
- const keyEnc = await getAppStateSyncKey(base64Key);
313
- if (!keyEnc) {
314
- throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } });
315
- }
316
- const result = mutationKeys(keyEnc.keyData);
317
- const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);
318
- if (Buffer.compare(snapshot.mac, computedSnapshotMac) !== 0) {
319
- // LTHash verification may fail when decodeSyncdMutations skipped undecryptable
320
- // records (poisoned server-side snapshot); the aggregate client hash diverges
321
- // from the server-computed mac. Fall through with a warning so the session stays
322
- // alive with partial state, symmetric to how decodePatches handles its own
323
- // LTHash mismatch a few lines below.
324
- logger?.warn({ name, version: newState.version }, 'LTHash verification failed on snapshot, continuing with partial state');
325
- }
326
- }
327
- return {
328
- state: newState,
329
- mutationMap
330
- };
331
- };
332
- export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, options, minimumVersionNumber, logger, validateMacs = true) => {
333
- const newState = {
334
- ...initial,
335
- indexValueMap: { ...initial.indexValueMap }
336
- };
337
- const mutationMap = {};
338
- for (const syncd of syncds) {
339
- const { version, keyId, snapshotMac } = syncd;
340
- if (syncd.externalMutations) {
341
- logger?.trace({ name, version }, 'downloading external patch');
342
- const ref = await downloadExternalPatch(syncd.externalMutations, options);
343
- logger?.debug({ name, version, mutations: ref.mutations.length }, 'downloaded external patch');
344
- syncd.mutations?.push(...ref.mutations);
345
- }
346
- const patchVersion = toNumber(version.version);
347
- newState.version = patchVersion;
348
- const shouldMutate = typeof minimumVersionNumber === 'undefined' || patchVersion > minimumVersionNumber;
349
- let decodeResult;
350
- try {
351
- decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, shouldMutate
352
- ? mutation => {
353
- const index = mutation.syncAction.index?.toString();
354
- mutationMap[index] = mutation;
355
- }
356
- : () => { }, validateMacs);
357
- }
358
- catch (err) {
359
- if (isMissingKeyError(err))
360
- throw err;
361
- logger?.warn({ name, version: patchVersion, error: err.message }, 'failed to decode patch, skipping');
362
- continue;
363
- }
364
- newState.hash = decodeResult.hash;
365
- newState.indexValueMap = decodeResult.indexValueMap;
366
- if (validateMacs) {
367
- const base64Key = Buffer.from(keyId.id).toString('base64');
368
- const keyEnc = await getAppStateSyncKey(base64Key);
369
- if (!keyEnc) {
370
- throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } });
371
- }
372
- const result = mutationKeys(keyEnc.keyData);
373
- const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);
374
- if (Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
375
- logger?.warn({ name, version: newState.version }, 'LTHash verification failed, skipping remaining patches');
376
- break;
377
- }
378
- }
379
- // clear memory used up by the mutations
380
- syncd.mutations = [];
381
- }
382
- return { state: newState, mutationMap };
383
- };
384
- export const chatModificationToAppPatch = (mod, jid) => {
385
- const OP = proto.SyncdMutation.SyncdOperation;
386
- const getMessageRange = (lastMessages) => {
387
- let messageRange;
388
- if (Array.isArray(lastMessages)) {
389
- const lastMsg = lastMessages[lastMessages.length - 1];
390
- messageRange = {
391
- lastMessageTimestamp: lastMsg?.messageTimestamp,
392
- messages: lastMessages?.length
393
- ? lastMessages.map(m => {
394
- if (!m.key?.id || !m.key?.remoteJid) {
395
- throw new Boom('Incomplete key', { statusCode: 400, data: m });
396
- }
397
- if (isJidGroup(m.key.remoteJid) && !m.key.fromMe && !m.key.participant) {
398
- throw new Boom('Expected not from me message to have participant', { statusCode: 400, data: m });
399
- }
400
- if (!m.messageTimestamp || !toNumber(m.messageTimestamp)) {
401
- throw new Boom('Missing timestamp in last message list', { statusCode: 400, data: m });
402
- }
403
- if (m.key.participant) {
404
- m.key.participant = jidNormalizedUser(m.key.participant);
405
- }
406
- return m;
407
- })
408
- : undefined
409
- };
410
- }
411
- else {
412
- messageRange = lastMessages;
413
- }
414
- return messageRange;
415
- };
416
- let patch;
417
- if ('mute' in mod) {
418
- patch = {
419
- syncAction: {
420
- muteAction: {
421
- muted: !!mod.mute,
422
- muteEndTimestamp: mod.mute || undefined
423
- }
424
- },
425
- index: ['mute', jid],
426
- type: 'regular_high',
427
- apiVersion: 2,
428
- operation: OP.SET
429
- };
430
- }
431
- else if ('archive' in mod) {
432
- patch = {
433
- syncAction: {
434
- archiveChatAction: {
435
- archived: !!mod.archive,
436
- messageRange: getMessageRange(mod.lastMessages)
437
- }
438
- },
439
- index: ['archive', jid],
440
- type: 'regular_low',
441
- apiVersion: 3,
442
- operation: OP.SET
443
- };
444
- }
445
- else if ('markRead' in mod) {
446
- patch = {
447
- syncAction: {
448
- markChatAsReadAction: {
449
- read: mod.markRead,
450
- messageRange: getMessageRange(mod.lastMessages)
451
- }
452
- },
453
- index: ['markChatAsRead', jid],
454
- type: 'regular_low',
455
- apiVersion: 3,
456
- operation: OP.SET
457
- };
458
- }
459
- else if ('deleteForMe' in mod) {
460
- const { timestamp, key, deleteMedia } = mod.deleteForMe;
461
- patch = {
462
- syncAction: {
463
- deleteMessageForMeAction: {
464
- deleteMedia,
465
- messageTimestamp: timestamp
466
- }
467
- },
468
- index: ['deleteMessageForMe', jid, key.id, key.fromMe ? '1' : '0', '0'],
469
- type: 'regular_high',
470
- apiVersion: 3,
471
- operation: OP.SET
472
- };
473
- }
474
- else if ('clear' in mod) {
475
- patch = {
476
- syncAction: {
477
- clearChatAction: {
478
- messageRange: getMessageRange(mod.lastMessages)
479
- }
480
- },
481
- index: ['clearChat', jid, '1' /*the option here is 0 when keep starred messages is enabled*/, '0'],
482
- type: 'regular_high',
483
- apiVersion: 6,
484
- operation: OP.SET
485
- };
486
- }
487
- else if ('pin' in mod) {
488
- patch = {
489
- syncAction: {
490
- pinAction: {
491
- pinned: !!mod.pin
492
- }
493
- },
494
- index: ['pin_v1', jid],
495
- type: 'regular_low',
496
- apiVersion: 5,
497
- operation: OP.SET
498
- };
499
- }
500
- else if ('contact' in mod) {
501
- patch = {
502
- syncAction: {
503
- contactAction: mod.contact || {}
504
- },
505
- index: ['contact', jid],
506
- type: 'critical_unblock_low',
507
- apiVersion: 2,
508
- operation: mod.contact ? OP.SET : OP.REMOVE
509
- };
510
- }
511
- else if ('disableLinkPreviews' in mod) {
512
- patch = {
513
- syncAction: {
514
- privacySettingDisableLinkPreviewsAction: mod.disableLinkPreviews || {}
515
- },
516
- index: ['setting_disableLinkPreviews'],
517
- type: 'regular',
518
- apiVersion: 8,
519
- operation: OP.SET
520
- };
521
- }
522
- else if ('star' in mod) {
523
- const key = mod.star.messages[0];
524
- patch = {
525
- syncAction: {
526
- starAction: {
527
- starred: !!mod.star.star
528
- }
529
- },
530
- index: ['star', jid, key.id, key.fromMe ? '1' : '0', '0'],
531
- type: 'regular_low',
532
- apiVersion: 2,
533
- operation: OP.SET
534
- };
535
- }
536
- else if ('delete' in mod) {
537
- patch = {
538
- syncAction: {
539
- deleteChatAction: {
540
- messageRange: getMessageRange(mod.lastMessages)
541
- }
542
- },
543
- index: ['deleteChat', jid, '1'],
544
- type: 'regular_high',
545
- apiVersion: 6,
546
- operation: OP.SET
547
- };
548
- }
549
- else if ('pushNameSetting' in mod) {
550
- patch = {
551
- syncAction: {
552
- pushNameSetting: {
553
- name: mod.pushNameSetting
554
- }
555
- },
556
- index: ['setting_pushName'],
557
- type: 'critical_block',
558
- apiVersion: 1,
559
- operation: OP.SET
560
- };
561
- }
562
- else if ('quickReply' in mod) {
563
- patch = {
564
- syncAction: {
565
- quickReplyAction: {
566
- count: 0,
567
- deleted: mod.quickReply.deleted || false,
568
- keywords: [],
569
- message: mod.quickReply.message || '',
570
- shortcut: mod.quickReply.shortcut || ''
571
- }
572
- },
573
- index: ['quick_reply', mod.quickReply.timestamp || String(Math.floor(Date.now() / 1000))],
574
- type: 'regular',
575
- apiVersion: 2,
576
- operation: OP.SET
577
- };
578
- }
579
- else if ('addLabel' in mod) {
580
- patch = {
581
- syncAction: {
582
- labelEditAction: {
583
- name: mod.addLabel.name,
584
- color: mod.addLabel.color,
585
- predefinedId: mod.addLabel.predefinedId,
586
- deleted: mod.addLabel.deleted
587
- }
588
- },
589
- index: ['label_edit', mod.addLabel.id],
590
- type: 'regular',
591
- apiVersion: 3,
592
- operation: OP.SET
593
- };
594
- }
595
- else if ('addChatLabel' in mod) {
596
- patch = {
597
- syncAction: {
598
- labelAssociationAction: {
599
- labeled: true
600
- }
601
- },
602
- index: [LabelAssociationType.Chat, mod.addChatLabel.labelId, jid],
603
- type: 'regular',
604
- apiVersion: 3,
605
- operation: OP.SET
606
- };
607
- }
608
- else if ('removeChatLabel' in mod) {
609
- patch = {
610
- syncAction: {
611
- labelAssociationAction: {
612
- labeled: false
613
- }
614
- },
615
- index: [LabelAssociationType.Chat, mod.removeChatLabel.labelId, jid],
616
- type: 'regular',
617
- apiVersion: 3,
618
- operation: OP.SET
619
- };
620
- }
621
- else if ('addMessageLabel' in mod) {
622
- patch = {
623
- syncAction: {
624
- labelAssociationAction: {
625
- labeled: true
626
- }
627
- },
628
- index: [LabelAssociationType.Message, mod.addMessageLabel.labelId, jid, mod.addMessageLabel.messageId, '0', '0'],
629
- type: 'regular',
630
- apiVersion: 3,
631
- operation: OP.SET
632
- };
633
- }
634
- else if ('removeMessageLabel' in mod) {
635
- patch = {
636
- syncAction: {
637
- labelAssociationAction: {
638
- labeled: false
639
- }
640
- },
641
- index: [
642
- LabelAssociationType.Message,
643
- mod.removeMessageLabel.labelId,
644
- jid,
645
- mod.removeMessageLabel.messageId,
646
- '0',
647
- '0'
648
- ],
649
- type: 'regular',
650
- apiVersion: 3,
651
- operation: OP.SET
652
- };
653
- }
654
- else {
655
- throw new Boom('not supported');
656
- }
657
- patch.syncAction.timestamp = Date.now();
658
- return patch;
659
- };
660
- export const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) => {
661
- const isInitialSync = !!initialSyncOpts;
662
- const accountSettings = initialSyncOpts?.accountSettings;
663
- logger?.trace({ syncAction, initialSync: !!initialSyncOpts }, 'processing sync action');
664
- const { syncAction: { value: action }, index: [type, id, msgId, fromMe] } = syncAction;
665
- if (action?.muteAction) {
666
- ev.emit('chats.update', [
667
- {
668
- id,
669
- muteEndTime: action.muteAction?.muted ? toNumber(action.muteAction.muteEndTimestamp) : null,
670
- conditional: getChatUpdateConditional(id, undefined)
671
- }
672
- ]);
673
- }
674
- else if (action?.archiveChatAction || type === 'archive' || type === 'unarchive') {
675
- // okay so we've to do some annoying computation here
676
- // when we're initially syncing the app state
677
- // there are a few cases we need to handle
678
- // 1. if the account unarchiveChats setting is true
679
- // a. if the chat is archived, and no further messages have been received -- simple, keep archived
680
- // b. if the chat was archived, and the user received messages from the other person afterwards
681
- // then the chat should be marked unarchved --
682
- // we compare the timestamp of latest message from the other person to determine this
683
- // 2. if the account unarchiveChats setting is false -- then it doesn't matter,
684
- // it'll always take an app state action to mark in unarchived -- which we'll get anyway
685
- const archiveAction = action?.archiveChatAction;
686
- const isArchived = archiveAction ? archiveAction.archived : type === 'archive';
687
- // // basically we don't need to fire an "archive" update if the chat is being marked unarchvied
688
- // // this only applies for the initial sync
689
- // if(isInitialSync && !isArchived) {
690
- // isArchived = false
691
- // }
692
- const msgRange = !accountSettings?.unarchiveChats ? undefined : archiveAction?.messageRange;
693
- // logger?.debug({ chat: id, syncAction }, 'message range archive')
694
- ev.emit('chats.update', [
695
- {
696
- id,
697
- archived: isArchived,
698
- conditional: getChatUpdateConditional(id, msgRange)
699
- }
700
- ]);
701
- }
702
- else if (action?.markChatAsReadAction) {
703
- const markReadAction = action.markChatAsReadAction;
704
- // basically we don't need to fire an "read" update if the chat is being marked as read
705
- // because the chat is read by default
706
- // this only applies for the initial sync
707
- const isNullUpdate = isInitialSync && markReadAction.read;
708
- ev.emit('chats.update', [
709
- {
710
- id,
711
- unreadCount: isNullUpdate ? null : !!markReadAction?.read ? 0 : -1,
712
- conditional: getChatUpdateConditional(id, markReadAction?.messageRange)
713
- }
714
- ]);
715
- }
716
- else if (action?.deleteMessageForMeAction || type === 'deleteMessageForMe') {
717
- ev.emit('messages.delete', {
718
- keys: [
719
- {
720
- remoteJid: id,
721
- id: msgId,
722
- fromMe: fromMe === '1'
723
- }
724
- ]
725
- });
726
- }
727
- else if (action?.contactAction) {
728
- const results = processContactAction(action.contactAction, id, logger);
729
- emitSyncActionResults(ev, results);
730
- }
731
- else if (action?.pushNameSetting) {
732
- const name = action?.pushNameSetting?.name;
733
- if (name && me?.name !== name) {
734
- ev.emit('creds.update', { me: { ...me, name } });
735
- }
736
- }
737
- else if (action?.pinAction) {
738
- ev.emit('chats.update', [
739
- {
740
- id,
741
- pinned: action.pinAction?.pinned ? toNumber(action.timestamp) : null,
742
- conditional: getChatUpdateConditional(id, undefined)
743
- }
744
- ]);
745
- }
746
- else if (action?.unarchiveChatsSetting) {
747
- const unarchiveChats = !!action.unarchiveChatsSetting.unarchiveChats;
748
- ev.emit('creds.update', { accountSettings: { unarchiveChats } });
749
- logger?.info(`archive setting updated => '${action.unarchiveChatsSetting.unarchiveChats}'`);
750
- if (accountSettings) {
751
- accountSettings.unarchiveChats = unarchiveChats;
752
- }
753
- }
754
- else if (action?.starAction || type === 'star') {
755
- let starred = action?.starAction?.starred;
756
- if (typeof starred !== 'boolean') {
757
- starred = syncAction.index[syncAction.index.length - 1] === '1';
758
- }
759
- ev.emit('messages.update', [
760
- {
761
- key: { remoteJid: id, id: msgId, fromMe: fromMe === '1' },
762
- update: { starred }
763
- }
764
- ]);
765
- }
766
- else if (action?.deleteChatAction || type === 'deleteChat') {
767
- if (!isInitialSync) {
768
- ev.emit('chats.delete', [id]);
769
- }
770
- }
771
- else if (action?.labelEditAction) {
772
- const { name, color, deleted, predefinedId } = action.labelEditAction;
773
- ev.emit('labels.edit', {
774
- id: id,
775
- name: name,
776
- color: color,
777
- deleted: deleted,
778
- predefinedId: predefinedId ? String(predefinedId) : undefined
779
- });
780
- }
781
- else if (action?.labelAssociationAction) {
782
- ev.emit('labels.association', {
783
- type: action.labelAssociationAction.labeled ? 'add' : 'remove',
784
- association: type === LabelAssociationType.Chat
785
- ? {
786
- type: LabelAssociationType.Chat,
787
- chatId: syncAction.index[2],
788
- labelId: syncAction.index[1]
789
- }
790
- : {
791
- type: LabelAssociationType.Message,
792
- chatId: syncAction.index[2],
793
- messageId: syncAction.index[3],
794
- labelId: syncAction.index[1]
795
- }
796
- });
797
- }
798
- else if (action?.localeSetting?.locale) {
799
- ev.emit('settings.update', { setting: 'locale', value: action.localeSetting.locale });
800
- }
801
- else if (action?.timeFormatAction) {
802
- ev.emit('settings.update', { setting: 'timeFormat', value: action.timeFormatAction });
803
- }
804
- else if (action?.pnForLidChatAction) {
805
- if (action.pnForLidChatAction.pnJid) {
806
- ev.emit('lid-mapping.update', { lid: id, pn: action.pnForLidChatAction.pnJid });
807
- }
808
- }
809
- else if (action?.privacySettingRelayAllCalls) {
810
- ev.emit('settings.update', {
811
- setting: 'privacySettingRelayAllCalls',
812
- value: action.privacySettingRelayAllCalls
813
- });
814
- }
815
- else if (action?.statusPrivacy) {
816
- ev.emit('settings.update', { setting: 'statusPrivacy', value: action.statusPrivacy });
817
- }
818
- else if (action?.lockChatAction) {
819
- ev.emit('chats.lock', { id: id, locked: !!action.lockChatAction.locked });
820
- }
821
- else if (action?.privacySettingDisableLinkPreviewsAction) {
822
- ev.emit('settings.update', {
823
- setting: 'disableLinkPreviews',
824
- value: action.privacySettingDisableLinkPreviewsAction
825
- });
826
- }
827
- else if (action?.notificationActivitySettingAction?.notificationActivitySetting) {
828
- ev.emit('settings.update', {
829
- setting: 'notificationActivitySetting',
830
- value: action.notificationActivitySettingAction.notificationActivitySetting
831
- });
832
- }
833
- else if (action?.lidContactAction) {
834
- ev.emit('contacts.upsert', [
835
- {
836
- id: id,
837
- name: action.lidContactAction.fullName ||
838
- action.lidContactAction.firstName ||
839
- action.lidContactAction.username ||
840
- undefined,
841
- username: action.lidContactAction.username || undefined,
842
- lid: id,
843
- phoneNumber: undefined
844
- }
845
- ]);
846
- }
847
- else if (action?.privacySettingChannelsPersonalisedRecommendationAction) {
848
- ev.emit('settings.update', {
849
- setting: 'channelsPersonalisedRecommendation',
850
- value: action.privacySettingChannelsPersonalisedRecommendationAction
851
- });
852
- }
853
- else {
854
- logger?.debug({ syncAction, id }, 'unprocessable update');
855
- }
856
- function getChatUpdateConditional(id, msgRange) {
857
- return isInitialSync
858
- ? data => {
859
- const chat = data.historySets.chats[id] || data.chatUpserts[id];
860
- if (chat) {
861
- return msgRange ? isValidPatchBasedOnMessageRange(chat, msgRange) : true;
862
- }
863
- }
864
- : undefined;
865
- }
866
- function isValidPatchBasedOnMessageRange(chat, msgRange) {
867
- const lastMsgTimestamp = Number(msgRange?.lastMessageTimestamp || msgRange?.lastSystemMessageTimestamp || 0);
868
- const chatLastMsgTimestamp = Number(chat?.lastMessageRecvTimestamp || 0);
869
- return lastMsgTimestamp >= chatLastMsgTimestamp;
870
- }
871
- };
872
- //# sourceMappingURL=chat-utils.js.map
94
+ const total = Buffer.concat([snapshotMac, ...valueMacs, to64BitNetworkOrder(version), Buffer.from(type, 'utf-8')])
95
+ return (0, crypto_1.hmacSign)(total, key)
96
+ }
97
+ const newLTHashState = () => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} })
98
+ exports.newLTHashState = newLTHashState
99
+ exports.makeLtHashGenerator = makeLtHashGenerator
100
+ const ensureLTHashStateVersion = state => {
101
+ if (typeof state.version !== 'number' || isNaN(state.version)) {
102
+ state.version = 0
103
+ }
104
+ return state
105
+ }
106
+ exports.ensureLTHashStateVersion = ensureLTHashStateVersion
107
+ exports.MAX_SYNC_ATTEMPTS = 2
108
+ const isMissingKeyError = error => {
109
+ return error?.data?.isMissingKey === true
110
+ }
111
+ exports.isMissingKeyError = isMissingKeyError
112
+ const isAppStateSyncIrrecoverable = (error, attempts) => {
113
+ return attempts >= exports.MAX_SYNC_ATTEMPTS || error?.name === 'TypeError'
114
+ }
115
+ exports.isAppStateSyncIrrecoverable = isAppStateSyncIrrecoverable
116
+ const encodeSyncdPatch = async (
117
+ { type, index, syncAction, apiVersion, operation },
118
+ myAppStateKeyId,
119
+ state,
120
+ getAppStateSyncKey
121
+ ) => {
122
+ const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined
123
+ if (!key) {
124
+ throw new boom_1.Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 })
125
+ }
126
+ const encKeyId = Buffer.from(myAppStateKeyId, 'base64')
127
+ state = { ...state, indexValueMap: { ...state.indexValueMap } }
128
+ const indexBuffer = Buffer.from(JSON.stringify(index))
129
+ const dataProto = index_js_1.proto.SyncActionData.fromObject({
130
+ index: indexBuffer,
131
+ value: syncAction,
132
+ padding: new Uint8Array(0),
133
+ version: apiVersion
134
+ })
135
+ const encoded = index_js_1.proto.SyncActionData.encode(dataProto).finish()
136
+ const keyValue = mutationKeys(key.keyData)
137
+ const encValue = (0, crypto_1.aesEncrypt)(encoded, keyValue.valueEncryptionKey)
138
+ const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
139
+ const indexMac = (0, crypto_1.hmacSign)(indexBuffer, keyValue.indexKey)
140
+ // update LT hash
141
+ const generator = makeLtHashGenerator(state)
142
+ generator.mix({ indexMac, valueMac, operation })
143
+ Object.assign(state, generator.finish())
144
+ state.version += 1
145
+ const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey)
146
+ const patch = {
147
+ patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey),
148
+ snapshotMac: snapshotMac,
149
+ keyId: { id: encKeyId },
150
+ mutations: [
151
+ {
152
+ operation: operation,
153
+ record: {
154
+ index: {
155
+ blob: indexMac
156
+ },
157
+ value: {
158
+ blob: Buffer.concat([encValue, valueMac])
159
+ },
160
+ keyId: { id: encKeyId }
161
+ }
162
+ }
163
+ ]
164
+ }
165
+ const base64Index = indexMac.toString('base64')
166
+ state.indexValueMap[base64Index] = { valueMac }
167
+ return { patch, state }
168
+ }
169
+ exports.encodeSyncdPatch = encodeSyncdPatch
170
+ const decodeSyncdMutations = async (msgMutations, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
171
+ const ltGenerator = makeLtHashGenerator(initialState)
172
+ const derivedKeyCache = new Map()
173
+ // indexKey used to HMAC sign record.index.blob
174
+ // valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]
175
+ // the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId
176
+ for (const msgMutation of msgMutations) {
177
+ // if it's a syncdmutation, get the operation property
178
+ // otherwise, if it's only a record -- it'll be a SET mutation
179
+ const operation =
180
+ 'operation' in msgMutation ? msgMutation.operation : index_js_1.proto.SyncdMutation.SyncdOperation.SET
181
+ const record = 'record' in msgMutation && !!msgMutation.record ? msgMutation.record : msgMutation
182
+ const key = await getKey(record.keyId.id)
183
+ const content = record.value.blob
184
+ const encContent = content.subarray(0, -32)
185
+ const ogValueMac = content.subarray(-32)
186
+ if (validateMacs) {
187
+ const contentHmac = generateMac(operation, encContent, record.keyId.id, key.valueMacKey)
188
+ if (Buffer.compare(contentHmac, ogValueMac) !== 0) {
189
+ throw new boom_1.Boom('HMAC content verification failed')
190
+ }
191
+ }
192
+ const result = (0, crypto_1.aesDecrypt)(encContent, key.valueEncryptionKey)
193
+ const syncAction = index_js_1.proto.SyncActionData.decode(result)
194
+ if (validateMacs) {
195
+ const hmac = (0, crypto_1.hmacSign)(syncAction.index, key.indexKey)
196
+ if (Buffer.compare(hmac, record.index.blob) !== 0) {
197
+ throw new boom_1.Boom('HMAC index verification failed')
198
+ }
199
+ }
200
+ const indexStr = Buffer.from(syncAction.index).toString()
201
+ onMutation({ syncAction, index: JSON.parse(indexStr) })
202
+ ltGenerator.mix({
203
+ indexMac: record.index.blob,
204
+ valueMac: ogValueMac,
205
+ operation: operation
206
+ })
207
+ }
208
+ return ltGenerator.finish()
209
+ async function getKey(keyId) {
210
+ const base64Key = Buffer.from(keyId).toString('base64')
211
+ const cached = derivedKeyCache.get(base64Key)
212
+ if (cached) {
213
+ return cached
214
+ }
215
+ const keyEnc = await getAppStateSyncKey(base64Key)
216
+ if (!keyEnc) {
217
+ throw new boom_1.Boom(`failed to find key "${base64Key}" to decode mutation`, {
218
+ statusCode: 404,
219
+ data: { msgMutations }
220
+ })
221
+ }
222
+ const keys = mutationKeys(keyEnc.keyData)
223
+ derivedKeyCache.set(base64Key, keys)
224
+ return keys
225
+ }
226
+ }
227
+ exports.decodeSyncdMutations = decodeSyncdMutations
228
+ const decodeSyncdPatch = async (msg, name, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
229
+ if (validateMacs) {
230
+ const base64Key = Buffer.from(msg.keyId.id).toString('base64')
231
+ const mainKeyObj = await getAppStateSyncKey(base64Key)
232
+ if (!mainKeyObj) {
233
+ throw new boom_1.Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } })
234
+ }
235
+ const mainKey = mutationKeys(mainKeyObj.keyData)
236
+ const mutationmacs = msg.mutations.map(mutation => mutation.record.value.blob.slice(-32))
237
+ const patchMac = generatePatchMac(
238
+ msg.snapshotMac,
239
+ mutationmacs,
240
+ (0, generics_1.toNumber)(msg.version.version),
241
+ name,
242
+ mainKey.patchMacKey
243
+ )
244
+ if (Buffer.compare(patchMac, msg.patchMac) !== 0) {
245
+ throw new boom_1.Boom('Invalid patch mac')
246
+ }
247
+ }
248
+ const result = await (0, exports.decodeSyncdMutations)(
249
+ msg.mutations,
250
+ initialState,
251
+ getAppStateSyncKey,
252
+ onMutation,
253
+ validateMacs
254
+ )
255
+ return result
256
+ }
257
+ exports.decodeSyncdPatch = decodeSyncdPatch
258
+ const extractSyncdPatches = async (result, options) => {
259
+ const syncNode = (0, WABinary_1.getBinaryNodeChild)(result, 'sync')
260
+ const collectionNodes = (0, WABinary_1.getBinaryNodeChildren)(syncNode, 'collection')
261
+ const final = {}
262
+ await Promise.all(
263
+ collectionNodes.map(async collectionNode => {
264
+ const patchesNode = (0, WABinary_1.getBinaryNodeChild)(collectionNode, 'patches')
265
+ const patches = (0, WABinary_1.getBinaryNodeChildren)(patchesNode || collectionNode, 'patch')
266
+ const snapshotNode = (0, WABinary_1.getBinaryNodeChild)(collectionNode, 'snapshot')
267
+ const syncds = []
268
+ const name = collectionNode.attrs.name
269
+ const hasMorePatches = collectionNode.attrs.has_more_patches === 'true'
270
+ let snapshot = undefined
271
+ if (snapshotNode && !!snapshotNode.content) {
272
+ if (!Buffer.isBuffer(snapshotNode)) {
273
+ snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
274
+ }
275
+ const blobRef = index_js_1.proto.ExternalBlobReference.decode(snapshotNode.content)
276
+ const data = await (0, exports.downloadExternalBlob)(blobRef, options)
277
+ snapshot = index_js_1.proto.SyncdSnapshot.decode(data)
278
+ }
279
+ for (let { content } of patches) {
280
+ if (content) {
281
+ if (!Buffer.isBuffer(content)) {
282
+ content = Buffer.from(Object.values(content))
283
+ }
284
+ const syncd = index_js_1.proto.SyncdPatch.decode(content)
285
+ if (!syncd.version) {
286
+ syncd.version = { version: +collectionNode.attrs.version + 1 }
287
+ }
288
+ syncds.push(syncd)
289
+ }
290
+ }
291
+ final[name] = { patches: syncds, hasMorePatches, snapshot }
292
+ })
293
+ )
294
+ return final
295
+ }
296
+ exports.extractSyncdPatches = extractSyncdPatches
297
+ const downloadExternalBlob = async (blob, options) => {
298
+ const stream = await (0, messages_media_1.downloadContentFromMessage)(blob, 'md-app-state', { options })
299
+ const bufferArray = []
300
+ for await (const chunk of stream) {
301
+ bufferArray.push(chunk)
302
+ }
303
+ return Buffer.concat(bufferArray)
304
+ }
305
+ exports.downloadExternalBlob = downloadExternalBlob
306
+ const downloadExternalPatch = async (blob, options) => {
307
+ const buffer = await (0, exports.downloadExternalBlob)(blob, options)
308
+ const syncData = index_js_1.proto.SyncdMutations.decode(buffer)
309
+ return syncData
310
+ }
311
+ exports.downloadExternalPatch = downloadExternalPatch
312
+ const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, minimumVersionNumber, validateMacs = true) => {
313
+ const newState = (0, exports.newLTHashState)()
314
+ newState.version = (0, generics_1.toNumber)(snapshot.version.version)
315
+ const mutationMap = {}
316
+ const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber
317
+ const { hash, indexValueMap } = await (0, exports.decodeSyncdMutations)(
318
+ snapshot.records,
319
+ newState,
320
+ getAppStateSyncKey,
321
+ areMutationsRequired
322
+ ? mutation => {
323
+ const index = mutation.syncAction.index?.toString()
324
+ mutationMap[index] = mutation
325
+ }
326
+ : () => {},
327
+ validateMacs
328
+ )
329
+ newState.hash = hash
330
+ newState.indexValueMap = indexValueMap
331
+ if (validateMacs) {
332
+ const base64Key = Buffer.from(snapshot.keyId.id).toString('base64')
333
+ const keyEnc = await getAppStateSyncKey(base64Key)
334
+ if (!keyEnc) {
335
+ throw new boom_1.Boom(`failed to find key "${base64Key}" to decode mutation`)
336
+ }
337
+ const result = mutationKeys(keyEnc.keyData)
338
+ const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
339
+ if (Buffer.compare(snapshot.mac, computedSnapshotMac) !== 0) {
340
+ throw new boom_1.Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`)
341
+ }
342
+ }
343
+ return {
344
+ state: newState,
345
+ mutationMap
346
+ }
347
+ }
348
+ exports.decodeSyncdSnapshot = decodeSyncdSnapshot
349
+ const decodePatches = async (
350
+ name,
351
+ syncds,
352
+ initial,
353
+ getAppStateSyncKey,
354
+ options,
355
+ minimumVersionNumber,
356
+ logger,
357
+ validateMacs = true
358
+ ) => {
359
+ const newState = {
360
+ ...initial,
361
+ indexValueMap: { ...initial.indexValueMap }
362
+ }
363
+ const mutationMap = {}
364
+ for (const syncd of syncds) {
365
+ const { version, keyId, snapshotMac } = syncd
366
+ if (syncd.externalMutations) {
367
+ logger?.trace({ name, version }, 'downloading external patch')
368
+ const ref = await (0, exports.downloadExternalPatch)(syncd.externalMutations, options)
369
+ logger?.debug({ name, version, mutations: ref.mutations.length }, 'downloaded external patch')
370
+ syncd.mutations?.push(...ref.mutations)
371
+ }
372
+ const patchVersion = (0, generics_1.toNumber)(version.version)
373
+ newState.version = patchVersion
374
+ const shouldMutate = typeof minimumVersionNumber === 'undefined' || patchVersion > minimumVersionNumber
375
+ const decodeResult = await (0, exports.decodeSyncdPatch)(
376
+ syncd,
377
+ name,
378
+ newState,
379
+ getAppStateSyncKey,
380
+ shouldMutate
381
+ ? mutation => {
382
+ const index = mutation.syncAction.index?.toString()
383
+ mutationMap[index] = mutation
384
+ }
385
+ : () => {},
386
+ true
387
+ )
388
+ newState.hash = decodeResult.hash
389
+ newState.indexValueMap = decodeResult.indexValueMap
390
+ if (validateMacs) {
391
+ const base64Key = Buffer.from(keyId.id).toString('base64')
392
+ const keyEnc = await getAppStateSyncKey(base64Key)
393
+ if (!keyEnc) {
394
+ throw new boom_1.Boom(`failed to find key "${base64Key}" to decode mutation`)
395
+ }
396
+ const result = mutationKeys(keyEnc.keyData)
397
+ const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
398
+ if (Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
399
+ throw new boom_1.Boom(`failed to verify LTHash at ${newState.version} of ${name}`)
400
+ }
401
+ }
402
+ // clear memory used up by the mutations
403
+ syncd.mutations = []
404
+ }
405
+ return { state: newState, mutationMap }
406
+ }
407
+ exports.decodePatches = decodePatches
408
+ const chatModificationToAppPatch = (mod, jid) => {
409
+ const OP = index_js_1.proto.SyncdMutation.SyncdOperation
410
+ const getMessageRange = lastMessages => {
411
+ let messageRange
412
+ if (Array.isArray(lastMessages)) {
413
+ const lastMsg = lastMessages[lastMessages.length - 1]
414
+ messageRange = {
415
+ lastMessageTimestamp: lastMsg?.messageTimestamp,
416
+ messages: lastMessages?.length
417
+ ? lastMessages.map(m => {
418
+ if (!m.key?.id || !m.key?.remoteJid) {
419
+ throw new boom_1.Boom('Incomplete key', { statusCode: 400, data: m })
420
+ }
421
+ if ((0, WABinary_1.isJidGroup)(m.key.remoteJid) && !m.key.fromMe && !m.key.participant) {
422
+ throw new boom_1.Boom('Expected not from me message to have participant', { statusCode: 400, data: m })
423
+ }
424
+ if (!m.messageTimestamp || !(0, generics_1.toNumber)(m.messageTimestamp)) {
425
+ throw new boom_1.Boom('Missing timestamp in last message list', { statusCode: 400, data: m })
426
+ }
427
+ if (m.key.participant) {
428
+ m.key.participant = (0, WABinary_1.jidNormalizedUser)(m.key.participant)
429
+ }
430
+ return m
431
+ })
432
+ : undefined
433
+ }
434
+ } else {
435
+ messageRange = lastMessages
436
+ }
437
+ return messageRange
438
+ }
439
+ let patch
440
+ if ('mute' in mod) {
441
+ patch = {
442
+ syncAction: {
443
+ muteAction: {
444
+ muted: !!mod.mute,
445
+ muteEndTimestamp: mod.mute || undefined
446
+ }
447
+ },
448
+ index: ['mute', jid],
449
+ type: 'regular_high',
450
+ apiVersion: 2,
451
+ operation: OP.SET
452
+ }
453
+ } else if ('archive' in mod) {
454
+ patch = {
455
+ syncAction: {
456
+ archiveChatAction: {
457
+ archived: !!mod.archive,
458
+ messageRange: getMessageRange(mod.lastMessages)
459
+ }
460
+ },
461
+ index: ['archive', jid],
462
+ type: 'regular_low',
463
+ apiVersion: 3,
464
+ operation: OP.SET
465
+ }
466
+ } else if ('markRead' in mod) {
467
+ patch = {
468
+ syncAction: {
469
+ markChatAsReadAction: {
470
+ read: mod.markRead,
471
+ messageRange: getMessageRange(mod.lastMessages)
472
+ }
473
+ },
474
+ index: ['markChatAsRead', jid],
475
+ type: 'regular_low',
476
+ apiVersion: 3,
477
+ operation: OP.SET
478
+ }
479
+ } else if ('deleteForMe' in mod) {
480
+ const { timestamp, key, deleteMedia } = mod.deleteForMe
481
+ patch = {
482
+ syncAction: {
483
+ deleteMessageForMeAction: {
484
+ deleteMedia,
485
+ messageTimestamp: timestamp
486
+ }
487
+ },
488
+ index: ['deleteMessageForMe', jid, key.id, key.fromMe ? '1' : '0', '0'],
489
+ type: 'regular_high',
490
+ apiVersion: 3,
491
+ operation: OP.SET
492
+ }
493
+ } else if ('clear' in mod) {
494
+ patch = {
495
+ syncAction: {
496
+ clearChatAction: {
497
+ messageRange: getMessageRange(mod.lastMessages)
498
+ }
499
+ },
500
+ index: ['clearChat', jid, '1' /*the option here is 0 when keep starred messages is enabled*/, '0'],
501
+ type: 'regular_high',
502
+ apiVersion: 6,
503
+ operation: OP.SET
504
+ }
505
+ } else if ('pin' in mod) {
506
+ patch = {
507
+ syncAction: {
508
+ pinAction: {
509
+ pinned: !!mod.pin
510
+ }
511
+ },
512
+ index: ['pin_v1', jid],
513
+ type: 'regular_low',
514
+ apiVersion: 5,
515
+ operation: OP.SET
516
+ }
517
+ } else if ('contact' in mod) {
518
+ patch = {
519
+ syncAction: {
520
+ contactAction: mod.contact || {}
521
+ },
522
+ index: ['contact', jid],
523
+ type: 'critical_unblock_low',
524
+ apiVersion: 2,
525
+ operation: mod.contact ? OP.SET : OP.REMOVE
526
+ }
527
+ } else if ('disableLinkPreviews' in mod) {
528
+ patch = {
529
+ syncAction: {
530
+ privacySettingDisableLinkPreviewsAction: mod.disableLinkPreviews || {}
531
+ },
532
+ index: ['setting_disableLinkPreviews'],
533
+ type: 'regular',
534
+ apiVersion: 8,
535
+ operation: OP.SET
536
+ }
537
+ } else if ('star' in mod) {
538
+ const key = mod.star.messages[0]
539
+ patch = {
540
+ syncAction: {
541
+ starAction: {
542
+ starred: !!mod.star.star
543
+ }
544
+ },
545
+ index: ['star', jid, key.id, key.fromMe ? '1' : '0', '0'],
546
+ type: 'regular_low',
547
+ apiVersion: 2,
548
+ operation: OP.SET
549
+ }
550
+ } else if ('delete' in mod) {
551
+ patch = {
552
+ syncAction: {
553
+ deleteChatAction: {
554
+ messageRange: getMessageRange(mod.lastMessages)
555
+ }
556
+ },
557
+ index: ['deleteChat', jid, '1'],
558
+ type: 'regular_high',
559
+ apiVersion: 6,
560
+ operation: OP.SET
561
+ }
562
+ } else if ('pushNameSetting' in mod) {
563
+ patch = {
564
+ syncAction: {
565
+ pushNameSetting: {
566
+ name: mod.pushNameSetting
567
+ }
568
+ },
569
+ index: ['setting_pushName'],
570
+ type: 'critical_block',
571
+ apiVersion: 1,
572
+ operation: OP.SET
573
+ }
574
+ } else if ('quickReply' in mod) {
575
+ patch = {
576
+ syncAction: {
577
+ quickReplyAction: {
578
+ count: 0,
579
+ deleted: mod.quickReply.deleted || false,
580
+ keywords: [],
581
+ message: mod.quickReply.message || '',
582
+ shortcut: mod.quickReply.shortcut || ''
583
+ }
584
+ },
585
+ index: ['quick_reply', mod.quickReply.timestamp || String(Math.floor(Date.now() / 1000))],
586
+ type: 'regular',
587
+ apiVersion: 2,
588
+ operation: OP.SET
589
+ }
590
+ } else if ('addLabel' in mod) {
591
+ patch = {
592
+ syncAction: {
593
+ labelEditAction: {
594
+ name: mod.addLabel.name,
595
+ color: mod.addLabel.color,
596
+ predefinedId: mod.addLabel.predefinedId,
597
+ deleted: mod.addLabel.deleted
598
+ }
599
+ },
600
+ index: ['label_edit', mod.addLabel.id],
601
+ type: 'regular',
602
+ apiVersion: 3,
603
+ operation: OP.SET
604
+ }
605
+ } else if ('addChatLabel' in mod) {
606
+ patch = {
607
+ syncAction: {
608
+ labelAssociationAction: {
609
+ labeled: true
610
+ }
611
+ },
612
+ index: [LabelAssociation_1.LabelAssociationType.Chat, mod.addChatLabel.labelId, jid],
613
+ type: 'regular',
614
+ apiVersion: 3,
615
+ operation: OP.SET
616
+ }
617
+ } else if ('removeChatLabel' in mod) {
618
+ patch = {
619
+ syncAction: {
620
+ labelAssociationAction: {
621
+ labeled: false
622
+ }
623
+ },
624
+ index: [LabelAssociation_1.LabelAssociationType.Chat, mod.removeChatLabel.labelId, jid],
625
+ type: 'regular',
626
+ apiVersion: 3,
627
+ operation: OP.SET
628
+ }
629
+ } else if ('addMessageLabel' in mod) {
630
+ patch = {
631
+ syncAction: {
632
+ labelAssociationAction: {
633
+ labeled: true
634
+ }
635
+ },
636
+ index: [
637
+ LabelAssociation_1.LabelAssociationType.Message,
638
+ mod.addMessageLabel.labelId,
639
+ jid,
640
+ mod.addMessageLabel.messageId,
641
+ '0',
642
+ '0'
643
+ ],
644
+ type: 'regular',
645
+ apiVersion: 3,
646
+ operation: OP.SET
647
+ }
648
+ } else if ('removeMessageLabel' in mod) {
649
+ patch = {
650
+ syncAction: {
651
+ labelAssociationAction: {
652
+ labeled: false
653
+ }
654
+ },
655
+ index: [
656
+ LabelAssociation_1.LabelAssociationType.Message,
657
+ mod.removeMessageLabel.labelId,
658
+ jid,
659
+ mod.removeMessageLabel.messageId,
660
+ '0',
661
+ '0'
662
+ ],
663
+ type: 'regular',
664
+ apiVersion: 3,
665
+ operation: OP.SET
666
+ }
667
+ } else if ('markAsUnread' in mod) {
668
+ patch = {
669
+ syncAction: {
670
+ markChatAsReadAction: {
671
+ read: false,
672
+ messageRange: getMessageRange(mod.lastMessages)
673
+ }
674
+ },
675
+ index: ['markChatAsRead', jid],
676
+ type: 'regular_low',
677
+ apiVersion: 3,
678
+ operation: OP.SET
679
+ }
680
+ } else if ('setChatEphemeral' in mod) {
681
+ const duration =
682
+ typeof mod.setChatEphemeral === 'number' ? mod.setChatEphemeral : (mod.setChatEphemeral?.duration ?? 0)
683
+ patch = {
684
+ syncAction: {
685
+ chatEphemeralAction: {
686
+ ephemeralExpiration: duration,
687
+ ephemeralSettingTimestamp: Date.now()
688
+ }
689
+ },
690
+ index: ['ephemeral', jid],
691
+ type: 'regular',
692
+ apiVersion: 1,
693
+ operation: OP.SET
694
+ }
695
+ } else if ('silenceChat' in mod) {
696
+ patch = {
697
+ syncAction: {
698
+ muteAction: {
699
+ muted: mod.silenceChat.silent !== false,
700
+ muteEndTimestamp: mod.silenceChat.until ?? null
701
+ }
702
+ },
703
+ index: ['mute', jid],
704
+ type: 'regular_high',
705
+ apiVersion: 2,
706
+ operation: OP.SET
707
+ }
708
+ } else if ('muteStatus' in mod) {
709
+ patch = {
710
+ syncAction: {
711
+ userStatusMuteAction: { muted: mod.muteStatus.muted !== false }
712
+ },
713
+ index: ['user_status_mute', jid],
714
+ type: 'regular',
715
+ apiVersion: 1,
716
+ operation: OP.SET
717
+ }
718
+ } else if ('favorite' in mod) {
719
+ patch = {
720
+ syncAction: {
721
+ favoritesAction: { isFavorite: !!mod.favorite.isFavorite }
722
+ },
723
+ index: ['favorites', jid],
724
+ type: 'regular',
725
+ apiVersion: 1,
726
+ operation: OP.SET
727
+ }
728
+ } else if ('reorderLabel' in mod) {
729
+ patch = {
730
+ syncAction: {
731
+ labelReorderingAction: { sortedLabelIds: mod.reorderLabel.sortedLabelIds || [] }
732
+ },
733
+ index: ['label_reordering'],
734
+ type: 'regular',
735
+ apiVersion: 1,
736
+ operation: OP.SET
737
+ }
738
+ } else if ('deleteCallLog' in mod) {
739
+ patch = {
740
+ syncAction: {
741
+ deleteIndividualCallLog: { callId: mod.deleteCallLog.callId || mod.deleteCallLog }
742
+ },
743
+ index: ['delete_call_log', jid],
744
+ type: 'regular',
745
+ apiVersion: 1,
746
+ operation: OP.SET
747
+ }
748
+ } else if ('noteEdit' in mod) {
749
+ patch = {
750
+ syncAction: {
751
+ noteEditAction: { note: mod.noteEdit.note || mod.noteEdit, deleted: mod.noteEdit.deleted ?? false }
752
+ },
753
+ index: ['note_edit', jid],
754
+ type: 'regular',
755
+ apiVersion: 1,
756
+ operation: OP.SET
757
+ }
758
+ } else if ('aiThreadRename' in mod) {
759
+ patch = {
760
+ syncAction: {
761
+ aiThreadRenameAction: {
762
+ newTitle: mod.aiThreadRename.title || mod.aiThreadRename
763
+ }
764
+ },
765
+ index: ['ai_thread_rename', jid],
766
+ type: 'regular_high',
767
+ apiVersion: 1,
768
+ operation: OP.SET
769
+ }
770
+ } else if ('threadPin' in mod) {
771
+ patch = {
772
+ syncAction: {
773
+ threadPinAction: {
774
+ pinned: !!mod.threadPin.pinned
775
+ }
776
+ },
777
+ index: ['thread_pin', jid, mod.threadPin.messageId || ''],
778
+ type: 'regular_low',
779
+ apiVersion: 1,
780
+ operation: OP.SET
781
+ }
782
+ } else if ('chatLock' in mod) {
783
+ patch = {
784
+ syncAction: {
785
+ lockChatAction: {
786
+ locked: mod.chatLock.locked !== false
787
+ }
788
+ },
789
+ index: ['lock_chat', jid],
790
+ type: 'regular_high',
791
+ apiVersion: 1,
792
+ operation: OP.SET
793
+ }
794
+ } else if ('wallpaper' in mod) {
795
+ const wallpaperData = mod.wallpaper || {}
796
+ patch = {
797
+ syncAction: {
798
+ chatCustomImageWallpaper: wallpaperData.remove
799
+ ? {}
800
+ : {
801
+ directPath: wallpaperData.directPath,
802
+ mediaKey: wallpaperData.mediaKey,
803
+ fileEncSha256: wallpaperData.fileEncSha256,
804
+ fileSha256: wallpaperData.fileSha256,
805
+ dimLevel: wallpaperData.dimLevel ?? 0
806
+ }
807
+ },
808
+ index: ['chat_custom_image_wallpaper', jid],
809
+ type: 'regular',
810
+ apiVersion: 1,
811
+ operation: wallpaperData.remove ? OP.REMOVE : OP.SET
812
+ }
813
+ } else if ('mediaVisibility' in mod) {
814
+ const visibilityEnum = index_js_1.proto.MediaVisibility
815
+ const visMap = { default: visibilityEnum.DEFAULT, off: visibilityEnum.OFF, on: visibilityEnum.ON }
816
+ const visibility =
817
+ typeof mod.mediaVisibility === 'string'
818
+ ? (visMap[mod.mediaVisibility.toLowerCase()] ?? visibilityEnum.DEFAULT)
819
+ : (mod.mediaVisibility ?? visibilityEnum.DEFAULT)
820
+ patch = {
821
+ syncAction: {
822
+ mediaVisibilityAction: { visibility }
823
+ },
824
+ index: ['media_visibility', jid],
825
+ type: 'regular',
826
+ apiVersion: 1,
827
+ operation: OP.SET
828
+ }
829
+ } else if ('privateProcessingSetting' in mod) {
830
+ const statusEnum = index_js_1.proto.SyncActionValue.PrivateProcessingSettingAction.PrivateProcessingStatus
831
+ const statusMap = { enabled: statusEnum.ENABLED, disabled: statusEnum.DISABLED }
832
+ const status =
833
+ typeof mod.privateProcessingSetting === 'string'
834
+ ? (statusMap[mod.privateProcessingSetting.toLowerCase()] ?? statusEnum.ENABLED)
835
+ : (mod.privateProcessingSetting.status ??
836
+ (mod.privateProcessingSetting.enabled ? statusEnum.ENABLED : statusEnum.DISABLED))
837
+ patch = {
838
+ syncAction: {
839
+ privateProcessingSettingAction: { privateProcessingStatus: status }
840
+ },
841
+ index: ['setting_private_processing'],
842
+ type: 'regular',
843
+ apiVersion: 1,
844
+ operation: OP.SET
845
+ }
846
+ } else {
847
+ throw new boom_1.Boom('not supported')
848
+ }
849
+ patch.syncAction.timestamp = Date.now()
850
+ return patch
851
+ }
852
+ exports.chatModificationToAppPatch = chatModificationToAppPatch
853
+ const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) => {
854
+ const isInitialSync = !!initialSyncOpts
855
+ const accountSettings = initialSyncOpts?.accountSettings
856
+ logger?.trace({ syncAction, initialSync: !!initialSyncOpts }, 'processing sync action')
857
+ const {
858
+ syncAction: { value: action },
859
+ index: [type, id, msgId, fromMe]
860
+ } = syncAction
861
+ if (action?.muteAction) {
862
+ ev.emit('chats.update', [
863
+ {
864
+ id,
865
+ muteEndTime: action.muteAction?.muted ? (0, generics_1.toNumber)(action.muteAction.muteEndTimestamp) : null,
866
+ conditional: getChatUpdateConditional(id, undefined)
867
+ }
868
+ ])
869
+ } else if (action?.archiveChatAction || type === 'archive' || type === 'unarchive') {
870
+ // okay so we've to do some annoying computation here
871
+ // when we're initially syncing the app state
872
+ // there are a few cases we need to handle
873
+ // 1. if the account unarchiveChats setting is true
874
+ // a. if the chat is archived, and no further messages have been received -- simple, keep archived
875
+ // b. if the chat was archived, and the user received messages from the other person afterwards
876
+ // then the chat should be marked unarchved --
877
+ // we compare the timestamp of latest message from the other person to determine this
878
+ // 2. if the account unarchiveChats setting is false -- then it doesn't matter,
879
+ // it'll always take an app state action to mark in unarchived -- which we'll get anyway
880
+ const archiveAction = action?.archiveChatAction
881
+ const isArchived = archiveAction ? archiveAction.archived : type === 'archive'
882
+ // // basically we don't need to fire an "archive" update if the chat is being marked unarchvied
883
+ // // this only applies for the initial sync
884
+ // if(isInitialSync && !isArchived) {
885
+ // isArchived = false
886
+ // }
887
+ const msgRange = !accountSettings?.unarchiveChats ? undefined : archiveAction?.messageRange
888
+ // logger?.debug({ chat: id, syncAction }, 'message range archive')
889
+ ev.emit('chats.update', [
890
+ {
891
+ id,
892
+ archived: isArchived,
893
+ conditional: getChatUpdateConditional(id, msgRange)
894
+ }
895
+ ])
896
+ } else if (action?.markChatAsReadAction) {
897
+ const markReadAction = action.markChatAsReadAction
898
+ // basically we don't need to fire an "read" update if the chat is being marked as read
899
+ // because the chat is read by default
900
+ // this only applies for the initial sync
901
+ const isNullUpdate = isInitialSync && markReadAction.read
902
+ ev.emit('chats.update', [
903
+ {
904
+ id,
905
+ unreadCount: isNullUpdate ? null : !!markReadAction?.read ? 0 : -1,
906
+ conditional: getChatUpdateConditional(id, markReadAction?.messageRange)
907
+ }
908
+ ])
909
+ } else if (action?.deleteMessageForMeAction || type === 'deleteMessageForMe') {
910
+ ev.emit('messages.delete', {
911
+ keys: [
912
+ {
913
+ remoteJid: id,
914
+ id: msgId,
915
+ fromMe: fromMe === '1'
916
+ }
917
+ ]
918
+ })
919
+ } else if (action?.contactAction) {
920
+ const results = (0, sync_action_utils_1.processContactAction)(action.contactAction, id, logger)
921
+ ;(0, sync_action_utils_1.emitSyncActionResults)(ev, results)
922
+ } else if (action?.pushNameSetting) {
923
+ const name = action?.pushNameSetting?.name
924
+ if (name && me?.name !== name) {
925
+ ev.emit('creds.update', { me: { ...me, name } })
926
+ }
927
+ } else if (action?.pinAction) {
928
+ ev.emit('chats.update', [
929
+ {
930
+ id,
931
+ pinned: action.pinAction?.pinned ? (0, generics_1.toNumber)(action.timestamp) : null,
932
+ conditional: getChatUpdateConditional(id, undefined)
933
+ }
934
+ ])
935
+ } else if (action?.unarchiveChatsSetting) {
936
+ const unarchiveChats = !!action.unarchiveChatsSetting.unarchiveChats
937
+ ev.emit('creds.update', { accountSettings: { unarchiveChats } })
938
+ logger?.info(`archive setting updated => '${action.unarchiveChatsSetting.unarchiveChats}'`)
939
+ if (accountSettings) {
940
+ accountSettings.unarchiveChats = unarchiveChats
941
+ }
942
+ } else if (action?.starAction || type === 'star') {
943
+ let starred = action?.starAction?.starred
944
+ if (typeof starred !== 'boolean') {
945
+ starred = syncAction.index[syncAction.index.length - 1] === '1'
946
+ }
947
+ ev.emit('messages.update', [
948
+ {
949
+ key: { remoteJid: id, id: msgId, fromMe: fromMe === '1' },
950
+ update: { starred }
951
+ }
952
+ ])
953
+ } else if (action?.deleteChatAction || type === 'deleteChat') {
954
+ if (!isInitialSync) {
955
+ ev.emit('chats.delete', [id])
956
+ }
957
+ } else if (action?.labelEditAction) {
958
+ const { name, color, deleted, predefinedId } = action.labelEditAction
959
+ ev.emit('labels.edit', {
960
+ id: id,
961
+ name: name,
962
+ color: color,
963
+ deleted: deleted,
964
+ predefinedId: predefinedId ? String(predefinedId) : undefined
965
+ })
966
+ } else if (action?.labelAssociationAction) {
967
+ ev.emit('labels.association', {
968
+ type: action.labelAssociationAction.labeled ? 'add' : 'remove',
969
+ association:
970
+ type === LabelAssociation_1.LabelAssociationType.Chat
971
+ ? {
972
+ type: LabelAssociation_1.LabelAssociationType.Chat,
973
+ chatId: syncAction.index[2],
974
+ labelId: syncAction.index[1]
975
+ }
976
+ : {
977
+ type: LabelAssociation_1.LabelAssociationType.Message,
978
+ chatId: syncAction.index[2],
979
+ messageId: syncAction.index[3],
980
+ labelId: syncAction.index[1]
981
+ }
982
+ })
983
+ } else if (action?.localeSetting?.locale) {
984
+ ev.emit('settings.update', { setting: 'locale', value: action.localeSetting.locale })
985
+ } else if (action?.timeFormatAction) {
986
+ ev.emit('settings.update', { setting: 'timeFormat', value: action.timeFormatAction })
987
+ } else if (action?.pnForLidChatAction) {
988
+ if (action.pnForLidChatAction.pnJid) {
989
+ ev.emit('lid-mapping.update', { lid: id, pn: action.pnForLidChatAction.pnJid })
990
+ }
991
+ } else if (action?.privacySettingRelayAllCalls) {
992
+ ev.emit('settings.update', {
993
+ setting: 'privacySettingRelayAllCalls',
994
+ value: action.privacySettingRelayAllCalls
995
+ })
996
+ } else if (action?.statusPrivacy) {
997
+ ev.emit('settings.update', { setting: 'statusPrivacy', value: action.statusPrivacy })
998
+ } else if (action?.lockChatAction) {
999
+ ev.emit('chats.lock', { id: id, locked: !!action.lockChatAction.locked })
1000
+ } else if (action?.privacySettingDisableLinkPreviewsAction) {
1001
+ ev.emit('settings.update', {
1002
+ setting: 'disableLinkPreviews',
1003
+ value: action.privacySettingDisableLinkPreviewsAction
1004
+ })
1005
+ } else if (action?.notificationActivitySettingAction?.notificationActivitySetting) {
1006
+ ev.emit('settings.update', {
1007
+ setting: 'notificationActivitySetting',
1008
+ value: action.notificationActivitySettingAction.notificationActivitySetting
1009
+ })
1010
+ } else if (action?.lidContactAction) {
1011
+ ev.emit('contacts.upsert', [
1012
+ {
1013
+ id: id,
1014
+ name:
1015
+ action.lidContactAction.fullName ||
1016
+ action.lidContactAction.firstName ||
1017
+ action.lidContactAction.username ||
1018
+ undefined,
1019
+ username: action.lidContactAction.username || undefined,
1020
+ lid: id,
1021
+ phoneNumber: undefined
1022
+ }
1023
+ ])
1024
+ } else if (action?.privacySettingChannelsPersonalisedRecommendationAction) {
1025
+ ev.emit('settings.update', {
1026
+ setting: 'channelsPersonalisedRecommendation',
1027
+ value: action.privacySettingChannelsPersonalisedRecommendationAction
1028
+ })
1029
+ } else if (action?.aiThreadRenameAction) {
1030
+ ev.emit('chats.update', [
1031
+ {
1032
+ id,
1033
+ name: action.aiThreadRenameAction.newTitle,
1034
+ conditional: getChatUpdateConditional(id, undefined)
1035
+ }
1036
+ ])
1037
+ } else if (action?.threadPinAction) {
1038
+ ev.emit('chats.update', [
1039
+ {
1040
+ id,
1041
+ pinned: action.threadPinAction.pinned ? (0, generics_1.toNumber)(action.timestamp) : null,
1042
+ conditional: getChatUpdateConditional(id, undefined)
1043
+ }
1044
+ ])
1045
+ } else if (action?.privateProcessingSettingAction) {
1046
+ ev.emit('settings.update', {
1047
+ setting: 'privateProcessing',
1048
+ value: action.privateProcessingSettingAction
1049
+ })
1050
+ } else if (action?.settingsSyncAction) {
1051
+ ev.emit('settings.update', {
1052
+ setting: 'clientSettings',
1053
+ value: action.settingsSyncAction
1054
+ })
1055
+ } else if (action?.musicUserIdAction) {
1056
+ ev.emit('settings.update', {
1057
+ setting: 'musicUserId',
1058
+ value: action.musicUserIdAction
1059
+ })
1060
+ } else if (action?.avatarUpdatedAction) {
1061
+ ev.emit('settings.update', {
1062
+ setting: 'avatarUpdated',
1063
+ value: action.avatarUpdatedAction
1064
+ })
1065
+ } else if (action?.recentEmojiWeightsAction) {
1066
+ ev.emit('settings.update', {
1067
+ setting: 'recentEmojiWeights',
1068
+ value: action.recentEmojiWeightsAction?.weights || []
1069
+ })
1070
+ } else if (action?.stickerAction) {
1071
+ ev.emit('settings.update', { setting: 'stickerSync', value: action.stickerAction })
1072
+ } else if (action?.removeRecentStickerAction) {
1073
+ ev.emit('settings.update', { setting: 'removedRecentSticker', value: action.removeRecentStickerAction })
1074
+ } else if (action?.userStatusMuteAction) {
1075
+ ev.emit('contacts.update', [{ id, statusMuted: !!action.userStatusMuteAction.muted }])
1076
+ } else if (action?.chatAssignment) {
1077
+ ev.emit('chats.update', [
1078
+ {
1079
+ id,
1080
+ chatAssignment: action.chatAssignment,
1081
+ conditional: getChatUpdateConditional(id, undefined)
1082
+ }
1083
+ ])
1084
+ } else if (action?.chatAssignmentOpenedStatus) {
1085
+ ev.emit('chats.update', [
1086
+ {
1087
+ id,
1088
+ chatAssignmentOpened: !!action.chatAssignmentOpenedStatus.opened,
1089
+ conditional: getChatUpdateConditional(id, undefined)
1090
+ }
1091
+ ])
1092
+ } else if (action?.callLogAction) {
1093
+ ev.emit('settings.update', { setting: 'callLog', value: action.callLogAction })
1094
+ } else if (action?.deleteIndividualCallLog) {
1095
+ ev.emit('settings.update', { setting: 'callLogDeleted', value: action.deleteIndividualCallLog })
1096
+ } else if (action?.labelReorderingAction) {
1097
+ ev.emit('labels.reorder', { labelIds: action.labelReorderingAction.sortedLabelIds || [] })
1098
+ } else if (action?.paymentInfoAction) {
1099
+ ev.emit('settings.update', { setting: 'paymentInfo', value: action.paymentInfoAction })
1100
+ } else if (action?.noteEditAction) {
1101
+ ev.emit('chats.update', [
1102
+ { id, draftNote: action.noteEditAction, conditional: getChatUpdateConditional(id, undefined) }
1103
+ ])
1104
+ } else if (action?.favoritesAction) {
1105
+ ev.emit('contacts.update', [{ id, favorite: !!action.favoritesAction.isFavorite }])
1106
+ } else if (action?.usernameChatStartMode) {
1107
+ ev.emit('settings.update', { setting: 'usernameChatStartMode', value: action.usernameChatStartMode })
1108
+ } else if (action?.maibaAiFeaturesControlAction) {
1109
+ ev.emit('settings.update', { setting: 'aiFeatures', value: action.maibaAiFeaturesControlAction })
1110
+ } else if (action?.statusPostOptInNotificationPreferencesAction) {
1111
+ ev.emit('settings.update', {
1112
+ setting: 'statusPostOptInNotifications',
1113
+ value: action.statusPostOptInNotificationPreferencesAction
1114
+ })
1115
+ } else if (action?.subscriptionsSyncV2Action) {
1116
+ ev.emit('settings.update', { setting: 'subscriptions', value: action.subscriptionsSyncV2Action })
1117
+ } else if (action?.newsletterSavedInterestsAction) {
1118
+ ev.emit('settings.update', { setting: 'newsletterSavedInterests', value: action.newsletterSavedInterestsAction })
1119
+ } else if (action?.interactiveMessageAction) {
1120
+ ev.emit('settings.update', { setting: 'interactiveMessageAction', value: action.interactiveMessageAction })
1121
+ } else if (action?.outContactAction) {
1122
+ const results = (0, sync_action_utils_1.processContactAction)(
1123
+ {
1124
+ fullName: action.outContactAction.fullName,
1125
+ firstName: action.outContactAction.firstName
1126
+ },
1127
+ id,
1128
+ logger
1129
+ )
1130
+ ;(0, sync_action_utils_1.emitSyncActionResults)(ev, results)
1131
+ } else if (action?.businessBroadcastListAction) {
1132
+ ev.emit('settings.update', { setting: 'broadcastList', value: action.businessBroadcastListAction })
1133
+ } else if (action?.customerDataAction) {
1134
+ ev.emit('chats.update', [
1135
+ { id, customerData: action.customerDataAction, conditional: getChatUpdateConditional(id, undefined) }
1136
+ ])
1137
+ } else if (action?.autoOrganizeBusinessChatSetting) {
1138
+ ev.emit('settings.update', { setting: 'autoOrganizeBusinessChat', value: action.autoOrganizeBusinessChatSetting })
1139
+ } else if (action?.chatLockSettings) {
1140
+ ev.emit('chats.update', [
1141
+ { id, chatLockSettings: action.chatLockSettings, conditional: getChatUpdateConditional(id, undefined) }
1142
+ ])
1143
+ } else if (action?.agentAction) {
1144
+ ev.emit('settings.update', { setting: 'agentAction', value: action.agentAction })
1145
+ } else if (action?.nuxAction) {
1146
+ ev.emit('settings.update', { setting: 'nux', value: action.nuxAction })
1147
+ } else if (action?.quickReplyAction) {
1148
+ ev.emit('settings.update', { setting: 'quickReply', value: action.quickReplyAction })
1149
+ } else if (action?.keyExpiration) {
1150
+ ev.emit('settings.update', { setting: 'keyExpiration', value: action.keyExpiration })
1151
+ } else if (action?.primaryFeature) {
1152
+ ev.emit('settings.update', { setting: 'primaryFeature', value: action.primaryFeature })
1153
+ } else if (action?.androidUnsupportedActions) {
1154
+ ev.emit('settings.update', { setting: 'androidUnsupported', value: action.androidUnsupportedActions })
1155
+ } else if (action?.subscriptionAction) {
1156
+ ev.emit('settings.update', { setting: 'subscription', value: action.subscriptionAction })
1157
+ } else if (action?.primaryVersionAction) {
1158
+ ev.emit('settings.update', { setting: 'primaryVersion', value: action.primaryVersionAction })
1159
+ } else if (action?.marketingMessageAction) {
1160
+ ev.emit('settings.update', { setting: 'marketingMessage', value: action.marketingMessageAction })
1161
+ } else if (action?.marketingMessageBroadcastAction) {
1162
+ ev.emit('settings.update', { setting: 'marketingMessageBroadcast', value: action.marketingMessageBroadcastAction })
1163
+ } else if (action?.externalWebBetaAction) {
1164
+ ev.emit('settings.update', { setting: 'externalWebBeta', value: action.externalWebBetaAction })
1165
+ } else if (action?.botWelcomeRequestAction) {
1166
+ ev.emit('settings.update', { setting: 'botWelcomeRequest', value: action.botWelcomeRequestAction })
1167
+ } else if (action?.customPaymentMethodsAction) {
1168
+ ev.emit('settings.update', { setting: 'customPaymentMethods', value: action.customPaymentMethodsAction })
1169
+ } else if (action?.wamoUserIdentifierAction) {
1170
+ ev.emit('settings.update', { setting: 'wamoUserIdentifier', value: action.wamoUserIdentifierAction })
1171
+ } else if (action?.merchantPaymentPartnerAction) {
1172
+ ev.emit('settings.update', { setting: 'merchantPaymentPartner', value: action.merchantPaymentPartnerAction })
1173
+ } else if (action?.waffleAccountLinkStateAction) {
1174
+ ev.emit('settings.update', { setting: 'waffleAccountLinkState', value: action.waffleAccountLinkStateAction })
1175
+ } else if (action?.ctwaPerCustomerDataSharingAction) {
1176
+ ev.emit('settings.update', { setting: 'ctwaDataSharing', value: action.ctwaPerCustomerDataSharingAction })
1177
+ } else if (action?.paymentTosAction) {
1178
+ ev.emit('settings.update', { setting: 'paymentTos', value: action.paymentTosAction })
1179
+ } else if (action?.detectedOutcomesStatusAction) {
1180
+ ev.emit('settings.update', { setting: 'detectedOutcomesStatus', value: action.detectedOutcomesStatusAction })
1181
+ } else if (action?.nctSaltSyncAction) {
1182
+ ev.emit('settings.update', { setting: 'nctSaltSync', value: action.nctSaltSyncAction })
1183
+ } else if (action?.businessBroadcastCampaignAction) {
1184
+ ev.emit('settings.update', { setting: 'businessBroadcastCampaign', value: action.businessBroadcastCampaignAction })
1185
+ } else if (action?.businessBroadcastInsightsAction) {
1186
+ ev.emit('settings.update', { setting: 'businessBroadcastInsights', value: action.businessBroadcastInsightsAction })
1187
+ } else if (action?.bizAiSettingsNudgeAction) {
1188
+ ev.emit('settings.update', { setting: 'bizAiSettingsNudge', value: action.bizAiSettingsNudgeAction })
1189
+ } else {
1190
+ logger?.debug({ syncAction, id }, 'unprocessable update')
1191
+ }
1192
+ function getChatUpdateConditional(id, msgRange) {
1193
+ return isInitialSync
1194
+ ? data => {
1195
+ const chat = data.historySets.chats[id] || data.chatUpserts[id]
1196
+ if (chat) {
1197
+ return msgRange ? isValidPatchBasedOnMessageRange(chat, msgRange) : true
1198
+ }
1199
+ }
1200
+ : undefined
1201
+ }
1202
+ function isValidPatchBasedOnMessageRange(chat, msgRange) {
1203
+ const lastMsgTimestamp = Number(msgRange?.lastMessageTimestamp || msgRange?.lastSystemMessageTimestamp || 0)
1204
+ const chatLastMsgTimestamp = Number(chat?.lastMessageRecvTimestamp || 0)
1205
+ return lastMsgTimestamp >= chatLastMsgTimestamp
1206
+ }
1207
+ }
1208
+ exports.processSyncAction = processSyncAction