mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
feat: implement external patch parsing + app state sync on login
This commit is contained in:
@@ -35,7 +35,8 @@ export const MEDIA_PATH_MAP: { [T in MediaType]: string } = {
|
|||||||
document: '/mms/document',
|
document: '/mms/document',
|
||||||
audio: '/mms/audio',
|
audio: '/mms/audio',
|
||||||
sticker: '/mms/image',
|
sticker: '/mms/image',
|
||||||
history: ''
|
history: '',
|
||||||
|
'md-app-state': ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
|
export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
|
||||||
|
|||||||
@@ -167,7 +167,16 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionSync = async(collections: { name: WAPatchName, version: number }[]) => {
|
const resyncAppState = async(collections: WAPatchName[], fromScratch: boolean = false, returnSnapshot: boolean = false) => {
|
||||||
|
const states = { } as { [T in WAPatchName]: LTHashState }
|
||||||
|
for(const name of collections) {
|
||||||
|
let state: LTHashState = fromScratch ? undefined : await authState.keys.getAppStateSyncVersion(name)
|
||||||
|
if(!state) state = { version: 0, hash: Buffer.alloc(128), mutations: [] }
|
||||||
|
|
||||||
|
states[name] = state
|
||||||
|
|
||||||
|
logger.info(`resyncing ${name} from v${state.version}`)
|
||||||
|
}
|
||||||
const result = await query({
|
const result = await query({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -180,25 +189,35 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
tag: 'sync',
|
tag: 'sync',
|
||||||
attrs: { },
|
attrs: { },
|
||||||
content: collections.map(
|
content: collections.map(
|
||||||
({ name, version }) => ({
|
(name) => ({
|
||||||
tag: 'collection',
|
tag: 'collection',
|
||||||
attrs: { name, version: version.toString(), return_snapshot: 'true' }
|
attrs: {
|
||||||
|
name,
|
||||||
|
version: states[name].version.toString(),
|
||||||
|
return_snapshot: returnSnapshot ? 'true' : 'false'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
const syncNode = getBinaryNodeChild(result, 'sync')
|
|
||||||
const collectionNodes = getBinaryNodeChildren(syncNode, 'collection')
|
const decoded = extractSyncdPatches(result) // extract from binary node
|
||||||
return collectionNodes.reduce(
|
|
||||||
(dict, node) => {
|
for(const key in decoded) {
|
||||||
const snapshotNode = getBinaryNodeChild(node, 'snapshot')
|
const name = key as WAPatchName
|
||||||
if(snapshotNode) {
|
// only process if there are syncd patches
|
||||||
dict[node.attrs.name] = snapshotNode.content as Uint8Array
|
if(decoded[name].length) {
|
||||||
}
|
const { newMutations, state: newState } = await decodePatches(name, decoded[name], states[name], authState, true)
|
||||||
return dict
|
|
||||||
}, { } as { [P in WAPatchName]: Uint8Array }
|
await authState.keys.setAppStateSyncVersion(name, newState)
|
||||||
)
|
|
||||||
|
logger.info(`synced ${name} to v${newState.version}`)
|
||||||
|
processSyncActions(newMutations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.emit('auth-state.update', authState)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -282,7 +301,6 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const processSyncActions = (actions: ChatMutation[]) => {
|
const processSyncActions = (actions: ChatMutation[]) => {
|
||||||
|
|
||||||
const updates: { [jid: string]: Partial<Chat> } = {}
|
const updates: { [jid: string]: Partial<Chat> } = {}
|
||||||
const contactUpdates: { [jid: string]: Contact } = {}
|
const contactUpdates: { [jid: string]: Contact } = {}
|
||||||
const msgDeletes: proto.IMessageKey[] = []
|
const msgDeletes: proto.IMessageKey[] = []
|
||||||
@@ -337,10 +355,10 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
const appPatch = async(patchCreate: WAPatchCreate) => {
|
const appPatch = async(patchCreate: WAPatchCreate) => {
|
||||||
const name = patchCreate.type
|
const name = patchCreate.type
|
||||||
try {
|
try {
|
||||||
await resyncState(name, false)
|
await resyncAppState([name])
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.info({ name, error: error.stack }, 'failed to sync state from version, trying from scratch')
|
logger.info({ name, error: error.stack }, 'failed to sync state from version, trying from scratch')
|
||||||
await resyncState(name, true)
|
await resyncAppState([name], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { patch, state } = await encodeSyncdPatch(
|
const { patch, state } = await encodeSyncdPatch(
|
||||||
@@ -349,7 +367,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
)
|
)
|
||||||
const initial = await authState.keys.getAppStateSyncVersion(name)
|
const initial = await authState.keys.getAppStateSyncVersion(name)
|
||||||
// temp: verify it was encoded correctly
|
// temp: verify it was encoded correctly
|
||||||
const result = await decodePatches({ syncds: [{ ...patch, version: { version: state.version }, }], name }, initial, authState)
|
const result = await decodePatches(name, [{ ...patch, version: { version: state.version }, }], initial, authState)
|
||||||
|
|
||||||
const node: BinaryNode = {
|
const node: BinaryNode = {
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
@@ -396,52 +414,6 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
return appPatch(patch)
|
return appPatch(patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchAppState = async(name: WAPatchName, fromVersion: number) => {
|
|
||||||
const result = await query({
|
|
||||||
tag: 'iq',
|
|
||||||
attrs: {
|
|
||||||
type: 'set',
|
|
||||||
xmlns: 'w:sync:app:state',
|
|
||||||
to: S_WHATSAPP_NET
|
|
||||||
},
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
tag: 'sync',
|
|
||||||
attrs: { },
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
tag: 'collection',
|
|
||||||
attrs: {
|
|
||||||
name,
|
|
||||||
version: fromVersion.toString(),
|
|
||||||
return_snapshot: 'false'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const resyncState = async(name: WAPatchName, fromScratch: boolean) => {
|
|
||||||
let state: LTHashState = fromScratch ? undefined : await authState.keys.getAppStateSyncVersion(name)
|
|
||||||
if(!state) state = { version: 0, hash: Buffer.alloc(128), mutations: [] }
|
|
||||||
|
|
||||||
logger.info(`resyncing ${name} from v${state.version}`)
|
|
||||||
|
|
||||||
const result = await fetchAppState(name, state.version)
|
|
||||||
const decoded = extractSyncdPatches(result) // extract from binary node
|
|
||||||
const { newMutations, state: newState } = await decodePatches(decoded, state, authState, true)
|
|
||||||
|
|
||||||
await authState.keys.setAppStateSyncVersion(name, newState)
|
|
||||||
|
|
||||||
logger.info(`synced ${name} to v${newState.version}`)
|
|
||||||
processSyncActions(newMutations)
|
|
||||||
|
|
||||||
ev.emit('auth-state.update', authState)
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.on('CB:presence', handlePresenceUpdate)
|
ws.on('CB:presence', handlePresenceUpdate)
|
||||||
ws.on('CB:chatstate', handlePresenceUpdate)
|
ws.on('CB:chatstate', handlePresenceUpdate)
|
||||||
|
|
||||||
@@ -461,7 +433,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
ws.on('CB:notification,type:server_sync', (node: BinaryNode) => {
|
ws.on('CB:notification,type:server_sync', (node: BinaryNode) => {
|
||||||
const update = getBinaryNodeChild(node, 'collection')
|
const update = getBinaryNodeChild(node, 'collection')
|
||||||
if(update) {
|
if(update) {
|
||||||
resyncState(update.attrs.name as WAPatchName, false)
|
const name = update.attrs.name as WAPatchName
|
||||||
|
resyncAppState([name], false)
|
||||||
.catch(err => logger.error({ trace: err.stack, node }, `failed to sync state`))
|
.catch(err => logger.error({ trace: err.stack, node }, `failed to sync state`))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -471,6 +444,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
sendPresenceUpdate('available')
|
sendPresenceUpdate('available')
|
||||||
fetchBlocklist()
|
fetchBlocklist()
|
||||||
fetchPrivacySettings()
|
fetchPrivacySettings()
|
||||||
|
resyncAppState([ 'critical_block', 'critical_unblock_low' ])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -485,7 +459,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
fetchStatus,
|
fetchStatus,
|
||||||
updateProfilePicture,
|
updateProfilePicture,
|
||||||
updateBlockStatus,
|
updateBlockStatus,
|
||||||
resyncState,
|
resyncAppState,
|
||||||
chatModify,
|
chatModify,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ type WithDimensions = {
|
|||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
}
|
}
|
||||||
export type MediaType = 'image' | 'video' | 'sticker' | 'audio' | 'document' | 'history'
|
export type MediaType = 'image' | 'video' | 'sticker' | 'audio' | 'document' | 'history' | 'md-app-state'
|
||||||
export type AnyMediaMessageContent = (
|
export type AnyMediaMessageContent = (
|
||||||
({
|
({
|
||||||
image: WAMediaUpload
|
image: WAMediaUpload
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { proto } from '../../WAProto'
|
|||||||
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
|
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
|
||||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren } from '../WABinary'
|
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren } from '../WABinary'
|
||||||
import { toNumber } from './generics'
|
import { toNumber } from './generics'
|
||||||
|
import { downloadContentFromMessage, } from './messages-media'
|
||||||
|
|
||||||
export const mutationKeys = (keydata: Uint8Array) => {
|
export const mutationKeys = (keydata: Uint8Array) => {
|
||||||
const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
|
const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
|
||||||
@@ -235,26 +236,44 @@ export const decodeSyncdPatch = async(
|
|||||||
|
|
||||||
export const extractSyncdPatches = (result: BinaryNode) => {
|
export const extractSyncdPatches = (result: BinaryNode) => {
|
||||||
const syncNode = getBinaryNodeChild(result, 'sync')
|
const syncNode = getBinaryNodeChild(result, 'sync')
|
||||||
const collectionNode = getBinaryNodeChild(syncNode, 'collection')
|
const collectionNodes = getBinaryNodeChildren(syncNode, 'collection')
|
||||||
const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
|
|
||||||
|
const final = { } as { [T in WAPatchName]: proto.ISyncdPatch[] }
|
||||||
|
for(const collectionNode of collectionNodes) {
|
||||||
|
const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
|
||||||
|
|
||||||
const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch')
|
const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch')
|
||||||
const syncds: proto.ISyncdPatch[] = []
|
const syncds: proto.ISyncdPatch[] = []
|
||||||
const name = collectionNode.attrs.name as WAPatchName
|
const name = collectionNode.attrs.name as WAPatchName
|
||||||
for(let { content } of patches) {
|
for(let { content } of patches) {
|
||||||
if(content) {
|
if(content) {
|
||||||
const syncd = proto.SyncdPatch.decode(content! as Uint8Array)
|
const syncd = proto.SyncdPatch.decode(content! as Uint8Array)
|
||||||
if(!syncd.version) {
|
if(!syncd.version) {
|
||||||
syncd.version = { version: +collectionNode.attrs.version+1 }
|
syncd.version = { version: +collectionNode.attrs.version+1 }
|
||||||
|
}
|
||||||
|
syncds.push(syncd)
|
||||||
}
|
}
|
||||||
syncds.push(syncd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final[name] = syncds
|
||||||
}
|
}
|
||||||
return { syncds, name }
|
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|
||||||
|
export const downloadExternalPatch = async(blob: proto.IExternalBlobReference) => {
|
||||||
|
const stream = await downloadContentFromMessage(blob, 'md-app-state')
|
||||||
|
let buffer = Buffer.from([])
|
||||||
|
for await(const chunk of stream) {
|
||||||
|
buffer = Buffer.concat([buffer, chunk])
|
||||||
|
}
|
||||||
|
const syncData = proto.SyncdMutations.decode(buffer)
|
||||||
|
return syncData
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decodePatches = async(
|
export const decodePatches = async(
|
||||||
{ syncds, name }: ReturnType<typeof extractSyncdPatches>,
|
name: WAPatchName,
|
||||||
|
syncds: proto.ISyncdPatch[],
|
||||||
initial: LTHashState,
|
initial: LTHashState,
|
||||||
auth: AuthenticationState,
|
auth: AuthenticationState,
|
||||||
validateMacs: boolean = true
|
validateMacs: boolean = true
|
||||||
@@ -263,8 +282,13 @@ export const decodePatches = async(
|
|||||||
|
|
||||||
let current = initial.hash
|
let current = initial.hash
|
||||||
let currentVersion = initial.version
|
let currentVersion = initial.version
|
||||||
|
|
||||||
for(const syncd of syncds) {
|
for(const syncd of syncds) {
|
||||||
const { mutations, version, keyId, snapshotMac } = syncd
|
const { mutations, version, keyId, snapshotMac, externalMutations } = syncd
|
||||||
|
if(externalMutations) {
|
||||||
|
const ref = await downloadExternalPatch(externalMutations)
|
||||||
|
mutations.push(...ref.mutations)
|
||||||
|
}
|
||||||
const macs = mutations.map(
|
const macs = mutations.map(
|
||||||
m => ({
|
m => ({
|
||||||
operation: m.operation!,
|
operation: m.operation!,
|
||||||
@@ -308,7 +332,7 @@ export const decodePatches = async(
|
|||||||
const result = mutationKeys(keyEnc.keyData!)
|
const result = mutationKeys(keyEnc.keyData!)
|
||||||
const computedSnapshotMac = generateSnapshotMac(current, currentVersion, name, result.snapshotMacKey)
|
const computedSnapshotMac = generateSnapshotMac(current, currentVersion, name, result.snapshotMacKey)
|
||||||
if(Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
|
if(Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
|
||||||
throw new Boom(`failed to verify LTHash at ${currentVersion}`, { statusCode: 500 })
|
throw new Boom(`failed to verify LTHash at ${currentVersion} of ${name}`, { statusCode: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ import { hkdf } from './crypto'
|
|||||||
import { DEFAULT_ORIGIN } from '../Defaults'
|
import { DEFAULT_ORIGIN } from '../Defaults'
|
||||||
|
|
||||||
export const hkdfInfoKey = (type: MediaType) => {
|
export const hkdfInfoKey = (type: MediaType) => {
|
||||||
if(type === 'sticker') type = 'image'
|
let str: string = type
|
||||||
|
if(type === 'sticker') str = 'image'
|
||||||
|
if(type === 'md-app-state') str = 'App State'
|
||||||
|
|
||||||
let hkdfInfo = type[0].toUpperCase() + type.slice(1)
|
let hkdfInfo = str[0].toUpperCase() + str.slice(1)
|
||||||
return `WhatsApp ${hkdfInfo} Keys`
|
return `WhatsApp ${hkdfInfo} Keys`
|
||||||
}
|
}
|
||||||
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ const MIMETYPE_MAP: { [T in MediaType]: string } = {
|
|||||||
document: 'application/pdf',
|
document: 'application/pdf',
|
||||||
audio: 'audio/ogg; codecs=opus',
|
audio: 'audio/ogg; codecs=opus',
|
||||||
sticker: 'image/webp',
|
sticker: 'image/webp',
|
||||||
history: 'application/x-protobuf'
|
history: 'application/x-protobuf',
|
||||||
|
"md-app-state": 'application/x-protobuf',
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageTypeProto = {
|
const MessageTypeProto = {
|
||||||
|
|||||||
Reference in New Issue
Block a user