mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
feat(labels): modify chat utils
This commit is contained in:
@@ -37,13 +37,13 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
const processingMutex = makeMutex()
|
||||
|
||||
/** helper function to fetch the given app state sync key */
|
||||
const getAppStateSyncKey = async(keyId: string) => {
|
||||
const getAppStateSyncKey = async (keyId: string) => {
|
||||
const { [keyId]: key } = await authState.keys.get('app-state-sync-key', [keyId])
|
||||
return key
|
||||
}
|
||||
|
||||
const fetchPrivacySettings = async(force: boolean = false) => {
|
||||
if(!privacySettings || force) {
|
||||
const fetchPrivacySettings = async (force: boolean = false) => {
|
||||
if (!privacySettings || force) {
|
||||
const { content } = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
@@ -52,7 +52,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
type: 'get'
|
||||
},
|
||||
content: [
|
||||
{ tag: 'privacy', attrs: { } }
|
||||
{ tag: 'privacy', attrs: {} }
|
||||
]
|
||||
})
|
||||
privacySettings = reduceBinaryNodeToDictionary(content?.[0] as BinaryNode, 'category')
|
||||
@@ -62,7 +62,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
/** helper function to run a privacy IQ query */
|
||||
const privacyQuery = async(name: string, value: string) => {
|
||||
const privacyQuery = async (name: string, value: string) => {
|
||||
await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
@@ -83,31 +83,31 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
})
|
||||
}
|
||||
|
||||
const updateLastSeenPrivacy = async(value: WAPrivacyValue) => {
|
||||
const updateLastSeenPrivacy = async (value: WAPrivacyValue) => {
|
||||
await privacyQuery('last', value)
|
||||
}
|
||||
|
||||
const updateOnlinePrivacy = async(value: WAPrivacyOnlineValue) => {
|
||||
const updateOnlinePrivacy = async (value: WAPrivacyOnlineValue) => {
|
||||
await privacyQuery('online', value)
|
||||
}
|
||||
|
||||
const updateProfilePicturePrivacy = async(value: WAPrivacyValue) => {
|
||||
const updateProfilePicturePrivacy = async (value: WAPrivacyValue) => {
|
||||
await privacyQuery('profile', value)
|
||||
}
|
||||
|
||||
const updateStatusPrivacy = async(value: WAPrivacyValue) => {
|
||||
const updateStatusPrivacy = async (value: WAPrivacyValue) => {
|
||||
await privacyQuery('status', value)
|
||||
}
|
||||
|
||||
const updateReadReceiptsPrivacy = async(value: WAReadReceiptsValue) => {
|
||||
const updateReadReceiptsPrivacy = async (value: WAReadReceiptsValue) => {
|
||||
await privacyQuery('readreceipts', value)
|
||||
}
|
||||
|
||||
const updateGroupsAddPrivacy = async(value: WAPrivacyValue) => {
|
||||
const updateGroupsAddPrivacy = async (value: WAPrivacyValue) => {
|
||||
await privacyQuery('groupadd', value)
|
||||
}
|
||||
|
||||
const updateDefaultDisappearingMode = async(duration: number) => {
|
||||
const updateDefaultDisappearingMode = async (duration: number) => {
|
||||
await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
@@ -118,14 +118,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
content: [{
|
||||
tag: 'disappearing_mode',
|
||||
attrs: {
|
||||
duration : duration.toString()
|
||||
duration: duration.toString()
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
/** helper function to run a generic IQ query */
|
||||
const interactiveQuery = async(userNodes: BinaryNode[], queryNode: BinaryNode) => {
|
||||
const interactiveQuery = async (userNodes: BinaryNode[], queryNode: BinaryNode) => {
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
@@ -146,12 +146,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
content: [
|
||||
{
|
||||
tag: 'query',
|
||||
attrs: { },
|
||||
content: [ queryNode ]
|
||||
attrs: {},
|
||||
content: [queryNode]
|
||||
},
|
||||
{
|
||||
tag: 'list',
|
||||
attrs: { },
|
||||
attrs: {},
|
||||
content: userNodes
|
||||
}
|
||||
]
|
||||
@@ -166,22 +166,22 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
return users
|
||||
}
|
||||
|
||||
const onWhatsApp = async(...jids: string[]) => {
|
||||
const onWhatsApp = async (...jids: string[]) => {
|
||||
const results = await interactiveQuery(
|
||||
[
|
||||
{
|
||||
tag: 'user',
|
||||
attrs: { },
|
||||
attrs: {},
|
||||
content: jids.map(
|
||||
jid => ({
|
||||
tag: 'contact',
|
||||
attrs: { },
|
||||
attrs: {},
|
||||
content: `+${jid}`
|
||||
})
|
||||
)
|
||||
}
|
||||
],
|
||||
{ tag: 'contact', attrs: { } }
|
||||
{ tag: 'contact', attrs: {} }
|
||||
)
|
||||
|
||||
return results.map(user => {
|
||||
@@ -190,12 +190,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}).filter(item => item.exists)
|
||||
}
|
||||
|
||||
const fetchStatus = async(jid: string) => {
|
||||
const fetchStatus = async (jid: string) => {
|
||||
const [result] = await interactiveQuery(
|
||||
[{ tag: 'user', attrs: { jid } }],
|
||||
{ tag: 'status', attrs: { } }
|
||||
{ tag: 'status', attrs: {} }
|
||||
)
|
||||
if(result) {
|
||||
if (result) {
|
||||
const status = getBinaryNodeChild(result, 'status')
|
||||
return {
|
||||
status: status?.content!.toString(),
|
||||
@@ -205,7 +205,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
/** update the profile picture for yourself or a group */
|
||||
const updateProfilePicture = async(jid: string, content: WAMediaUpload) => {
|
||||
const updateProfilePicture = async (jid: string, content: WAMediaUpload) => {
|
||||
const { img } = await generateProfilePicture(content)
|
||||
await query({
|
||||
tag: 'iq',
|
||||
@@ -225,7 +225,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
/** remove the profile picture for yourself or a group */
|
||||
const removeProfilePicture = async(jid: string) => {
|
||||
const removeProfilePicture = async (jid: string) => {
|
||||
await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
@@ -237,7 +237,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
/** update the profile status for yourself */
|
||||
const updateProfileStatus = async(status: string) => {
|
||||
const updateProfileStatus = async (status: string) => {
|
||||
await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
@@ -248,18 +248,18 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
content: [
|
||||
{
|
||||
tag: 'status',
|
||||
attrs: { },
|
||||
attrs: {},
|
||||
content: Buffer.from(status, 'utf-8')
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const updateProfileName = async(name: string) => {
|
||||
const updateProfileName = async (name: string) => {
|
||||
await chatModify({ pushNameSetting: name }, '')
|
||||
}
|
||||
|
||||
const fetchBlocklist = async() => {
|
||||
const fetchBlocklist = async () => {
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
@@ -274,7 +274,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
.map(n => n.attrs.jid)
|
||||
}
|
||||
|
||||
const updateBlockStatus = async(jid: string, action: 'block' | 'unblock') => {
|
||||
const updateBlockStatus = async (jid: string, action: 'block' | 'unblock') => {
|
||||
await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
@@ -294,7 +294,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
})
|
||||
}
|
||||
|
||||
const getBusinessProfile = async(jid: string): Promise<WABusinessProfile | void> => {
|
||||
const getBusinessProfile = async (jid: string): Promise<WABusinessProfile | void> => {
|
||||
const results = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
@@ -314,7 +314,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
|
||||
const profileNode = getBinaryNodeChild(results, 'business_profile')
|
||||
const profiles = getBinaryNodeChild(profileNode, 'profile')
|
||||
if(profiles) {
|
||||
if (profiles) {
|
||||
const address = getBinaryNodeChild(profiles, 'address')
|
||||
const description = getBinaryNodeChild(profiles, 'description')
|
||||
const website = getBinaryNodeChild(profiles, 'website')
|
||||
@@ -340,7 +340,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateAccountSyncTimestamp = async(fromTimestamp: number | string) => {
|
||||
const updateAccountSyncTimestamp = async (fromTimestamp: number | string) => {
|
||||
logger.info({ fromTimestamp }, 'requesting account sync')
|
||||
await sendNode({
|
||||
tag: 'iq',
|
||||
@@ -376,30 +376,30 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
}
|
||||
|
||||
const resyncAppState = ev.createBufferedFunction(async(collections: readonly WAPatchName[], isInitialSync: boolean) => {
|
||||
const resyncAppState = ev.createBufferedFunction(async (collections: readonly WAPatchName[], isInitialSync: boolean) => {
|
||||
// we use this to determine which events to fire
|
||||
// otherwise when we resync from scratch -- all notifications will fire
|
||||
const initialVersionMap: { [T in WAPatchName]?: number } = { }
|
||||
const globalMutationMap: ChatMutationMap = { }
|
||||
const initialVersionMap: { [T in WAPatchName]?: number } = {}
|
||||
const globalMutationMap: ChatMutationMap = {}
|
||||
|
||||
await authState.keys.transaction(
|
||||
async() => {
|
||||
async () => {
|
||||
const collectionsToHandle = new Set<string>(collections)
|
||||
// in case something goes wrong -- ensure we don't enter a loop that cannot be exited from
|
||||
const attemptsMap: { [T in WAPatchName]?: number } = { }
|
||||
const attemptsMap: { [T in WAPatchName]?: number } = {}
|
||||
// keep executing till all collections are done
|
||||
// sometimes a single patch request will not return all the patches (God knows why)
|
||||
// so we fetch till they're all done (this is determined by the "has_more_patches" flag)
|
||||
while(collectionsToHandle.size) {
|
||||
const states = { } as { [T in WAPatchName]: LTHashState }
|
||||
while (collectionsToHandle.size) {
|
||||
const states = {} as { [T in WAPatchName]: LTHashState }
|
||||
const nodes: BinaryNode[] = []
|
||||
|
||||
for(const name of collectionsToHandle) {
|
||||
for (const name of collectionsToHandle) {
|
||||
const result = await authState.keys.get('app-state-sync-version', [name])
|
||||
let state = result[name]
|
||||
|
||||
if(state) {
|
||||
if(typeof initialVersionMap[name] === 'undefined') {
|
||||
if (state) {
|
||||
if (typeof initialVersionMap[name] === 'undefined') {
|
||||
initialVersionMap[name] = state.version
|
||||
}
|
||||
} else {
|
||||
@@ -412,7 +412,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
|
||||
nodes.push({
|
||||
tag: 'collection',
|
||||
attrs: {
|
||||
attrs: {
|
||||
name,
|
||||
version: state.version.toString(),
|
||||
// return snapshot if being synced from scratch
|
||||
@@ -431,7 +431,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
content: [
|
||||
{
|
||||
tag: 'sync',
|
||||
attrs: { },
|
||||
attrs: {},
|
||||
content: nodes
|
||||
}
|
||||
]
|
||||
@@ -439,11 +439,11 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
|
||||
// extract from binary node
|
||||
const decoded = await extractSyncdPatches(result, config?.options)
|
||||
for(const key in decoded) {
|
||||
for (const key in decoded) {
|
||||
const name = key as WAPatchName
|
||||
const { patches, hasMorePatches, snapshot } = decoded[name]
|
||||
try {
|
||||
if(snapshot) {
|
||||
if (snapshot) {
|
||||
const { state: newState, mutationMap } = await decodeSyncdSnapshot(
|
||||
name,
|
||||
snapshot,
|
||||
@@ -460,7 +460,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
// only process if there are syncd patches
|
||||
if(patches.length) {
|
||||
if (patches.length) {
|
||||
const { state: newState, mutationMap } = await decodePatches(
|
||||
name,
|
||||
patches,
|
||||
@@ -480,12 +480,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
Object.assign(globalMutationMap, mutationMap)
|
||||
}
|
||||
|
||||
if(hasMorePatches) {
|
||||
if (hasMorePatches) {
|
||||
logger.info(`${name} has more patches...`)
|
||||
} else { // collection is done with sync
|
||||
collectionsToHandle.delete(name)
|
||||
}
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
// if retry attempts overshoot
|
||||
// or key not found
|
||||
const isIrrecoverableError = attemptsMap[name]! >= MAX_SYNC_ATTEMPTS
|
||||
@@ -499,7 +499,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
// increment number of retries
|
||||
attemptsMap[name] = (attemptsMap[name] || 0) + 1
|
||||
|
||||
if(isIrrecoverableError) {
|
||||
if (isIrrecoverableError) {
|
||||
// stop retrying
|
||||
collectionsToHandle.delete(name)
|
||||
}
|
||||
@@ -510,17 +510,17 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
)
|
||||
|
||||
const { onMutation } = newAppStateChunkHandler(isInitialSync)
|
||||
for(const key in globalMutationMap) {
|
||||
for (const key in globalMutationMap) {
|
||||
onMutation(globalMutationMap[key])
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* fetch the profile picture of a user/group
|
||||
* type = "preview" for a low res picture
|
||||
* type = "image for the high res picture"
|
||||
*/
|
||||
const profilePictureUrl = async(jid: string, type: 'preview' | 'image' = 'preview', timeoutMs?: number) => {
|
||||
* fetch the profile picture of a user/group
|
||||
* type = "preview" for a low res picture
|
||||
* type = "image for the high res picture"
|
||||
*/
|
||||
const profilePictureUrl = async (jid: string, type: 'preview' | 'image' = 'preview', timeoutMs?: number) => {
|
||||
jid = jidNormalizedUser(jid)
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
@@ -537,10 +537,10 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
return child?.attrs?.url
|
||||
}
|
||||
|
||||
const sendPresenceUpdate = async(type: WAPresence, toJid?: string) => {
|
||||
const sendPresenceUpdate = async (type: WAPresence, toJid?: string) => {
|
||||
const me = authState.creds.me!
|
||||
if(type === 'available' || type === 'unavailable') {
|
||||
if(!me!.name) {
|
||||
if (type === 'available' || type === 'unavailable') {
|
||||
if (!me!.name) {
|
||||
logger.warn('no name present, ignoring presence update request...')
|
||||
return
|
||||
}
|
||||
@@ -564,7 +564,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
content: [
|
||||
{
|
||||
tag: type === 'recording' ? 'composing' : type,
|
||||
attrs: type === 'recording' ? { media : 'audio' } : {}
|
||||
attrs: type === 'recording' ? { media: 'audio' } : {}
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -587,7 +587,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
? [
|
||||
{
|
||||
tag: 'tctoken',
|
||||
attrs: { },
|
||||
attrs: {},
|
||||
content: tcToken
|
||||
}
|
||||
]
|
||||
@@ -600,23 +600,23 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
const jid = attrs.from
|
||||
const participant = attrs.participant || attrs.from
|
||||
|
||||
if(shouldIgnoreJid(jid)) {
|
||||
if (shouldIgnoreJid(jid)) {
|
||||
return
|
||||
}
|
||||
|
||||
if(tag === 'presence') {
|
||||
if (tag === 'presence') {
|
||||
presence = {
|
||||
lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available',
|
||||
lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined
|
||||
}
|
||||
} else if(Array.isArray(content)) {
|
||||
} else if (Array.isArray(content)) {
|
||||
const [firstChild] = content
|
||||
let type = firstChild.tag as WAPresence
|
||||
if(type === 'paused') {
|
||||
if (type === 'paused') {
|
||||
type = 'available'
|
||||
}
|
||||
|
||||
if(firstChild.attrs?.media === 'audio') {
|
||||
if (firstChild.attrs?.media === 'audio') {
|
||||
type = 'recording'
|
||||
}
|
||||
|
||||
@@ -625,15 +625,15 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
logger.error({ tag, attrs, content }, 'recv invalid presence node')
|
||||
}
|
||||
|
||||
if(presence) {
|
||||
if (presence) {
|
||||
ev.emit('presence.update', { id: jid, presences: { [participant]: presence } })
|
||||
}
|
||||
}
|
||||
|
||||
const appPatch = async(patchCreate: WAPatchCreate) => {
|
||||
const appPatch = async (patchCreate: WAPatchCreate) => {
|
||||
const name = patchCreate.type
|
||||
const myAppStateKeyId = authState.creds.myAppStateKeyId
|
||||
if(!myAppStateKeyId) {
|
||||
if (!myAppStateKeyId) {
|
||||
throw new Boom('App state key not present!', { statusCode: 400 })
|
||||
}
|
||||
|
||||
@@ -641,9 +641,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
let encodeResult: { patch: proto.ISyncdPatch, state: LTHashState }
|
||||
|
||||
await processingMutex.mutex(
|
||||
async() => {
|
||||
async () => {
|
||||
await authState.keys.transaction(
|
||||
async() => {
|
||||
async () => {
|
||||
logger.debug({ patch: patchCreate }, 'applying app patch')
|
||||
|
||||
await resyncAppState([name], false)
|
||||
@@ -669,7 +669,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
content: [
|
||||
{
|
||||
tag: 'sync',
|
||||
attrs: { },
|
||||
attrs: {},
|
||||
content: [
|
||||
{
|
||||
tag: 'collection',
|
||||
@@ -681,7 +681,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
content: [
|
||||
{
|
||||
tag: 'patch',
|
||||
attrs: { },
|
||||
attrs: {},
|
||||
content: proto.SyncdPatch.encode(patch).finish()
|
||||
}
|
||||
]
|
||||
@@ -698,7 +698,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
)
|
||||
|
||||
if(config.emitOwnEvents) {
|
||||
if (config.emitOwnEvents) {
|
||||
const { onMutation } = newAppStateChunkHandler(false)
|
||||
const { mutationMap } = await decodePatches(
|
||||
name,
|
||||
@@ -709,14 +709,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
undefined,
|
||||
logger,
|
||||
)
|
||||
for(const key in mutationMap) {
|
||||
for (const key in mutationMap) {
|
||||
onMutation(mutationMap[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** sending abt props may fix QR scan fail if server expects */
|
||||
const fetchAbt = async() => {
|
||||
const fetchAbt = async () => {
|
||||
const abtNode = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
@@ -731,8 +731,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
|
||||
const propsNode = getBinaryNodeChild(abtNode, 'props')
|
||||
|
||||
let props: { [_: string]: string } = { }
|
||||
if(propsNode) {
|
||||
let props: { [_: string]: string } = {}
|
||||
if (propsNode) {
|
||||
props = reduceBinaryNodeToDictionary(propsNode, 'prop')
|
||||
}
|
||||
|
||||
@@ -742,7 +742,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
/** sending non-abt props may fix QR scan fail if server expects */
|
||||
const fetchProps = async() => {
|
||||
const fetchProps = async () => {
|
||||
const resultNode = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
@@ -751,14 +751,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
type: 'get',
|
||||
},
|
||||
content: [
|
||||
{ tag: 'props', attrs: { } }
|
||||
{ tag: 'props', attrs: {} }
|
||||
]
|
||||
})
|
||||
|
||||
const propsNode = getBinaryNodeChild(resultNode, 'props')
|
||||
|
||||
let props: { [_: string]: string } = { }
|
||||
if(propsNode) {
|
||||
let props: { [_: string]: string } = {}
|
||||
if (propsNode) {
|
||||
props = reduceBinaryNodeToDictionary(propsNode, 'prop')
|
||||
}
|
||||
|
||||
@@ -768,20 +768,66 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* modify a chat -- mark unread, read etc.
|
||||
* lastMessages must be sorted in reverse chronologically
|
||||
* requires the last messages till the last message received; required for archive & unread
|
||||
*/
|
||||
* modify a chat -- mark unread, read etc.
|
||||
* lastMessages must be sorted in reverse chronologically
|
||||
* requires the last messages till the last message received; required for archive & unread
|
||||
*/
|
||||
const chatModify = (mod: ChatModification, jid: string) => {
|
||||
const patch = chatModificationToAppPatch(mod, jid)
|
||||
return appPatch(patch)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds label for the chats
|
||||
*/
|
||||
const addChatLabel = (jid: string, labelId: string) => {
|
||||
return chatModify({
|
||||
addChatLabel: {
|
||||
labelId
|
||||
}
|
||||
}, jid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes label for the chat
|
||||
*/
|
||||
const removeChatLabel = (jid: string, labelId: string) => {
|
||||
return chatModify({
|
||||
removeChatLabel: {
|
||||
labelId
|
||||
}
|
||||
}, jid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds label for the message
|
||||
*/
|
||||
const addMessageLabel = (jid: string, messageId: string, labelId: string) => {
|
||||
return chatModify({
|
||||
addMessageLabel: {
|
||||
messageId,
|
||||
labelId
|
||||
}
|
||||
}, jid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes label for the message
|
||||
*/
|
||||
const removeMessageLabel = (jid: string, messageId: string, labelId: string) => {
|
||||
return chatModify({
|
||||
removeMessageLabel: {
|
||||
messageId,
|
||||
labelId
|
||||
}
|
||||
}, jid)
|
||||
}
|
||||
|
||||
/**
|
||||
* queries need to be fired on connection open
|
||||
* help ensure parity with WA Web
|
||||
* */
|
||||
const executeInitQueries = async() => {
|
||||
const executeInitQueries = async () => {
|
||||
await Promise.all([
|
||||
fetchAbt(),
|
||||
fetchProps(),
|
||||
@@ -790,19 +836,19 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
])
|
||||
}
|
||||
|
||||
const upsertMessage = ev.createBufferedFunction(async(msg: WAMessage, type: MessageUpsertType) => {
|
||||
const upsertMessage = ev.createBufferedFunction(async (msg: WAMessage, type: MessageUpsertType) => {
|
||||
ev.emit('messages.upsert', { messages: [msg], type })
|
||||
|
||||
if(!!msg.pushName) {
|
||||
if (!!msg.pushName) {
|
||||
let jid = msg.key.fromMe ? authState.creds.me!.id : (msg.key.participant || msg.key.remoteJid)
|
||||
jid = jidNormalizedUser(jid!)
|
||||
|
||||
if(!msg.key.fromMe) {
|
||||
if (!msg.key.fromMe) {
|
||||
ev.emit('contacts.update', [{ id: jid, notify: msg.pushName, verifiedName: msg.verifiedBizName! }])
|
||||
}
|
||||
|
||||
// update our pushname too
|
||||
if(msg.key.fromMe && msg.pushName && authState.creds.me?.name !== msg.pushName) {
|
||||
if (msg.key.fromMe && msg.pushName && authState.creds.me?.name !== msg.pushName) {
|
||||
ev.emit('creds.update', { me: { ...authState.creds.me!, name: msg.pushName! } })
|
||||
}
|
||||
}
|
||||
@@ -815,14 +861,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
)
|
||||
: false
|
||||
|
||||
if(historyMsg && !authState.creds.myAppStateKeyId) {
|
||||
if (historyMsg && !authState.creds.myAppStateKeyId) {
|
||||
logger.warn('skipping app state sync, as myAppStateKeyId is not set')
|
||||
pendingAppStateSync = true
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
(async() => {
|
||||
if(
|
||||
(async () => {
|
||||
if (
|
||||
historyMsg
|
||||
&& authState.creds.myAppStateKeyId
|
||||
) {
|
||||
@@ -844,7 +890,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
)
|
||||
])
|
||||
|
||||
if(
|
||||
if (
|
||||
msg.message?.protocolMessage?.appStateSyncKeyShare
|
||||
&& pendingAppStateSync
|
||||
) {
|
||||
@@ -853,14 +899,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
async function doAppStateSync() {
|
||||
if(!authState.creds.accountSyncCounter) {
|
||||
if (!authState.creds.accountSyncCounter) {
|
||||
logger.info('doing initial app state sync')
|
||||
await resyncAppState(ALL_WA_PATCH_NAMES, true)
|
||||
|
||||
const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1
|
||||
ev.emit('creds.update', { accountSyncCounter })
|
||||
|
||||
if(needToFlushWithAppStateSync) {
|
||||
if (needToFlushWithAppStateSync) {
|
||||
logger.debug('flushing with app state sync')
|
||||
ev.flush()
|
||||
}
|
||||
@@ -871,31 +917,31 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
ws.on('CB:presence', handlePresenceUpdate)
|
||||
ws.on('CB:chatstate', handlePresenceUpdate)
|
||||
|
||||
ws.on('CB:ib,,dirty', async(node: BinaryNode) => {
|
||||
ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
|
||||
const { attrs } = getBinaryNodeChild(node, 'dirty')!
|
||||
const type = attrs.type
|
||||
switch (type) {
|
||||
case 'account_sync':
|
||||
if(attrs.timestamp) {
|
||||
let { lastAccountSyncTimestamp } = authState.creds
|
||||
if(lastAccountSyncTimestamp) {
|
||||
await updateAccountSyncTimestamp(lastAccountSyncTimestamp)
|
||||
case 'account_sync':
|
||||
if (attrs.timestamp) {
|
||||
let { lastAccountSyncTimestamp } = authState.creds
|
||||
if (lastAccountSyncTimestamp) {
|
||||
await updateAccountSyncTimestamp(lastAccountSyncTimestamp)
|
||||
}
|
||||
|
||||
lastAccountSyncTimestamp = +attrs.timestamp
|
||||
ev.emit('creds.update', { lastAccountSyncTimestamp })
|
||||
}
|
||||
|
||||
lastAccountSyncTimestamp = +attrs.timestamp
|
||||
ev.emit('creds.update', { lastAccountSyncTimestamp })
|
||||
}
|
||||
|
||||
break
|
||||
default:
|
||||
logger.info({ node }, 'received unknown sync')
|
||||
break
|
||||
break
|
||||
default:
|
||||
logger.info({ node }, 'received unknown sync')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
|
||||
if(connection === 'open') {
|
||||
if(fireInitQueries) {
|
||||
if (connection === 'open') {
|
||||
if (fireInitQueries) {
|
||||
executeInitQueries()
|
||||
.catch(
|
||||
error => onUnexpectedError(error, 'init queries')
|
||||
@@ -908,11 +954,11 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
)
|
||||
}
|
||||
|
||||
if(receivedPendingNotifications) {
|
||||
if (receivedPendingNotifications) {
|
||||
// if we don't have the app state key
|
||||
// we keep buffering events until we finally have
|
||||
// the key and can sync the messages
|
||||
if(!authState.creds?.myAppStateKeyId) {
|
||||
if (!authState.creds?.myAppStateKeyId) {
|
||||
ev.buffer()
|
||||
needToFlushWithAppStateSync = true
|
||||
}
|
||||
@@ -945,6 +991,10 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
updateDefaultDisappearingMode,
|
||||
getBusinessProfile,
|
||||
resyncAppState,
|
||||
chatModify
|
||||
chatModify,
|
||||
addChatLabel,
|
||||
removeChatLabel,
|
||||
addMessageLabel,
|
||||
removeMessageLabel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { proto } from '../../WAProto'
|
||||
import type { AccountSettings } from './Auth'
|
||||
import type { BufferedEventData } from './Events'
|
||||
import type { ChatLabelAssociationActionBody } from './LabelAssociation'
|
||||
import type { MessageLabelAssociationActionBody } from './LabelAssociation'
|
||||
import type { MinimalMessage } from './Message'
|
||||
|
||||
/** privacy settings in WhatsApp Web */
|
||||
@@ -14,11 +16,11 @@ export type WAReadReceiptsValue = 'all' | 'none'
|
||||
export type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
|
||||
|
||||
export const ALL_WA_PATCH_NAMES = [
|
||||
'critical_block',
|
||||
'critical_unblock_low',
|
||||
'regular_high',
|
||||
'regular_low',
|
||||
'regular'
|
||||
'critical_block',
|
||||
'critical_unblock_low',
|
||||
'regular_high',
|
||||
'regular_low',
|
||||
'regular'
|
||||
] as const
|
||||
|
||||
export type WAPatchName = typeof ALL_WA_PATCH_NAMES[number]
|
||||
@@ -77,7 +79,7 @@ export type ChatModification =
|
||||
mute: number | null
|
||||
}
|
||||
| {
|
||||
clear: 'all' | { messages: {id: string, fromMe?: boolean, timestamp: number}[] }
|
||||
clear: 'all' | { messages: { id: string, fromMe?: boolean, timestamp: number }[] }
|
||||
}
|
||||
| {
|
||||
star: {
|
||||
@@ -90,6 +92,11 @@ export type ChatModification =
|
||||
lastMessages: LastMessageList
|
||||
}
|
||||
| { delete: true, lastMessages: LastMessageList }
|
||||
// Label assosiation
|
||||
| { addChatLabel: ChatLabelAssociationActionBody }
|
||||
| { removeChatLabel: ChatLabelAssociationActionBody }
|
||||
| { addMessageLabel: MessageLabelAssociationActionBody }
|
||||
| { removeMessageLabel: MessageLabelAssociationActionBody }
|
||||
|
||||
export type InitialReceivedChatsState = {
|
||||
[jid: string]: {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { WACallEvent } from './Call'
|
||||
import { Chat, ChatUpdate, PresenceData } from './Chat'
|
||||
import { Contact } from './Contact'
|
||||
import { GroupMetadata, ParticipantAction } from './GroupMetadata'
|
||||
import { Label } from './Label'
|
||||
import { LabelAssociation } from './LabelAssociation'
|
||||
import { MessageUpsertType, MessageUserReceiptUpdate, WAMessage, WAMessageKey, WAMessageUpdate } from './Message'
|
||||
import { ConnectionState } from './State'
|
||||
|
||||
@@ -54,6 +56,8 @@ export type BaileysEventMap = {
|
||||
'blocklist.update': { blocklist: string[], type: 'add' | 'remove' }
|
||||
/** Receive an update on a call, including when the call was received, rejected, accepted */
|
||||
'call': WACallEvent[]
|
||||
'labels.edit': Label
|
||||
'labels.association': { association: LabelAssociation, type: 'add' | 'remove' }
|
||||
}
|
||||
|
||||
export type BufferedEventData = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AxiosRequestConfig } from 'axios'
|
||||
import type { Logger } from 'pino'
|
||||
import { proto } from '../../WAProto'
|
||||
import { BaileysEventEmitter, Chat, ChatModification, ChatMutation, ChatUpdate, Contact, InitialAppStateSyncOptions, LastMessageList, LTHashState, WAPatchCreate, WAPatchName } from '../Types'
|
||||
import { ChatLabelAssociation, LabelAssociationType, MessageLabelAssociation } from '../Types/LabelAssociation'
|
||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary'
|
||||
import { aesDecrypt, aesEncrypt, hkdf, hmacSign } from './crypto'
|
||||
import { toNumber } from './generics'
|
||||
@@ -28,24 +29,24 @@ const generateMac = (operation: proto.SyncdMutation.SyncdOperation, data: Buffer
|
||||
const getKeyData = () => {
|
||||
let r: number
|
||||
switch (operation) {
|
||||
case proto.SyncdMutation.SyncdOperation.SET:
|
||||
r = 0x01
|
||||
break
|
||||
case proto.SyncdMutation.SyncdOperation.REMOVE:
|
||||
r = 0x02
|
||||
break
|
||||
case proto.SyncdMutation.SyncdOperation.SET:
|
||||
r = 0x01
|
||||
break
|
||||
case proto.SyncdMutation.SyncdOperation.REMOVE:
|
||||
r = 0x02
|
||||
break
|
||||
}
|
||||
|
||||
const buff = Buffer.from([r])
|
||||
return Buffer.concat([ buff, Buffer.from(keyId as any, 'base64') ])
|
||||
return Buffer.concat([buff, Buffer.from(keyId as any, 'base64')])
|
||||
}
|
||||
|
||||
const keyData = getKeyData()
|
||||
|
||||
const last = Buffer.alloc(8) // 8 bytes
|
||||
last.set([ keyData.length ], last.length - 1)
|
||||
last.set([keyData.length], last.length - 1)
|
||||
|
||||
const total = Buffer.concat([ keyData, data, last ])
|
||||
const total = Buffer.concat([keyData, data, last])
|
||||
const hmac = hmacSign(total, key, 'sha512')
|
||||
|
||||
return hmac.slice(0, 32)
|
||||
@@ -68,8 +69,8 @@ const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' |
|
||||
mix: ({ indexMac, valueMac, operation }: Mac) => {
|
||||
const indexMacBase64 = Buffer.from(indexMac).toString('base64')
|
||||
const prevOp = indexValueMap[indexMacBase64]
|
||||
if(operation === proto.SyncdMutation.SyncdOperation.REMOVE) {
|
||||
if(!prevOp) {
|
||||
if (operation === proto.SyncdMutation.SyncdOperation.REMOVE) {
|
||||
if (!prevOp) {
|
||||
throw new Boom('tried remove, but no previous op', { data: { indexMac, valueMac } })
|
||||
}
|
||||
|
||||
@@ -81,7 +82,7 @@ const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' |
|
||||
indexValueMap[indexMacBase64] = { valueMac }
|
||||
}
|
||||
|
||||
if(prevOp) {
|
||||
if (prevOp) {
|
||||
subBuffs.push(new Uint8Array(prevOp.valueMac).buffer)
|
||||
}
|
||||
},
|
||||
@@ -119,14 +120,14 @@ const generatePatchMac = (snapshotMac: Uint8Array, valueMacs: Uint8Array[], vers
|
||||
|
||||
export const newLTHashState = (): LTHashState => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} })
|
||||
|
||||
export const encodeSyncdPatch = async(
|
||||
export const encodeSyncdPatch = async (
|
||||
{ type, index, syncAction, apiVersion, operation }: WAPatchCreate,
|
||||
myAppStateKeyId: string,
|
||||
state: LTHashState,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey
|
||||
) => {
|
||||
const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined
|
||||
if(!key) {
|
||||
if (!key) {
|
||||
throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 })
|
||||
}
|
||||
|
||||
@@ -170,7 +171,7 @@ export const encodeSyncdPatch = async(
|
||||
blob: indexMac
|
||||
},
|
||||
value: {
|
||||
blob: Buffer.concat([ encValue, valueMac ])
|
||||
blob: Buffer.concat([encValue, valueMac])
|
||||
},
|
||||
keyId: { id: encKeyId }
|
||||
}
|
||||
@@ -184,7 +185,7 @@ export const encodeSyncdPatch = async(
|
||||
return { patch, state }
|
||||
}
|
||||
|
||||
export const decodeSyncdMutations = async(
|
||||
export const decodeSyncdMutations = async (
|
||||
msgMutations: (proto.ISyncdMutation | proto.ISyncdRecord)[],
|
||||
initialState: LTHashState,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||
@@ -195,7 +196,7 @@ export const decodeSyncdMutations = async(
|
||||
// indexKey used to HMAC sign record.index.blob
|
||||
// valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]
|
||||
// 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
|
||||
for(const msgMutation of msgMutations!) {
|
||||
for (const msgMutation of msgMutations!) {
|
||||
// if it's a syncdmutation, get the operation property
|
||||
// otherwise, if it's only a record -- it'll be a SET mutation
|
||||
const operation = 'operation' in msgMutation ? msgMutation.operation : proto.SyncdMutation.SyncdOperation.SET
|
||||
@@ -205,9 +206,9 @@ export const decodeSyncdMutations = async(
|
||||
const content = Buffer.from(record.value!.blob!)
|
||||
const encContent = content.slice(0, -32)
|
||||
const ogValueMac = content.slice(-32)
|
||||
if(validateMacs) {
|
||||
if (validateMacs) {
|
||||
const contentHmac = generateMac(operation!, encContent, record.keyId!.id!, key.valueMacKey)
|
||||
if(Buffer.compare(contentHmac, ogValueMac) !== 0) {
|
||||
if (Buffer.compare(contentHmac, ogValueMac) !== 0) {
|
||||
throw new Boom('HMAC content verification failed')
|
||||
}
|
||||
}
|
||||
@@ -215,9 +216,9 @@ export const decodeSyncdMutations = async(
|
||||
const result = aesDecrypt(encContent, key.valueEncryptionKey)
|
||||
const syncAction = proto.SyncActionData.decode(result)
|
||||
|
||||
if(validateMacs) {
|
||||
if (validateMacs) {
|
||||
const hmac = hmacSign(syncAction.index, key.indexKey)
|
||||
if(Buffer.compare(hmac, record.index!.blob!) !== 0) {
|
||||
if (Buffer.compare(hmac, record.index!.blob!) !== 0) {
|
||||
throw new Boom('HMAC index verification failed')
|
||||
}
|
||||
}
|
||||
@@ -237,7 +238,7 @@ export const decodeSyncdMutations = async(
|
||||
async function getKey(keyId: Uint8Array) {
|
||||
const base64Key = Buffer.from(keyId!).toString('base64')
|
||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||
if(!keyEnc) {
|
||||
if (!keyEnc) {
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 404, data: { msgMutations } })
|
||||
}
|
||||
|
||||
@@ -245,7 +246,7 @@ export const decodeSyncdMutations = async(
|
||||
}
|
||||
}
|
||||
|
||||
export const decodeSyncdPatch = async(
|
||||
export const decodeSyncdPatch = async (
|
||||
msg: proto.ISyncdPatch,
|
||||
name: WAPatchName,
|
||||
initialState: LTHashState,
|
||||
@@ -253,10 +254,10 @@ export const decodeSyncdPatch = async(
|
||||
onMutation: (mutation: ChatMutation) => void,
|
||||
validateMacs: boolean
|
||||
) => {
|
||||
if(validateMacs) {
|
||||
if (validateMacs) {
|
||||
const base64Key = Buffer.from(msg.keyId!.id!).toString('base64')
|
||||
const mainKeyObj = await getAppStateSyncKey(base64Key)
|
||||
if(!mainKeyObj) {
|
||||
if (!mainKeyObj) {
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } })
|
||||
}
|
||||
|
||||
@@ -264,7 +265,7 @@ export const decodeSyncdPatch = async(
|
||||
const mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32))
|
||||
|
||||
const patchMac = generatePatchMac(msg.snapshotMac!, mutationmacs, toNumber(msg.version!.version!), name, mainKey.patchMacKey)
|
||||
if(Buffer.compare(patchMac, msg.patchMac!) !== 0) {
|
||||
if (Buffer.compare(patchMac, msg.patchMac!) !== 0) {
|
||||
throw new Boom('Invalid patch mac')
|
||||
}
|
||||
}
|
||||
@@ -273,14 +274,14 @@ export const decodeSyncdPatch = async(
|
||||
return result
|
||||
}
|
||||
|
||||
export const extractSyncdPatches = async(
|
||||
export const extractSyncdPatches = async (
|
||||
result: BinaryNode,
|
||||
options: AxiosRequestConfig<any>
|
||||
) => {
|
||||
const syncNode = getBinaryNodeChild(result, 'sync')
|
||||
const collectionNodes = getBinaryNodeChildren(syncNode, 'collection')
|
||||
|
||||
const final = { } as { [T in WAPatchName]: { patches: proto.ISyncdPatch[], hasMorePatches: boolean, snapshot?: proto.ISyncdSnapshot } }
|
||||
const final = {} as { [T in WAPatchName]: { patches: proto.ISyncdPatch[], hasMorePatches: boolean, snapshot?: proto.ISyncdSnapshot } }
|
||||
await Promise.all(
|
||||
collectionNodes.map(
|
||||
async collectionNode => {
|
||||
@@ -295,26 +296,26 @@ export const extractSyncdPatches = async(
|
||||
const hasMorePatches = collectionNode.attrs.has_more_patches === 'true'
|
||||
|
||||
let snapshot: proto.ISyncdSnapshot | undefined = undefined
|
||||
if(snapshotNode && !!snapshotNode.content) {
|
||||
if(!Buffer.isBuffer(snapshotNode)) {
|
||||
if (snapshotNode && !!snapshotNode.content) {
|
||||
if (!Buffer.isBuffer(snapshotNode)) {
|
||||
snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
|
||||
}
|
||||
|
||||
const blobRef = proto.ExternalBlobReference.decode(
|
||||
snapshotNode.content! as Buffer
|
||||
snapshotNode.content! as Buffer
|
||||
)
|
||||
const data = await downloadExternalBlob(blobRef, options)
|
||||
snapshot = proto.SyncdSnapshot.decode(data)
|
||||
}
|
||||
|
||||
for(let { content } of patches) {
|
||||
if(content) {
|
||||
if(!Buffer.isBuffer(content)) {
|
||||
for (let { content } of patches) {
|
||||
if (content) {
|
||||
if (!Buffer.isBuffer(content)) {
|
||||
content = Buffer.from(Object.values(content))
|
||||
}
|
||||
|
||||
const syncd = proto.SyncdPatch.decode(content! as Uint8Array)
|
||||
if(!syncd.version) {
|
||||
if (!syncd.version) {
|
||||
syncd.version = { version: +collectionNode.attrs.version + 1 }
|
||||
}
|
||||
|
||||
@@ -331,7 +332,7 @@ export const extractSyncdPatches = async(
|
||||
}
|
||||
|
||||
|
||||
export const downloadExternalBlob = async(
|
||||
export const downloadExternalBlob = async (
|
||||
blob: proto.IExternalBlobReference,
|
||||
options: AxiosRequestConfig<any>
|
||||
) => {
|
||||
@@ -344,7 +345,7 @@ export const downloadExternalBlob = async(
|
||||
return Buffer.concat(bufferArray)
|
||||
}
|
||||
|
||||
export const downloadExternalPatch = async(
|
||||
export const downloadExternalPatch = async (
|
||||
blob: proto.IExternalBlobReference,
|
||||
options: AxiosRequestConfig<any>
|
||||
) => {
|
||||
@@ -353,7 +354,7 @@ export const downloadExternalPatch = async(
|
||||
return syncData
|
||||
}
|
||||
|
||||
export const decodeSyncdSnapshot = async(
|
||||
export const decodeSyncdSnapshot = async (
|
||||
name: WAPatchName,
|
||||
snapshot: proto.ISyncdSnapshot,
|
||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||
@@ -382,16 +383,16 @@ export const decodeSyncdSnapshot = async(
|
||||
newState.hash = hash
|
||||
newState.indexValueMap = indexValueMap
|
||||
|
||||
if(validateMacs) {
|
||||
if (validateMacs) {
|
||||
const base64Key = Buffer.from(snapshot.keyId!.id!).toString('base64')
|
||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||
if(!keyEnc) {
|
||||
if (!keyEnc) {
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
||||
}
|
||||
|
||||
const result = mutationKeys(keyEnc.keyData!)
|
||||
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||
if(Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
|
||||
if (Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
|
||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`)
|
||||
}
|
||||
}
|
||||
@@ -402,7 +403,7 @@ export const decodeSyncdSnapshot = async(
|
||||
}
|
||||
}
|
||||
|
||||
export const decodePatches = async(
|
||||
export const decodePatches = async (
|
||||
name: WAPatchName,
|
||||
syncds: proto.ISyncdPatch[],
|
||||
initial: LTHashState,
|
||||
@@ -417,12 +418,12 @@ export const decodePatches = async(
|
||||
indexValueMap: { ...initial.indexValueMap }
|
||||
}
|
||||
|
||||
const mutationMap: ChatMutationMap = { }
|
||||
const mutationMap: ChatMutationMap = {}
|
||||
|
||||
for(let i = 0;i < syncds.length;i++) {
|
||||
for (let i = 0; i < syncds.length; i++) {
|
||||
const syncd = syncds[i]
|
||||
const { version, keyId, snapshotMac } = syncd
|
||||
if(syncd.externalMutations) {
|
||||
if (syncd.externalMutations) {
|
||||
logger?.trace({ name, version }, 'downloading external patch')
|
||||
const ref = await downloadExternalPatch(syncd.externalMutations, options)
|
||||
logger?.debug({ name, version, mutations: ref.mutations.length }, 'downloaded external patch')
|
||||
@@ -451,16 +452,16 @@ export const decodePatches = async(
|
||||
newState.hash = decodeResult.hash
|
||||
newState.indexValueMap = decodeResult.indexValueMap
|
||||
|
||||
if(validateMacs) {
|
||||
if (validateMacs) {
|
||||
const base64Key = Buffer.from(keyId!.id!).toString('base64')
|
||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||
if(!keyEnc) {
|
||||
if (!keyEnc) {
|
||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
||||
}
|
||||
|
||||
const result = mutationKeys(keyEnc.keyData!)
|
||||
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||
if(Buffer.compare(snapshotMac!, computedSnapshotMac) !== 0) {
|
||||
if (Buffer.compare(snapshotMac!, computedSnapshotMac) !== 0) {
|
||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`)
|
||||
}
|
||||
}
|
||||
@@ -479,25 +480,25 @@ export const chatModificationToAppPatch = (
|
||||
const OP = proto.SyncdMutation.SyncdOperation
|
||||
const getMessageRange = (lastMessages: LastMessageList) => {
|
||||
let messageRange: proto.SyncActionValue.ISyncActionMessageRange
|
||||
if(Array.isArray(lastMessages)) {
|
||||
if (Array.isArray(lastMessages)) {
|
||||
const lastMsg = lastMessages[lastMessages.length - 1]
|
||||
messageRange = {
|
||||
lastMessageTimestamp: lastMsg?.messageTimestamp,
|
||||
messages: lastMessages?.length ? lastMessages.map(
|
||||
m => {
|
||||
if(!m.key?.id || !m.key?.remoteJid) {
|
||||
if (!m.key?.id || !m.key?.remoteJid) {
|
||||
throw new Boom('Incomplete key', { statusCode: 400, data: m })
|
||||
}
|
||||
|
||||
if(isJidGroup(m.key.remoteJid) && !m.key.fromMe && !m.key.participant) {
|
||||
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 })
|
||||
}
|
||||
|
||||
if(!m.messageTimestamp || !toNumber(m.messageTimestamp)) {
|
||||
if (!m.messageTimestamp || !toNumber(m.messageTimestamp)) {
|
||||
throw new Boom('Missing timestamp in last message list', { statusCode: 400, data: m })
|
||||
}
|
||||
|
||||
if(m.key.participant) {
|
||||
if (m.key.participant) {
|
||||
m.key.participant = jidNormalizedUser(m.key.participant)
|
||||
}
|
||||
|
||||
@@ -513,7 +514,7 @@ export const chatModificationToAppPatch = (
|
||||
}
|
||||
|
||||
let patch: WAPatchCreate
|
||||
if('mute' in mod) {
|
||||
if ('mute' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
muteAction: {
|
||||
@@ -526,7 +527,7 @@ export const chatModificationToAppPatch = (
|
||||
apiVersion: 2,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else if('archive' in mod) {
|
||||
} else if ('archive' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
archiveChatAction: {
|
||||
@@ -539,7 +540,7 @@ export const chatModificationToAppPatch = (
|
||||
apiVersion: 3,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else if('markRead' in mod) {
|
||||
} else if ('markRead' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
markChatAsReadAction: {
|
||||
@@ -552,8 +553,8 @@ export const chatModificationToAppPatch = (
|
||||
apiVersion: 3,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else if('clear' in mod) {
|
||||
if(mod.clear === 'all') {
|
||||
} else if ('clear' in mod) {
|
||||
if (mod.clear === 'all') {
|
||||
throw new Boom('not supported')
|
||||
} else {
|
||||
const key = mod.clear.messages[0]
|
||||
@@ -570,7 +571,7 @@ export const chatModificationToAppPatch = (
|
||||
operation: OP.SET
|
||||
}
|
||||
}
|
||||
} else if('pin' in mod) {
|
||||
} else if ('pin' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
pinAction: {
|
||||
@@ -582,7 +583,7 @@ export const chatModificationToAppPatch = (
|
||||
apiVersion: 5,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else if('delete' in mod) {
|
||||
} else if ('delete' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
deleteChatAction: {
|
||||
@@ -594,7 +595,7 @@ export const chatModificationToAppPatch = (
|
||||
apiVersion: 6,
|
||||
operation: OP.SET
|
||||
}
|
||||
} else if('pushNameSetting' in mod) {
|
||||
} else if ('pushNameSetting' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
pushNameSetting: {
|
||||
@@ -606,6 +607,68 @@ export const chatModificationToAppPatch = (
|
||||
apiVersion: 1,
|
||||
operation: OP.SET,
|
||||
}
|
||||
} else if ('addChatLabel' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
labelAssociationAction: {
|
||||
labeled: true,
|
||||
}
|
||||
},
|
||||
index: [LabelAssociationType.Chat, mod.addChatLabel.labelId, jid],
|
||||
type: 'regular',
|
||||
apiVersion: 3,
|
||||
operation: OP.SET,
|
||||
}
|
||||
} else if ('removeChatLabel' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
labelAssociationAction: {
|
||||
labeled: false,
|
||||
}
|
||||
},
|
||||
index: [LabelAssociationType.Chat, mod.removeChatLabel.labelId, jid],
|
||||
type: 'regular',
|
||||
apiVersion: 3,
|
||||
operation: OP.SET,
|
||||
}
|
||||
} 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,
|
||||
}
|
||||
} 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,
|
||||
}
|
||||
} else {
|
||||
throw new Boom('not supported')
|
||||
}
|
||||
@@ -632,7 +695,7 @@ export const processSyncAction = (
|
||||
index: [type, id, msgId, fromMe]
|
||||
} = syncAction
|
||||
|
||||
if(action?.muteAction) {
|
||||
if (action?.muteAction) {
|
||||
ev.emit(
|
||||
'chats.update',
|
||||
[
|
||||
@@ -645,7 +708,7 @@ export const processSyncAction = (
|
||||
}
|
||||
]
|
||||
)
|
||||
} else if(action?.archiveChatAction || type === 'archive' || type === 'unarchive') {
|
||||
} else if (action?.archiveChatAction || type === 'archive' || type === 'unarchive') {
|
||||
// okay so we've to do some annoying computation here
|
||||
// when we're initially syncing the app state
|
||||
// there are a few cases we need to handle
|
||||
@@ -659,7 +722,7 @@ export const processSyncAction = (
|
||||
const archiveAction = action?.archiveChatAction
|
||||
const isArchived = archiveAction
|
||||
? archiveAction.archived
|
||||
: type === 'archive'
|
||||
: type === 'archive'
|
||||
// // basically we don't need to fire an "archive" update if the chat is being marked unarchvied
|
||||
// // this only applies for the initial sync
|
||||
// if(isInitialSync && !isArchived) {
|
||||
@@ -674,7 +737,7 @@ export const processSyncAction = (
|
||||
archived: isArchived,
|
||||
conditional: getChatUpdateConditional(id, msgRange)
|
||||
}])
|
||||
} else if(action?.markChatAsReadAction) {
|
||||
} else if (action?.markChatAsReadAction) {
|
||||
const markReadAction = action.markChatAsReadAction
|
||||
// basically we don't need to fire an "read" update if the chat is being marked as read
|
||||
// because the chat is read by default
|
||||
@@ -686,38 +749,40 @@ export const processSyncAction = (
|
||||
unreadCount: isNullUpdate ? null : !!markReadAction?.read ? 0 : -1,
|
||||
conditional: getChatUpdateConditional(id, markReadAction?.messageRange)
|
||||
}])
|
||||
} else if(action?.deleteMessageForMeAction || type === 'deleteMessageForMe') {
|
||||
ev.emit('messages.delete', { keys: [
|
||||
{
|
||||
remoteJid: id,
|
||||
id: msgId,
|
||||
fromMe: fromMe === '1'
|
||||
}
|
||||
] })
|
||||
} else if(action?.contactAction) {
|
||||
} else if (action?.deleteMessageForMeAction || type === 'deleteMessageForMe') {
|
||||
ev.emit('messages.delete', {
|
||||
keys: [
|
||||
{
|
||||
remoteJid: id,
|
||||
id: msgId,
|
||||
fromMe: fromMe === '1'
|
||||
}
|
||||
]
|
||||
})
|
||||
} else if (action?.contactAction) {
|
||||
ev.emit('contacts.upsert', [{ id, name: action.contactAction!.fullName! }])
|
||||
} else if(action?.pushNameSetting) {
|
||||
} else if (action?.pushNameSetting) {
|
||||
const name = action?.pushNameSetting?.name
|
||||
if(name && me?.name !== name) {
|
||||
if (name && me?.name !== name) {
|
||||
ev.emit('creds.update', { me: { ...me, name } })
|
||||
}
|
||||
} else if(action?.pinAction) {
|
||||
} else if (action?.pinAction) {
|
||||
ev.emit('chats.update', [{
|
||||
id,
|
||||
pinned: action.pinAction?.pinned ? toNumber(action.timestamp!) : null,
|
||||
conditional: getChatUpdateConditional(id, undefined)
|
||||
}])
|
||||
} else if(action?.unarchiveChatsSetting) {
|
||||
} else if (action?.unarchiveChatsSetting) {
|
||||
const unarchiveChats = !!action.unarchiveChatsSetting.unarchiveChats
|
||||
ev.emit('creds.update', { accountSettings: { unarchiveChats } })
|
||||
|
||||
logger?.info(`archive setting updated => '${action.unarchiveChatsSetting.unarchiveChats}'`)
|
||||
if(accountSettings) {
|
||||
if (accountSettings) {
|
||||
accountSettings.unarchiveChats = unarchiveChats
|
||||
}
|
||||
} else if(action?.starAction || type === 'star') {
|
||||
} else if (action?.starAction || type === 'star') {
|
||||
let starred = action?.starAction?.starred
|
||||
if(typeof starred !== 'boolean') {
|
||||
if (typeof starred !== 'boolean') {
|
||||
starred = syncAction.index[syncAction.index.length - 1] === '1'
|
||||
}
|
||||
|
||||
@@ -727,10 +792,38 @@ export const processSyncAction = (
|
||||
update: { starred }
|
||||
}
|
||||
])
|
||||
} else if(action?.deleteChatAction || type === 'deleteChat') {
|
||||
if(!isInitialSync) {
|
||||
} else if (action?.deleteChatAction || type === 'deleteChat') {
|
||||
if (!isInitialSync) {
|
||||
ev.emit('chats.delete', [id])
|
||||
}
|
||||
} else if (action?.labelEditAction) {
|
||||
const { name, color, deleted, predefinedId } = action.labelEditAction!
|
||||
|
||||
ev.emit('labels.edit', {
|
||||
id,
|
||||
name: name!,
|
||||
color: color!,
|
||||
deleted: deleted!,
|
||||
predefinedId: predefinedId ? String(predefinedId) : undefined
|
||||
})
|
||||
} 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]
|
||||
} as ChatLabelAssociation
|
||||
: {
|
||||
type: LabelAssociationType.Message,
|
||||
chatId: syncAction.index[2],
|
||||
messageId: syncAction.index[3],
|
||||
labelId: syncAction.index[1]
|
||||
} as MessageLabelAssociation
|
||||
})
|
||||
} else {
|
||||
logger?.debug({ syncAction, id }, 'unprocessable update')
|
||||
}
|
||||
@@ -739,7 +832,7 @@ export const processSyncAction = (
|
||||
return isInitialSync
|
||||
? (data) => {
|
||||
const chat = data.historySets.chats[id] || data.chatUpserts[id]
|
||||
if(chat) {
|
||||
if (chat) {
|
||||
return msgRange ? isValidPatchBasedOnMessageRange(chat, msgRange) : true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user