@zervonex/baileys 0.0.0-stage → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3203 -2
- package/WAProto/WAProtoCompile.js +291 -0
- package/WAProto/fetcher.js +69 -0
- package/WAProto/index.js +88 -0
- package/WAProto/parser.js +313 -0
- package/engine-requirements.js +10 -0
- package/lib/Builders/AIRich.js +590 -0
- package/lib/Builders/base.js +130 -0
- package/lib/Builders/builders.js +448 -0
- package/lib/Builders/index.js +3 -0
- package/lib/Defaults/index.js +225 -0
- package/lib/Defaults/phonenumber-mcc.json +223 -0
- package/lib/Signal/index.js +2 -0
- package/lib/Signal/libsignal.js +523 -0
- package/lib/Signal/lid-mapping.js +283 -0
- package/lib/Socket/Client/index.js +2 -0
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.js +84 -0
- package/lib/Socket/business.js +152 -0
- package/lib/Socket/chats.js +855 -0
- package/lib/Socket/communities.js +147 -0
- package/lib/Socket/groups.js +294 -0
- package/lib/Socket/index.js +15 -0
- package/lib/Socket/messages-recv.js +904 -0
- package/lib/Socket/messages-send.js +693 -0
- package/lib/Socket/mex.js +34 -0
- package/lib/Socket/newsletter.js +752 -0
- package/lib/Socket/nexus-handler.js +55 -0
- package/lib/Socket/registration.js +155 -0
- package/lib/Socket/socket.js +798 -0
- package/lib/Socket/spam-report.js +65 -0
- package/lib/Socket/username.js +82 -0
- package/lib/Store/index.js +6 -0
- package/lib/Store/make-cache-manager-store.js +81 -0
- package/lib/Store/make-in-memory-store.js +425 -0
- package/lib/Store/make-ordered-dictionary.js +82 -0
- package/lib/Store/object-repository.js +31 -0
- package/lib/Store/reporting-info-store.js +38 -0
- package/lib/Types/Auth.js +2 -0
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Call.js +2 -0
- package/lib/Types/Chat.js +8 -0
- package/lib/Types/Contact.js +2 -0
- package/lib/Types/Events.js +2 -0
- package/lib/Types/GroupMetadata.js +2 -0
- package/lib/Types/Label.js +25 -0
- package/lib/Types/LabelAssociation.js +7 -0
- package/lib/Types/Message.js +15 -0
- package/lib/Types/Newsletter.js +100 -0
- package/lib/Types/Product.js +2 -0
- package/lib/Types/Signal.js +2 -0
- package/lib/Types/Socket.js +14 -0
- package/lib/Types/State.js +26 -0
- package/lib/Types/USync.js +2 -0
- package/lib/Types/index.js +30 -0
- package/lib/Utils/auth-utils.js +142 -0
- package/lib/Utils/baileys-event-stream.js +33 -0
- package/lib/Utils/bot-capabilities.js +40 -0
- package/lib/Utils/bot-planning-replay.js +44 -0
- package/lib/Utils/browser-utils.js +26 -0
- package/lib/Utils/business.js +75 -0
- package/lib/Utils/chat-utils.js +316 -0
- package/lib/Utils/companion-reg-client-utils.js +20 -0
- package/lib/Utils/consumer-application.js +38 -0
- package/lib/Utils/crypto.js +52 -0
- package/lib/Utils/decode-wa-message.js +197 -0
- package/lib/Utils/event-buffer.js +273 -0
- package/lib/Utils/extras.js +186 -0
- package/lib/Utils/generics.js +195 -0
- package/lib/Utils/history.js +51 -0
- package/lib/Utils/identity-change-handler.js +19 -0
- package/lib/Utils/index.js +39 -0
- package/lib/Utils/key-store.js +3 -0
- package/lib/Utils/link-preview.js +51 -0
- package/lib/Utils/logger.js +2 -0
- package/lib/Utils/lt-hash.js +3 -0
- package/lib/Utils/make-mutex.js +16 -0
- package/lib/Utils/message-composer.js +59 -0
- package/lib/Utils/message-retry-manager.js +48 -0
- package/lib/Utils/messages-media.js +520 -0
- package/lib/Utils/messages.js +742 -0
- package/lib/Utils/meta-ai-msmsg.js +122 -0
- package/lib/Utils/meta-compositing.js +75 -0
- package/lib/Utils/modern-messages.js +235 -0
- package/lib/Utils/native-flow.js +55 -0
- package/lib/Utils/noise-handler.js +161 -0
- package/lib/Utils/pre-key-manager.js +76 -0
- package/lib/Utils/process-message.js +399 -0
- package/lib/Utils/reporting-utils.js +171 -0
- package/lib/Utils/signal.js +167 -0
- package/lib/Utils/spam-report-utils.js +142 -0
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +102 -0
- package/lib/Utils/use-multi-file-auth-state.js +307 -0
- package/lib/Utils/validate-connection.js +181 -0
- package/lib/WABinary/constants.js +1321 -0
- package/lib/WABinary/decode.js +152 -0
- package/lib/WABinary/encode.js +140 -0
- package/lib/WABinary/generic-utils.js +179 -0
- package/lib/WABinary/index.js +6 -0
- package/lib/WABinary/jid-utils.js +235 -0
- package/lib/WABinary/types.js +4 -0
- package/lib/WAM/BinaryInfo.js +10 -0
- package/lib/WAM/constants.js +22853 -0
- package/lib/WAM/encode.js +150 -0
- package/lib/WAM/index.js +3 -0
- package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
- package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
- package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
- package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
- package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
- package/lib/WAUSync/Protocols/index.js +12 -0
- package/lib/WAUSync/USyncQuery.js +100 -0
- package/lib/WAUSync/USyncUser.js +20 -0
- package/lib/WAUSync/index.js +3 -0
- package/lib/index.js +68 -0
- package/package.json +216 -5
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { buildPlainPlaceholder, PlanningStepStatus } from './meta-compositing.js'
|
|
2
|
+
import { delay } from './generics.js'
|
|
3
|
+
|
|
4
|
+
// Live planning animation: shows pending steps, edits each to DONE one-by-one,
|
|
5
|
+
// deletes placeholder, then sends the final message. Works on all WA clients.
|
|
6
|
+
export const replayPlanning = async (sock, jid, steps, finalContent, { description = 'Thinking…', placeholderText = '', stepDelayMs = 900, finalPauseMs = 600, abortOnDisconnect = true, sendOptions = {} } = {}) => {
|
|
7
|
+
if (!steps?.length) throw new Error('replayPlanning: steps must have at least one entry')
|
|
8
|
+
|
|
9
|
+
let aborted = false
|
|
10
|
+
if (abortOnDisconnect) sock.ev?.once?.('connection.update', ({ connection }) => { if (connection === 'close') aborted = true })
|
|
11
|
+
|
|
12
|
+
await sock.sendPresenceUpdate('composing', jid)
|
|
13
|
+
|
|
14
|
+
const initial = steps.map(s => ({ ...s, status: PlanningStepStatus.IN_PROGRESS }))
|
|
15
|
+
const placeholder = await sock.sendMessage(jid, buildPlainPlaceholder(description, initial, placeholderText))
|
|
16
|
+
const key = placeholder?.key
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const current = [...initial]
|
|
20
|
+
for (let i = 0; i < current.length; i++) {
|
|
21
|
+
if (aborted) break
|
|
22
|
+
await delay(stepDelayMs)
|
|
23
|
+
if (aborted) break
|
|
24
|
+
current[i] = { ...current[i], status: PlanningStepStatus.DONE }
|
|
25
|
+
if (key) await sock.sendMessage(jid, { edit: key, ...buildPlainPlaceholder(description, current, placeholderText) })
|
|
26
|
+
}
|
|
27
|
+
if (!aborted && finalPauseMs > 0) await delay(finalPauseMs)
|
|
28
|
+
if (key && !aborted) await sock.sendMessage(jid, { delete: key })
|
|
29
|
+
} catch (_) {
|
|
30
|
+
try { if (key) await sock.sendMessage(jid, { delete: key }) } catch (__) { }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await sock.sendPresenceUpdate('paused', jid)
|
|
34
|
+
if (sendOptions._skipFinalSend) return placeholder
|
|
35
|
+
return sock.sendMessage(jid, finalContent, sendOptions)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Animation without a final message
|
|
39
|
+
export const replayPlanningOnly = (sock, jid, steps, options = {}) =>
|
|
40
|
+
replayPlanning(sock, jid, steps, null, { ...options, sendOptions: { ...options.sendOptions, _skipFinalSend: true } })
|
|
41
|
+
|
|
42
|
+
export const buildReasoningSteps = titles => titles.map(title => ({ title, isReasoning: true }))
|
|
43
|
+
export const buildSearchSteps = titles => titles.map(title => ({ title, isEnhancedSearch: true }))
|
|
44
|
+
export const mixedSteps = defs => defs.map(({ title, body, type }) => ({ title, ...(body ? { body } : {}), ...(type === 'reasoning' ? { isReasoning: true } : {}), ...(type === 'search' ? { isEnhancedSearch: true } : {}) }))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { platform, release } from 'os'
|
|
2
|
+
import { proto } from '../../WAProto/index.js'
|
|
3
|
+
|
|
4
|
+
const PLATFORM_MAP = {
|
|
5
|
+
aix: 'AIX', darwin: 'Mac OS', win32: 'Windows', android: 'Android',
|
|
6
|
+
freebsd: 'FreeBSD', openbsd: 'OpenBSD', sunos: 'Solaris',
|
|
7
|
+
linux: undefined, haiku: undefined, cygwin: undefined, netbsd: undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Browsers = {
|
|
11
|
+
ubuntu: browser => ['Ubuntu', browser, '24.04.2'],
|
|
12
|
+
macOS: browser => ['Mac OS', browser, '15.4.1'],
|
|
13
|
+
baileys: browser => ['Baileys', browser, '6.5.0'],
|
|
14
|
+
windows: browser => ['Windows', browser, '10.0.26100'],
|
|
15
|
+
android: browser => [browser, 'Android', ''],
|
|
16
|
+
appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const getPlatformId = browser => {
|
|
20
|
+
const upper = browser.toUpperCase()
|
|
21
|
+
if (upper === 'ANDROID') return proto.DeviceProps.PlatformType.ANDROID_PHONE.toString()
|
|
22
|
+
const type = proto.DeviceProps.PlatformType[upper]
|
|
23
|
+
return (type ?? proto.DeviceProps.PlatformType.CHROME).toString()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const isAndroidBrowser = browser => browser[1]?.toUpperCase() === 'ANDROID'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Boom } from '@hapi/boom'
|
|
2
|
+
import { createHash } from 'crypto'
|
|
3
|
+
import { getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString } from '../WABinary/index.js'
|
|
4
|
+
import { getStream, getUrlFromDirectPath, toReadable } from './messages-media.js'
|
|
5
|
+
|
|
6
|
+
export const parseCatalogNode = node => {
|
|
7
|
+
const catalogNode = getBinaryNodeChild(node, 'product_catalog')
|
|
8
|
+
const paging = getBinaryNodeChild(catalogNode, 'paging')
|
|
9
|
+
return { products: getBinaryNodeChildren(catalogNode, 'product').map(parseProductNode), nextPageCursor: paging ? getBinaryNodeChildString(paging, 'after') : undefined }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const parseCollectionsNode = node => {
|
|
13
|
+
const collectionsNode = getBinaryNodeChild(node, 'collections')
|
|
14
|
+
return { collections: getBinaryNodeChildren(collectionsNode, 'collection').map(collectionNode => ({ id: getBinaryNodeChildString(collectionNode, 'id'), name: getBinaryNodeChildString(collectionNode, 'name'), products: getBinaryNodeChildren(collectionNode, 'product').map(parseProductNode), status: parseStatusInfo(collectionNode) })) }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const parseOrderDetailsNode = node => {
|
|
18
|
+
const orderNode = getBinaryNodeChild(node, 'order')
|
|
19
|
+
const priceNode = getBinaryNodeChild(orderNode, 'price')
|
|
20
|
+
const products = getBinaryNodeChildren(orderNode, 'product').map(productNode => {
|
|
21
|
+
const imageNode = getBinaryNodeChild(productNode, 'image')
|
|
22
|
+
return { id: getBinaryNodeChildString(productNode, 'id'), name: getBinaryNodeChildString(productNode, 'name'), imageUrl: getBinaryNodeChildString(imageNode, 'url'), price: +getBinaryNodeChildString(productNode, 'price'), currency: getBinaryNodeChildString(productNode, 'currency'), quantity: +getBinaryNodeChildString(productNode, 'quantity') }
|
|
23
|
+
})
|
|
24
|
+
return { price: { total: +getBinaryNodeChildString(priceNode, 'total'), currency: getBinaryNodeChildString(priceNode, 'currency') }, products }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const toProductNode = (productId, product) => {
|
|
28
|
+
const attrs = {}
|
|
29
|
+
const content = []
|
|
30
|
+
const push = (tag, value) => content.push({ tag, attrs: {}, content: Buffer.from(value) })
|
|
31
|
+
if (productId !== undefined) push('id', productId)
|
|
32
|
+
if (product.name !== undefined) push('name', product.name)
|
|
33
|
+
if (product.description !== undefined) push('description', product.description)
|
|
34
|
+
if (product.retailerId !== undefined) push('retailer_id', product.retailerId)
|
|
35
|
+
if (product.images?.length) {
|
|
36
|
+
content.push({
|
|
37
|
+
tag: 'media', attrs: {}, content: product.images.map(img => {
|
|
38
|
+
if (!('url' in img)) throw new Boom('Expected img for product to already be uploaded', { statusCode: 400 })
|
|
39
|
+
return { tag: 'image', attrs: {}, content: [{ tag: 'url', attrs: {}, content: Buffer.from(img.url.toString()) }] }
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
if (product.price !== undefined) push('price', product.price.toString())
|
|
44
|
+
if (product.currency !== undefined) push('currency', product.currency)
|
|
45
|
+
if ('originCountryCode' in product) {
|
|
46
|
+
if (product.originCountryCode === undefined) attrs['compliance_category'] = 'COUNTRY_ORIGIN_EXEMPT'
|
|
47
|
+
else content.push({ tag: 'compliance_info', attrs: {}, content: [{ tag: 'country_code_origin', attrs: {}, content: Buffer.from(product.originCountryCode) }] })
|
|
48
|
+
}
|
|
49
|
+
if (product.isHidden !== undefined) attrs['is_hidden'] = product.isHidden.toString()
|
|
50
|
+
return { tag: 'product', attrs, content }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const parseProductNode = productNode => {
|
|
54
|
+
const mediaNode = getBinaryNodeChild(productNode, 'media')
|
|
55
|
+
const statusInfoNode = getBinaryNodeChild(productNode, 'status_info')
|
|
56
|
+
return { id: getBinaryNodeChildString(productNode, 'id'), imageUrls: parseImageUrls(mediaNode), reviewStatus: { whatsapp: getBinaryNodeChildString(statusInfoNode, 'status') }, availability: 'in stock', name: getBinaryNodeChildString(productNode, 'name'), retailerId: getBinaryNodeChildString(productNode, 'retailer_id'), url: getBinaryNodeChildString(productNode, 'url'), description: getBinaryNodeChildString(productNode, 'description'), price: +getBinaryNodeChildString(productNode, 'price'), currency: getBinaryNodeChildString(productNode, 'currency'), isHidden: productNode.attrs.is_hidden === 'true' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const uploadingNecessaryImagesOfProduct = async (product, waUploadToServer, timeoutMs = 30000) =>
|
|
60
|
+
({ ...product, images: product.images ? await uploadingNecessaryImages(product.images, waUploadToServer, timeoutMs) : product.images })
|
|
61
|
+
|
|
62
|
+
export const uploadingNecessaryImages = (images, waUploadToServer, timeoutMs = 30000) =>
|
|
63
|
+
Promise.all(images.map(async img => {
|
|
64
|
+
if ('url' in img && img.url.toString().includes('.whatsapp.net')) return { url: img.url.toString() }
|
|
65
|
+
const { stream } = await getStream(img)
|
|
66
|
+
const hasher = createHash('sha256')
|
|
67
|
+
const blocks = []
|
|
68
|
+
for await (const block of stream) { hasher.update(block); blocks.push(block) }
|
|
69
|
+
const sha = hasher.digest('base64')
|
|
70
|
+
const { directPath } = await waUploadToServer(toReadable(Buffer.concat(blocks)), { mediaType: 'product-catalog-image', fileEncSha256B64: sha, timeoutMs })
|
|
71
|
+
return { url: getUrlFromDirectPath(directPath) }
|
|
72
|
+
}))
|
|
73
|
+
|
|
74
|
+
const parseImageUrls = mediaNode => { const imgNode = getBinaryNodeChild(mediaNode, 'image'); return { requested: getBinaryNodeChildString(imgNode, 'request_image_url'), original: getBinaryNodeChildString(imgNode, 'original_image_url') } }
|
|
75
|
+
const parseStatusInfo = mediaNode => { const node = getBinaryNodeChild(mediaNode, 'status_info'); return { status: getBinaryNodeChildString(node, 'status'), canAppeal: getBinaryNodeChildString(node, 'can_appeal') === 'true' } }
|
|
@@ -0,0 +1,316 @@
|
|
|
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 { processContactAction, emitSyncActionResults } from './sync-action-utils.js'
|
|
6
|
+
import { getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary/index.js'
|
|
7
|
+
import { aesDecrypt, aesEncrypt, hmacSign } from './crypto.js'
|
|
8
|
+
import { toNumber } from './generics.js'
|
|
9
|
+
import { LT_HASH_ANTI_TAMPERING } from './lt-hash.js'
|
|
10
|
+
import { downloadContentFromMessage } from './messages-media.js'
|
|
11
|
+
|
|
12
|
+
const OP = proto.SyncdMutation.SyncdOperation
|
|
13
|
+
|
|
14
|
+
const mutationKeys = keydata => {
|
|
15
|
+
const keys = expandAppStateKeys(keydata)
|
|
16
|
+
return { indexKey: keys.indexKey, valueEncryptionKey: keys.valueEncryptionKey, valueMacKey: keys.valueMacKey, snapshotMacKey: keys.snapshotMacKey, patchMacKey: keys.patchMacKey }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const generateMac = (operation, data, keyId, key) => {
|
|
20
|
+
const opByte = operation === OP.SET ? 0x01 : 0x02
|
|
21
|
+
const keyIdBuffer = typeof keyId === 'string' ? Buffer.from(keyId, 'base64') : keyId
|
|
22
|
+
const keyData = new Uint8Array(1 + keyIdBuffer.length)
|
|
23
|
+
keyData[0] = opByte
|
|
24
|
+
keyData.set(keyIdBuffer, 1)
|
|
25
|
+
const last = new Uint8Array(8)
|
|
26
|
+
last[7] = keyData.length
|
|
27
|
+
const total = new Uint8Array(keyData.length + data.length + last.length)
|
|
28
|
+
total.set(keyData, 0); total.set(data, keyData.length); total.set(last, keyData.length + data.length)
|
|
29
|
+
return hmacSign(total, key, 'sha512').subarray(0, 32)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const to64BitNetworkOrder = e => { const buff = Buffer.alloc(8); buff.writeUint32BE(e, 4); return buff }
|
|
33
|
+
|
|
34
|
+
export const makeLtHashGenerator = ({ indexValueMap, hash }) => {
|
|
35
|
+
indexValueMap = { ...indexValueMap }
|
|
36
|
+
const addBuffs = [], subBuffs = []
|
|
37
|
+
return {
|
|
38
|
+
mix: ({ indexMac, valueMac, operation }) => {
|
|
39
|
+
const key = Buffer.from(indexMac).toString('base64')
|
|
40
|
+
const prev = indexValueMap[key]
|
|
41
|
+
if (operation === OP.REMOVE) { if (!prev) return; delete indexValueMap[key] }
|
|
42
|
+
else { addBuffs.push(valueMac); indexValueMap[key] = { valueMac } }
|
|
43
|
+
if (prev) subBuffs.push(prev.valueMac)
|
|
44
|
+
},
|
|
45
|
+
finish: async () => ({ hash: Buffer.from(LT_HASH_ANTI_TAMPERING.subtractThenAdd(hash, subBuffs, addBuffs)), indexValueMap })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const generateSnapshotMac = (lthash, version, name, key) => hmacSign(Buffer.concat([lthash, to64BitNetworkOrder(version), Buffer.from(name, 'utf-8')]), key, 'sha256')
|
|
50
|
+
const generatePatchMac = (snapshotMac, valueMacs, version, type, key) => hmacSign(Buffer.concat([snapshotMac, ...valueMacs, to64BitNetworkOrder(version), Buffer.from(type, 'utf-8')]), key)
|
|
51
|
+
|
|
52
|
+
export const newLTHashState = () => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} })
|
|
53
|
+
export const ensureLTHashStateVersion = state => { if (typeof state.version !== 'number' || isNaN(state.version)) state.version = 0; return state }
|
|
54
|
+
|
|
55
|
+
export const MAX_SYNC_ATTEMPTS = 2
|
|
56
|
+
export const isMissingKeyError = error => error?.data?.isMissingKey === true
|
|
57
|
+
export const isAppStateSyncIrrecoverable = (error, attempts) => attempts >= MAX_SYNC_ATTEMPTS || error?.name === 'TypeError'
|
|
58
|
+
|
|
59
|
+
export const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, operation }, myAppStateKeyId, state, getAppStateSyncKey) => {
|
|
60
|
+
const key = myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined
|
|
61
|
+
if (!key) throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { data: { isMissingKey: true } })
|
|
62
|
+
const encKeyId = Buffer.from(myAppStateKeyId, 'base64')
|
|
63
|
+
state = { ...state, indexValueMap: { ...state.indexValueMap } }
|
|
64
|
+
const indexBuffer = Buffer.from(JSON.stringify(index))
|
|
65
|
+
const dataProto = proto.SyncActionData.fromObject({ index: indexBuffer, value: syncAction, padding: new Uint8Array(0), version: apiVersion })
|
|
66
|
+
const encoded = proto.SyncActionData.encode(dataProto).finish()
|
|
67
|
+
const keyValue = mutationKeys(key.keyData)
|
|
68
|
+
const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
|
|
69
|
+
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
|
|
70
|
+
const indexMac = hmacSign(indexBuffer, keyValue.indexKey)
|
|
71
|
+
const generator = makeLtHashGenerator(state)
|
|
72
|
+
generator.mix({ indexMac, valueMac, operation })
|
|
73
|
+
Object.assign(state, await generator.finish())
|
|
74
|
+
state.version += 1
|
|
75
|
+
const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey)
|
|
76
|
+
const patch = {
|
|
77
|
+
patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey),
|
|
78
|
+
snapshotMac,
|
|
79
|
+
keyId: { id: encKeyId },
|
|
80
|
+
mutations: [{ operation, record: { index: { blob: indexMac }, value: { blob: Buffer.concat([encValue, valueMac]) }, keyId: { id: encKeyId } } }]
|
|
81
|
+
}
|
|
82
|
+
state.indexValueMap[indexMac.toString('base64')] = { valueMac }
|
|
83
|
+
return { patch, state }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const decodeSyncdMutations = async (msgMutations, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
|
|
87
|
+
const ltGenerator = makeLtHashGenerator(initialState)
|
|
88
|
+
const derivedKeyCache = new Map()
|
|
89
|
+
|
|
90
|
+
async function getKey(keyId) {
|
|
91
|
+
const base64Key = Buffer.from(keyId).toString('base64')
|
|
92
|
+
if (derivedKeyCache.has(base64Key)) return derivedKeyCache.get(base64Key)
|
|
93
|
+
const keyEnc = await getAppStateSyncKey(base64Key)
|
|
94
|
+
if (!keyEnc) throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true, msgMutations } })
|
|
95
|
+
const keys = mutationKeys(keyEnc.keyData)
|
|
96
|
+
derivedKeyCache.set(base64Key, keys)
|
|
97
|
+
return keys
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const msgMutation of msgMutations) {
|
|
101
|
+
const operation = 'operation' in msgMutation ? msgMutation.operation : OP.SET
|
|
102
|
+
const record = 'record' in msgMutation && msgMutation.record ? msgMutation.record : msgMutation
|
|
103
|
+
let key
|
|
104
|
+
try { key = await getKey(record.keyId.id) } catch (err) { if (isMissingKeyError(err)) throw err; continue }
|
|
105
|
+
const content = record.value.blob
|
|
106
|
+
const encContent = content.subarray(0, -32)
|
|
107
|
+
const ogValueMac = content.subarray(-32)
|
|
108
|
+
if (validateMacs) {
|
|
109
|
+
const contentHmac = generateMac(operation, encContent, record.keyId.id, key.valueMacKey)
|
|
110
|
+
if (Buffer.compare(contentHmac, ogValueMac) !== 0) continue
|
|
111
|
+
}
|
|
112
|
+
let result
|
|
113
|
+
try { result = aesDecrypt(encContent, key.valueEncryptionKey) } catch { continue }
|
|
114
|
+
const syncAction = proto.SyncActionData.decode(result)
|
|
115
|
+
if (validateMacs) {
|
|
116
|
+
const hmac = hmacSign(syncAction.index, key.indexKey)
|
|
117
|
+
if (Buffer.compare(hmac, record.index.blob) !== 0) throw new Boom('HMAC index verification failed')
|
|
118
|
+
}
|
|
119
|
+
onMutation({ syncAction, index: JSON.parse(Buffer.from(syncAction.index).toString()) })
|
|
120
|
+
ltGenerator.mix({ indexMac: record.index.blob, valueMac: ogValueMac, operation })
|
|
121
|
+
}
|
|
122
|
+
return ltGenerator.finish()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const decodeSyncdPatch = async (msg, name, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
|
|
126
|
+
if (validateMacs) {
|
|
127
|
+
const base64Key = Buffer.from(msg.keyId.id).toString('base64')
|
|
128
|
+
const mainKeyObj = await getAppStateSyncKey(base64Key)
|
|
129
|
+
if (!mainKeyObj) throw new Boom(`failed to find key "${base64Key}" to decode patch`, { data: { isMissingKey: true, msg } })
|
|
130
|
+
const mainKey = mutationKeys(mainKeyObj.keyData)
|
|
131
|
+
const mutationmacs = msg.mutations.map(m => m.record.value.blob.slice(-32))
|
|
132
|
+
const patchMac = generatePatchMac(msg.snapshotMac, mutationmacs, toNumber(msg.version.version), name, mainKey.patchMacKey)
|
|
133
|
+
if (Buffer.compare(patchMac, msg.patchMac) !== 0) throw new Boom('Invalid patch mac')
|
|
134
|
+
}
|
|
135
|
+
return decodeSyncdMutations(msg.mutations, initialState, getAppStateSyncKey, onMutation, validateMacs)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const extractSyncdPatches = async (result, options) => {
|
|
139
|
+
const syncNode = getBinaryNodeChild(result, 'sync')
|
|
140
|
+
const collectionNodes = getBinaryNodeChildren(syncNode, 'collection')
|
|
141
|
+
const final = {}
|
|
142
|
+
await Promise.all(collectionNodes.map(async collectionNode => {
|
|
143
|
+
const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
|
|
144
|
+
const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch')
|
|
145
|
+
const snapshotNode = getBinaryNodeChild(collectionNode, 'snapshot')
|
|
146
|
+
const syncds = []
|
|
147
|
+
const name = collectionNode.attrs.name
|
|
148
|
+
const hasMorePatches = collectionNode.attrs.has_more_patches === 'true'
|
|
149
|
+
let snapshot
|
|
150
|
+
if (snapshotNode?.content) {
|
|
151
|
+
if (!Buffer.isBuffer(snapshotNode)) snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
|
|
152
|
+
const blobRef = proto.ExternalBlobReference.decode(snapshotNode.content)
|
|
153
|
+
snapshot = proto.SyncdSnapshot.decode(await downloadExternalBlob(blobRef, options))
|
|
154
|
+
}
|
|
155
|
+
for (let { content } of patches) {
|
|
156
|
+
if (!content) continue
|
|
157
|
+
if (!Buffer.isBuffer(content)) content = Buffer.from(Object.values(content))
|
|
158
|
+
const syncd = proto.SyncdPatch.decode(content)
|
|
159
|
+
if (!syncd.version) syncd.version = { version: +collectionNode.attrs.version + 1 }
|
|
160
|
+
syncds.push(syncd)
|
|
161
|
+
}
|
|
162
|
+
final[name] = { patches: syncds, hasMorePatches, snapshot }
|
|
163
|
+
}))
|
|
164
|
+
return final
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const downloadExternalBlob = async (blob, options) => {
|
|
168
|
+
const stream = await downloadContentFromMessage(blob, 'md-app-state', { options })
|
|
169
|
+
const bufferArray = []
|
|
170
|
+
for await (const chunk of stream) bufferArray.push(chunk)
|
|
171
|
+
return Buffer.concat(bufferArray)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const downloadExternalPatch = async (blob, options) => {
|
|
175
|
+
const buffer = await downloadExternalBlob(blob, options)
|
|
176
|
+
return proto.SyncdMutations.decode(buffer)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, minimumVersionNumber, validateMacs = true, logger) => {
|
|
180
|
+
const newState = newLTHashState()
|
|
181
|
+
newState.version = toNumber(snapshot.version.version)
|
|
182
|
+
const mutationMap = {}
|
|
183
|
+
const areMutationsRequired = minimumVersionNumber === undefined || newState.version > minimumVersionNumber
|
|
184
|
+
const onMutation = areMutationsRequired ? mutation => { const index = mutation.syncAction.index?.toString(); mutationMap[index] = mutation } : () => { }
|
|
185
|
+
const { hash, indexValueMap } = await decodeSyncdMutations(snapshot.records, newState, getAppStateSyncKey, onMutation, validateMacs)
|
|
186
|
+
newState.hash = hash
|
|
187
|
+
newState.indexValueMap = indexValueMap
|
|
188
|
+
if (validateMacs) {
|
|
189
|
+
const base64Key = Buffer.from(snapshot.keyId.id).toString('base64')
|
|
190
|
+
const keyEnc = await getAppStateSyncKey(base64Key)
|
|
191
|
+
if (!keyEnc) throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } })
|
|
192
|
+
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, mutationKeys(keyEnc.keyData).snapshotMacKey)
|
|
193
|
+
// soft failure — MAC mismatch on snapshot doesn't abort; state is still usable
|
|
194
|
+
if (Buffer.compare(snapshot.mac, computedSnapshotMac) !== 0) logger?.warn({ name, version: newState.version }, 'LTHash verification failed on snapshot, continuing with partial state')
|
|
195
|
+
}
|
|
196
|
+
return { state: newState, mutationMap }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, options, minimumVersionNumber, logger, validateMacs = true) => {
|
|
200
|
+
const newState = { ...initial, indexValueMap: { ...initial.indexValueMap } }
|
|
201
|
+
const mutationMap = {}
|
|
202
|
+
for (const syncd of syncds) {
|
|
203
|
+
const { version, keyId, snapshotMac } = syncd
|
|
204
|
+
if (syncd.externalMutations) {
|
|
205
|
+
logger?.trace({ name, version }, 'downloading external patch')
|
|
206
|
+
const ref = await downloadExternalPatch(syncd.externalMutations, options)
|
|
207
|
+
logger?.debug({ name, version, mutations: ref.mutations.length }, 'downloaded external patch')
|
|
208
|
+
syncd.mutations?.push(...ref.mutations)
|
|
209
|
+
}
|
|
210
|
+
const patchVersion = toNumber(version.version)
|
|
211
|
+
newState.version = patchVersion
|
|
212
|
+
const shouldMutate = minimumVersionNumber === undefined || patchVersion > minimumVersionNumber
|
|
213
|
+
const onMutation = shouldMutate ? mutation => { const index = mutation.syncAction.index?.toString(); mutationMap[index] = mutation } : () => { }
|
|
214
|
+
let decodeResult
|
|
215
|
+
try { decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, onMutation, validateMacs) }
|
|
216
|
+
catch (err) { if (isMissingKeyError(err)) throw err; logger?.warn({ name, version: patchVersion, error: err.message }, 'failed to decode patch, skipping'); continue }
|
|
217
|
+
newState.hash = decodeResult.hash
|
|
218
|
+
newState.indexValueMap = decodeResult.indexValueMap
|
|
219
|
+
if (validateMacs) {
|
|
220
|
+
const base64Key = Buffer.from(keyId.id).toString('base64')
|
|
221
|
+
const keyEnc = await getAppStateSyncKey(base64Key)
|
|
222
|
+
if (!keyEnc) throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } })
|
|
223
|
+
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, mutationKeys(keyEnc.keyData).snapshotMacKey)
|
|
224
|
+
// MAC mismatch mid-patch-stream means state is corrupted from this point; stop processing
|
|
225
|
+
if (Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) { logger?.warn({ name, version: newState.version }, 'LTHash verification failed, skipping remaining patches'); break }
|
|
226
|
+
}
|
|
227
|
+
syncd.mutations = []
|
|
228
|
+
}
|
|
229
|
+
return { state: newState, mutationMap }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const chatModificationToAppPatch = (mod, jid) => {
|
|
233
|
+
const getMessageRange = lastMessages => {
|
|
234
|
+
if (!Array.isArray(lastMessages)) return lastMessages
|
|
235
|
+
const lastMsg = lastMessages[lastMessages.length - 1]
|
|
236
|
+
return {
|
|
237
|
+
lastMessageTimestamp: lastMsg?.messageTimestamp,
|
|
238
|
+
messages: lastMessages?.length ? lastMessages.map(m => {
|
|
239
|
+
if (!m.key?.id || !m.key?.remoteJid) throw new Boom('Incomplete key', { statusCode: 400, data: m })
|
|
240
|
+
if (isJidGroup(m.key.remoteJid) && !m.key.fromMe && !m.key.participant) throw new Boom('Expected not from me message to have participant', { statusCode: 400, data: m })
|
|
241
|
+
if (!m.messageTimestamp || !toNumber(m.messageTimestamp)) throw new Boom('Missing timestamp in last message list', { statusCode: 400, data: m })
|
|
242
|
+
if (m.key.participant) m.key.participant = jidNormalizedUser(m.key.participant)
|
|
243
|
+
return m
|
|
244
|
+
}) : undefined
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let patch
|
|
249
|
+
if ('mute' in mod) patch = { syncAction: { muteAction: { muted: !!mod.mute, muteEndTimestamp: mod.mute || undefined } }, index: ['mute', jid], type: 'regular_high', apiVersion: 2, operation: OP.SET }
|
|
250
|
+
else if ('archive' in mod) patch = { syncAction: { archiveChatAction: { archived: !!mod.archive, messageRange: getMessageRange(mod.lastMessages) } }, index: ['archive', jid], type: 'regular_low', apiVersion: 3, operation: OP.SET }
|
|
251
|
+
else if ('markRead' in mod) patch = { syncAction: { markChatAsReadAction: { read: mod.markRead, messageRange: getMessageRange(mod.lastMessages) } }, index: ['markChatAsRead', jid], type: 'regular_low', apiVersion: 3, operation: OP.SET }
|
|
252
|
+
else if ('deleteForMe' in mod) { const { timestamp, key, deleteMedia } = mod.deleteForMe; patch = { syncAction: { deleteMessageForMeAction: { deleteMedia, messageTimestamp: timestamp } }, index: ['deleteMessageForMe', jid, key.id, key.fromMe ? '1' : '0', '0'], type: 'regular_high', apiVersion: 3, operation: OP.SET } }
|
|
253
|
+
else if ('clear' in mod) patch = { syncAction: { clearChatAction: { messageRange: getMessageRange(mod.lastMessages) } }, index: ['clearChat', jid, '1', '0'], type: 'regular_high', apiVersion: 6, operation: OP.SET }
|
|
254
|
+
else if ('pin' in mod) patch = { syncAction: { pinAction: { pinned: !!mod.pin } }, index: ['pin_v1', jid], type: 'regular_low', apiVersion: 5, operation: OP.SET }
|
|
255
|
+
else if ('contact' in mod) patch = { syncAction: { contactAction: mod.contact || {} }, index: ['contact', jid], type: 'critical_unblock_low', apiVersion: 2, operation: mod.contact ? OP.SET : OP.REMOVE }
|
|
256
|
+
else if ('disableLinkPreviews' in mod) patch = { syncAction: { privacySettingDisableLinkPreviewsAction: mod.disableLinkPreviews || {} }, index: ['setting_disableLinkPreviews'], type: 'regular', apiVersion: 8, operation: OP.SET }
|
|
257
|
+
else if ('star' in mod) { const key = mod.star.messages[0]; patch = { syncAction: { starAction: { starred: !!mod.star.star } }, index: ['star', jid, key.id, key.fromMe ? '1' : '0', '0'], type: 'regular_low', apiVersion: 2, operation: OP.SET } }
|
|
258
|
+
else if ('delete' in mod) patch = { syncAction: { deleteChatAction: { messageRange: getMessageRange(mod.lastMessages) } }, index: ['deleteChat', jid, '1'], type: 'regular_high', apiVersion: 6, operation: OP.SET }
|
|
259
|
+
else if ('pushNameSetting' in mod) patch = { syncAction: { pushNameSetting: { name: mod.pushNameSetting } }, index: ['setting_pushName'], type: 'critical_block', apiVersion: 1, operation: OP.SET }
|
|
260
|
+
else if ('quickReply' in mod) patch = { syncAction: { quickReplyAction: { count: 0, deleted: mod.quickReply.deleted || false, keywords: [], message: mod.quickReply.message || '', shortcut: mod.quickReply.shortcut || '' } }, index: ['quick_reply', mod.quickReply.timestamp || String(Math.floor(Date.now() / 1000))], type: 'regular', apiVersion: 2, operation: OP.SET }
|
|
261
|
+
else if ('addLabel' in mod) patch = { syncAction: { labelEditAction: { name: mod.addLabel.name, color: mod.addLabel.color, predefinedId: mod.addLabel.predefinedId, deleted: mod.addLabel.deleted } }, index: ['label_edit', mod.addLabel.id], type: 'regular', apiVersion: 3, operation: OP.SET }
|
|
262
|
+
else if ('addChatLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: true } }, index: [LabelAssociationType.Chat, mod.addChatLabel.labelId, jid], type: 'regular', apiVersion: 3, operation: OP.SET }
|
|
263
|
+
else if ('removeChatLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: false } }, index: [LabelAssociationType.Chat, mod.removeChatLabel.labelId, jid], type: 'regular', apiVersion: 3, operation: OP.SET }
|
|
264
|
+
else if ('addMessageLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: true } }, index: [LabelAssociationType.Message, mod.addMessageLabel.labelId, jid, mod.addMessageLabel.messageId, '0', '0'], type: 'regular', apiVersion: 3, operation: OP.SET }
|
|
265
|
+
else if ('removeMessageLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: false } }, index: [LabelAssociationType.Message, mod.removeMessageLabel.labelId, jid, mod.removeMessageLabel.messageId, '0', '0'], type: 'regular', apiVersion: 3, operation: OP.SET }
|
|
266
|
+
else throw new Boom('not supported')
|
|
267
|
+
|
|
268
|
+
patch.syncAction.timestamp = Date.now()
|
|
269
|
+
return patch
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) => {
|
|
273
|
+
const isInitialSync = !!initialSyncOpts
|
|
274
|
+
const accountSettings = initialSyncOpts?.accountSettings
|
|
275
|
+
logger?.trace({ syncAction, initialSync: isInitialSync }, 'processing sync action')
|
|
276
|
+
const { syncAction: { value: action }, index: [type, id, msgId, fromMe] } = syncAction
|
|
277
|
+
|
|
278
|
+
const getChatUpdateConditional = (id, msgRange) => !isInitialSync ? undefined : data => {
|
|
279
|
+
const chat = data.historySets.chats[id] || data.chatUpserts[id]
|
|
280
|
+
if (!chat) return false
|
|
281
|
+
if (!msgRange) return true
|
|
282
|
+
const lastMsgTimestamp = Number(msgRange?.lastMessageTimestamp || msgRange?.lastSystemMessageTimestamp || 0)
|
|
283
|
+
return lastMsgTimestamp >= Number(chat?.lastMessageRecvTimestamp || 0)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (action?.muteAction) ev.emit('chats.update', [{ id, muteEndTime: action.muteAction?.muted ? toNumber(action.muteAction.muteEndTimestamp) : null, conditional: getChatUpdateConditional(id, undefined) }])
|
|
287
|
+
else if (action?.archiveChatAction || type === 'archive' || type === 'unarchive') { const archiveAction = action?.archiveChatAction; ev.emit('chats.update', [{ id, archived: archiveAction ? archiveAction.archived : type === 'archive', conditional: getChatUpdateConditional(id, !accountSettings?.unarchiveChats ? undefined : archiveAction?.messageRange) }]) }
|
|
288
|
+
else if (action?.markChatAsReadAction) ev.emit('chats.update', [{ id, unreadCount: isInitialSync && action.markChatAsReadAction.read ? null : action.markChatAsReadAction?.read ? 0 : -1, conditional: getChatUpdateConditional(id, action.markChatAsReadAction?.messageRange) }])
|
|
289
|
+
else if (action?.deleteMessageForMeAction || type === 'deleteMessageForMe') ev.emit('messages.delete', { keys: [{ remoteJid: id, id: msgId, fromMe: fromMe === '1' }] })
|
|
290
|
+
else if (action?.contactAction) emitSyncActionResults(ev, processContactAction(action.contactAction, id, logger))
|
|
291
|
+
else if (action?.pushNameSetting) { const name = action?.pushNameSetting?.name; if (name && me?.name !== name) ev.emit('creds.update', { me: { ...me, name } }) }
|
|
292
|
+
else if (action?.pinAction) ev.emit('chats.update', [{ id, pinned: action.pinAction?.pinned ? toNumber(action.timestamp) : null, conditional: getChatUpdateConditional(id, undefined) }])
|
|
293
|
+
else if (action?.unarchiveChatsSetting) { const unarchiveChats = !!action.unarchiveChatsSetting.unarchiveChats; ev.emit('creds.update', { accountSettings: { unarchiveChats } }); if (accountSettings) accountSettings.unarchiveChats = unarchiveChats }
|
|
294
|
+
else if (action?.starAction || type === 'star') { let starred = action?.starAction?.starred; if (typeof starred !== 'boolean') starred = syncAction.index[syncAction.index.length - 1] === '1'; ev.emit('messages.update', [{ key: { remoteJid: id, id: msgId, fromMe: fromMe === '1' }, update: { starred } }]) }
|
|
295
|
+
else if (action?.deleteChatAction || type === 'deleteChat') { if (!isInitialSync) ev.emit('chats.delete', [id]) }
|
|
296
|
+
else if (action?.labelEditAction) { const { name, color, deleted, predefinedId } = action.labelEditAction; ev.emit('labels.edit', { id, name, color, deleted, predefinedId: predefinedId ? String(predefinedId) : undefined }) }
|
|
297
|
+
else if (action?.labelAssociationAction) ev.emit('labels.association', { type: action.labelAssociationAction.labeled ? 'add' : 'remove', association: type === LabelAssociationType.Chat ? { type: LabelAssociationType.Chat, chatId: syncAction.index[2], labelId: syncAction.index[1] } : { type: LabelAssociationType.Message, chatId: syncAction.index[2], messageId: syncAction.index[3], labelId: syncAction.index[1] } })
|
|
298
|
+
else if (action?.localeSetting?.locale) ev.emit('settings.update', { setting: 'locale', value: action.localeSetting.locale })
|
|
299
|
+
else if (action?.timeFormatAction) ev.emit('settings.update', { setting: 'timeFormat', value: action.timeFormatAction })
|
|
300
|
+
else if (action?.pnForLidChatAction?.pnJid) ev.emit('lid-mapping.update', { lid: id, pn: action.pnForLidChatAction.pnJid })
|
|
301
|
+
else if (action?.privacySettingRelayAllCalls) ev.emit('settings.update', { setting: 'privacySettingRelayAllCalls', value: action.privacySettingRelayAllCalls })
|
|
302
|
+
else if (action?.statusPrivacy) ev.emit('settings.update', { setting: 'statusPrivacy', value: action.statusPrivacy })
|
|
303
|
+
else if (action?.lockChatAction) ev.emit('chats.lock', { id, locked: !!action.lockChatAction.locked })
|
|
304
|
+
else if (action?.privacySettingDisableLinkPreviewsAction) ev.emit('settings.update', { setting: 'disableLinkPreviews', value: action.privacySettingDisableLinkPreviewsAction })
|
|
305
|
+
else if (action?.notificationActivitySettingAction?.notificationActivitySetting) ev.emit('settings.update', { setting: 'notificationActivitySetting', value: action.notificationActivitySettingAction.notificationActivitySetting })
|
|
306
|
+
else if (action?.lidContactAction) ev.emit('contacts.upsert', [{ id, name: action.lidContactAction.fullName || action.lidContactAction.firstName || action.lidContactAction.username || undefined, username: action.lidContactAction.username || undefined, lid: id, phoneNumber: undefined }])
|
|
307
|
+
else if (action?.privacySettingChannelsPersonalisedRecommendationAction) ev.emit('settings.update', { setting: 'channelsPersonalisedRecommendation', value: action.privacySettingChannelsPersonalisedRecommendationAction })
|
|
308
|
+
else if (action?.aiThreadRenameAction) ev.emit('chats.update', [{ id, name: action.aiThreadRenameAction.newTitle || undefined, conditional: getChatUpdateConditional(id, undefined) }])
|
|
309
|
+
else if (action?.threadPinAction) ev.emit('chats.update', [{ id, pinned: action.threadPinAction.pinned ? toNumber(action.timestamp) : null, conditional: getChatUpdateConditional(id, undefined) }])
|
|
310
|
+
else if (action?.newsletterSavedInterestsAction) ev.emit('settings.update', { setting: 'newsletterSavedInterests', value: action.newsletterSavedInterestsAction.newsletterSavedInterests })
|
|
311
|
+
else if (action?.interactiveMessageAction) ev.emit('messages.update', [{ key: { remoteJid: id, id: msgId, fromMe: fromMe === '1' }, update: { interactiveMessageAction: action.interactiveMessageAction } }])
|
|
312
|
+
else if (action?.deviceCapabilities || action?.deviceCapabilitiesV2) ev.emit('creds.update', { deviceCapabilities: action.deviceCapabilities || action.deviceCapabilitiesV2 })
|
|
313
|
+
else if (action?.nctSaltSyncAction) ev.emit('creds.update', { nctSalt: action.nctSaltSyncAction.salt })
|
|
314
|
+
else if (action?.bubbleLockMessageAction) ev.emit('messages.update', [{ key: { remoteJid: id, id: msgId, fromMe: fromMe === '1' }, update: { bubbleLocked: !!action.bubbleLockMessageAction.locked } }])
|
|
315
|
+
else logger?.debug({ syncAction, id }, 'unprocessable update')
|
|
316
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const CompanionWebClientType = Object.freeze({ UNKNOWN: 0, CHROME: 1, EDGE: 2, FIREFOX: 3, IE: 4, OPERA: 5, SAFARI: 6, ELECTRON: 7, UWP: 8, OTHER_WEB_CLIENT: 9 })
|
|
2
|
+
|
|
3
|
+
const BROWSER_TO_COMPANION = {
|
|
4
|
+
Chrome: CompanionWebClientType.CHROME,
|
|
5
|
+
Edge: CompanionWebClientType.EDGE,
|
|
6
|
+
Firefox: CompanionWebClientType.FIREFOX,
|
|
7
|
+
IE: CompanionWebClientType.IE,
|
|
8
|
+
Opera: CompanionWebClientType.OPERA,
|
|
9
|
+
Safari: CompanionWebClientType.SAFARI
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const getCompanionWebClientType = ([os, browserName]) => {
|
|
13
|
+
if (browserName === 'Desktop') return os === 'Windows' ? CompanionWebClientType.UWP : CompanionWebClientType.ELECTRON
|
|
14
|
+
return BROWSER_TO_COMPANION[browserName] ?? CompanionWebClientType.OTHER_WEB_CLIENT
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const getCompanionPlatformId = browser => getCompanionWebClientType(browser).toString()
|
|
18
|
+
|
|
19
|
+
export const buildPairingQRData = (ref, noiseKeyB64, identityKeyB64, advB64, browser) =>
|
|
20
|
+
'https://wa.me/settings/linked_devices#' + [ref, noiseKeyB64, identityKeyB64, advB64, getCompanionPlatformId(browser)].join(',')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { proto } from '../../WAProto/index.js'
|
|
2
|
+
|
|
3
|
+
export const decodeConsumerApplication = buffer => {
|
|
4
|
+
if (!Buffer.isBuffer(buffer) && !(buffer instanceof Uint8Array)) throw new TypeError('decodeConsumerApplication: buffer must be Buffer or Uint8Array')
|
|
5
|
+
return proto.ConsumerApplication.decode(buffer)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const textOf = mt => (mt && typeof mt.text === 'string' ? mt.text : undefined)
|
|
9
|
+
|
|
10
|
+
const mapContent = content => {
|
|
11
|
+
if (!content) return null
|
|
12
|
+
switch (content.content) {
|
|
13
|
+
case 'messageText': return { conversation: textOf(content.messageText) ?? '' }
|
|
14
|
+
case 'extendedTextMessage': { const e = content.extendedTextMessage; return { extendedTextMessage: { text: textOf(e.text), matchedText: e.matchedText, canonicalUrl: e.canonicalUrl, description: e.description, title: e.title } } }
|
|
15
|
+
case 'imageMessage': return { imageMessage: { mediaPayload: content.imageMessage.image, caption: textOf(content.imageMessage.caption) } }
|
|
16
|
+
case 'videoMessage': return { videoMessage: { mediaPayload: content.videoMessage.video, caption: textOf(content.videoMessage.caption) } }
|
|
17
|
+
case 'audioMessage': return { audioMessage: { mediaPayload: content.audioMessage.audio, ptt: content.audioMessage.ptt } }
|
|
18
|
+
case 'documentMessage': return { documentMessage: { mediaPayload: content.documentMessage.document, fileName: content.documentMessage.fileName } }
|
|
19
|
+
case 'stickerMessage': return { stickerMessage: { mediaPayload: content.stickerMessage.sticker } }
|
|
20
|
+
case 'contactMessage': return { contactMessage: { mediaPayload: content.contactMessage.contact } }
|
|
21
|
+
case 'contactsArrayMessage': return { contactsArrayMessage: { displayName: content.contactsArrayMessage.displayName, contacts: content.contactsArrayMessage.contacts || [] } }
|
|
22
|
+
case 'locationMessage': { const loc = content.locationMessage.location || {}; return { locationMessage: { degreesLatitude: loc.degreesLatitude, degreesLongitude: loc.degreesLongitude, name: loc.name } } }
|
|
23
|
+
case 'liveLocationMessage': { const loc = content.liveLocationMessage.location || {}; return { liveLocationMessage: { degreesLatitude: loc.degreesLatitude, degreesLongitude: loc.degreesLongitude, caption: textOf(content.liveLocationMessage.caption) } } }
|
|
24
|
+
case 'reactionMessage': return { reactionMessage: content.reactionMessage }
|
|
25
|
+
default: return null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// primary use: newsletter/channel message decoding pipeline
|
|
30
|
+
export const consumerApplicationToMessage = app => {
|
|
31
|
+
const payload = app?.payload
|
|
32
|
+
if (!payload) return null
|
|
33
|
+
switch (payload.payload) {
|
|
34
|
+
case 'content': return mapContent(payload.content)
|
|
35
|
+
case 'applicationData': { const ad = payload.applicationData; if (ad?.applicationContent === 'revoke' && ad.revoke?.key) return { protocolMessage: { key: ad.revoke.key, type: proto.Message.ProtocolMessage.Type.REVOKE } }; return null }
|
|
36
|
+
default: return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto'
|
|
2
|
+
import { calculateAgreement, calculateSignature, verifySignature as rustVerifySignature, generateKeyPair as rustGenerateKeyPair, hkdf, md5 } from 'whatsapp-rust-bridge'
|
|
3
|
+
import { KEY_BUNDLE_TYPE } from '../Defaults/index.js'
|
|
4
|
+
|
|
5
|
+
export { hkdf, md5 }
|
|
6
|
+
|
|
7
|
+
const GCM_TAG_LENGTH = 16 // 128 >> 3
|
|
8
|
+
|
|
9
|
+
export const generateSignalPubKey = pubKey => pubKey.length === 33 ? pubKey : Buffer.concat([KEY_BUNDLE_TYPE, pubKey])
|
|
10
|
+
|
|
11
|
+
export const Curve = {
|
|
12
|
+
generateKeyPair: () => { const { pubKey, privKey } = rustGenerateKeyPair(); return { private: Buffer.from(privKey), public: Buffer.from(pubKey.slice(1)) } },
|
|
13
|
+
sharedKey: (privateKey, publicKey) => Buffer.from(calculateAgreement(generateSignalPubKey(publicKey), privateKey)),
|
|
14
|
+
sign: (privateKey, buf) => calculateSignature(privateKey, buf),
|
|
15
|
+
verify: (pubKey, message, signature) => { try { return rustVerifySignature(generateSignalPubKey(pubKey), message, signature) } catch { return false } }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const signedKeyPair = (identityKeyPair, keyId) => {
|
|
19
|
+
const preKey = Curve.generateKeyPair()
|
|
20
|
+
const pubKey = generateSignalPubKey(preKey.public)
|
|
21
|
+
return { keyPair: preKey, signature: Curve.sign(identityKeyPair.private, pubKey), keyId }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const aesEncryptGCM = (plaintext, key, iv, additionalData) => {
|
|
25
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv)
|
|
26
|
+
cipher.setAAD(additionalData)
|
|
27
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()])
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const aesDecryptGCM = (ciphertext, key, iv, additionalData) => {
|
|
31
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv)
|
|
32
|
+
decipher.setAAD(additionalData)
|
|
33
|
+
decipher.setAuthTag(ciphertext.subarray(-GCM_TAG_LENGTH))
|
|
34
|
+
return Buffer.concat([decipher.update(ciphertext.subarray(0, -GCM_TAG_LENGTH)), decipher.final()])
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const aesEncryptCTR = (plaintext, key, iv) => { const c = createCipheriv('aes-256-ctr', key, iv); return Buffer.concat([c.update(plaintext), c.final()]) }
|
|
38
|
+
export const aesDecryptCTR = (ciphertext, key, iv) => { const d = createDecipheriv('aes-256-ctr', key, iv); return Buffer.concat([d.update(ciphertext), d.final()]) }
|
|
39
|
+
|
|
40
|
+
export const aesDecryptWithIV = (buffer, key, IV) => { const d = createDecipheriv('aes-256-cbc', key, IV); return Buffer.concat([d.update(buffer), d.final()]) }
|
|
41
|
+
export const aesDecrypt = (buffer, key) => aesDecryptWithIV(buffer.subarray(16), key, buffer.subarray(0, 16))
|
|
42
|
+
|
|
43
|
+
export const aesEncryptWithIV = (buffer, key, IV) => { const c = createCipheriv('aes-256-cbc', key, IV); return Buffer.concat([c.update(buffer), c.final()]) }
|
|
44
|
+
export const aesEncrypt = (buffer, key) => { const IV = randomBytes(16); return Buffer.concat([IV, ...(() => { const c = createCipheriv('aes-256-cbc', key, IV); return [c.update(buffer), c.final()] })()]) }
|
|
45
|
+
|
|
46
|
+
export const hmacSign = (buffer, key, variant = 'sha256') => createHmac(variant, key).update(buffer).digest()
|
|
47
|
+
export const sha256 = buffer => createHash('sha256').update(buffer).digest()
|
|
48
|
+
|
|
49
|
+
export const derivePairingCodeKey = async (pairingCode, salt) => {
|
|
50
|
+
const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(pairingCode), { name: 'PBKDF2' }, false, ['deriveBits'])
|
|
51
|
+
return Buffer.from(await crypto.subtle.deriveBits({ name: 'PBKDF2', salt: salt instanceof Uint8Array ? salt : new Uint8Array(salt), iterations: 2 << 16, hash: 'SHA-256' }, keyMaterial, 256))
|
|
52
|
+
}
|