From 534ea174b959dcca699e9e655d54936872688aff Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 14 May 2023 12:32:49 -0500 Subject: [PATCH 01/11] feat(labels): add label entity --- src/Types/Label.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/Types/Label.ts diff --git a/src/Types/Label.ts b/src/Types/Label.ts new file mode 100644 index 0000000..6319b00 --- /dev/null +++ b/src/Types/Label.ts @@ -0,0 +1,36 @@ +export interface Label { + /** Label uniq ID */ + id: string + /** Label name */ + name: string + /** Label color ID */ + color: number + /** Is label has been deleted */ + deleted: boolean + /** WhatsApp has 5 predefined labels (New customer, New order & etc) */ + predefinedId?: string +} + +/** WhatsApp has 20 predefined colors */ +export enum LabelColor { + Color1 = 0, + Color2, + Color3, + Color4, + Color5, + Color6, + Color7, + Color8, + Color9, + Color10, + Color11, + Color12, + Color13, + Color14, + Color15, + Color16, + Color17, + Color18, + Color19, + Color20, +} \ No newline at end of file From 00a7b48749b378332c8bd216628195214124aa49 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 14 May 2023 12:35:19 -0500 Subject: [PATCH 02/11] feat(labels): add label association types --- src/Types/LabelAssociation.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/Types/LabelAssociation.ts diff --git a/src/Types/LabelAssociation.ts b/src/Types/LabelAssociation.ts new file mode 100644 index 0000000..38c1c6d --- /dev/null +++ b/src/Types/LabelAssociation.ts @@ -0,0 +1,35 @@ +/** Association type */ +export enum LabelAssociationType { + Chat = 'label_jid', + Message = 'label_message' + } + + export type LabelAssociationTypes = `${LabelAssociationType}` + + /** Association for chat */ + export interface ChatLabelAssociation { + type: LabelAssociationType.Chat + chatId: string + labelId: string + } + + /** Association for message */ + export interface MessageLabelAssociation { + type: LabelAssociationType.Message + chatId: string + messageId: string + labelId: string + } + + export type LabelAssociation = ChatLabelAssociation | MessageLabelAssociation + + /** Body for add/remove chat label association action */ + export interface ChatLabelAssociationActionBody { + labelId: string + } + + /** body for add/remove message label association action */ + export interface MessageLabelAssociationActionBody { + labelId: string + messageId: string + } \ No newline at end of file From 38f285760df426314b75ed752f1edcd6f23e9d23 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 14 May 2023 12:44:19 -0500 Subject: [PATCH 03/11] feat(labels): modify chat utils --- src/Socket/chats.ts | 286 +++++++++++++++++++++++----------------- src/Types/Chat.ts | 19 ++- src/Types/Events.ts | 4 + src/Utils/chat-utils.ts | 261 ++++++++++++++++++++++++------------ 4 files changed, 362 insertions(+), 208 deletions(-) diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index 35cb8e3..057896d 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -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 => { + const getBusinessProfile = async (jid: string): Promise => { 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(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 } } diff --git a/src/Types/Chat.ts b/src/Types/Chat.ts index a89a9cd..e3dcec8 100644 --- a/src/Types/Chat.ts +++ b/src/Types/Chat.ts @@ -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]: { diff --git a/src/Types/Events.ts b/src/Types/Events.ts index e4fc942..f37a6d2 100644 --- a/src/Types/Events.ts +++ b/src/Types/Events.ts @@ -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 = { diff --git a/src/Utils/chat-utils.ts b/src/Utils/chat-utils.ts index 2ce6695..2c21ea6 100644 --- a/src/Utils/chat-utils.ts +++ b/src/Utils/chat-utils.ts @@ -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 { 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 ({ 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 ) => { 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 ) => { @@ -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 ) => { @@ -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 } } From ce019dcf0f0e3c3f1a8c57b776aa5fe39854b598 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 14 May 2023 12:50:45 -0500 Subject: [PATCH 04/11] feat(store/labels): add ability to store and query labels --- src/Store/make-in-memory-store.ts | 296 +++++++++++++++++++++--------- src/Store/object-repository.ts | 31 ++++ 2 files changed, 243 insertions(+), 84 deletions(-) create mode 100644 src/Store/object-repository.ts diff --git a/src/Store/make-in-memory-store.ts b/src/Store/make-in-memory-store.ts index f8d7a6b..5787b24 100644 --- a/src/Store/make-in-memory-store.ts +++ b/src/Store/make-in-memory-store.ts @@ -1,46 +1,97 @@ -import type KeyedDB from '@adiwajshing/keyed-db' +import KeyedDB from '@adiwajshing/keyed-db' import type { Comparable } from '@adiwajshing/keyed-db/lib/Types' import type { Logger } from 'pino' import { proto } from '../../WAProto' import { DEFAULT_CONNECTION_CONFIG } from '../Defaults' import type makeMDSocket from '../Socket' import type { BaileysEventEmitter, Chat, ConnectionState, Contact, GroupMetadata, PresenceData, WAMessage, WAMessageCursor, WAMessageKey } from '../Types' +import { Label } from '../Types/Label' +import { LabelAssociation, LabelAssociationType, MessageLabelAssociation } from '../Types/LabelAssociation' import { toNumber, updateMessageWithReaction, updateMessageWithReceipt } from '../Utils' import { jidNormalizedUser } from '../WABinary' import makeOrderedDictionary from './make-ordered-dictionary' +import { ObjectRepository } from './object-repository' type WASocket = ReturnType export const waChatKey = (pin: boolean) => ({ key: (c: Chat) => (pin ? (c.pinned ? '1' : '0') : '') + (c.archived ? '0' : '1') + (c.conversationTimestamp ? c.conversationTimestamp.toString(16).padStart(8, '0') : '') + c.id, - compare: (k1: string, k2: string) => k2.localeCompare (k1) + compare: (k1: string, k2: string) => k2.localeCompare(k1) }) export const waMessageID = (m: WAMessage) => m.key.id || '' +export const waLabelAssociationKey: Comparable = { + key: (la: LabelAssociation) => (la.type === LabelAssociationType.Chat ? la.chatId + la.labelId : la.chatId + la.messageId + la.labelId), + compare: (k1: string, k2: string) => k2.localeCompare(k1) +} + export type BaileysInMemoryStoreConfig = { chatKey?: Comparable + labelAssociationKey?: Comparable logger?: Logger } const makeMessagesDictionary = () => makeOrderedDictionary(waMessageID) -export default ( - { logger: _logger, chatKey }: BaileysInMemoryStoreConfig -) => { - const logger = _logger || DEFAULT_CONNECTION_CONFIG.logger.child({ stream: 'in-mem-store' }) - chatKey = chatKey || waChatKey(true) - const KeyedDB = require('@adiwajshing/keyed-db').default as new (...args: any[]) => KeyedDB +const predefinedLabels = Object.freeze>({ + '0': { + id: '0', + name: 'New customer', + predefinedId: '0', + color: 0, + deleted: false + }, + '1': { + id: '1', + name: 'New order', + predefinedId: '1', + color: 1, + deleted: false + }, + '2': { + id: '2', + name: 'Pending payment', + predefinedId: '2', + color: 2, + deleted: false + }, + '3': { + id: '3', + name: 'Paid', + predefinedId: '3', + color: 3, + deleted: false + }, + '4': { + id: '4', + name: 'Order completed', + predefinedId: '4', + color: 4, + deleted: false + } +}) - const chats = new KeyedDB(chatKey, c => c.id) - const messages: { [_: string]: ReturnType } = { } - const contacts: { [_: string]: Contact } = { } - const groupMetadata: { [_: string]: GroupMetadata } = { } - const presences: { [id: string]: { [participant: string]: PresenceData } } = { } +export default ( + { logger: _logger, chatKey, labelAssociationKey }: BaileysInMemoryStoreConfig +) => { + // const logger = _logger || DEFAULT_CONNECTION_CONFIG.logger.child({ stream: 'in-mem-store' }) + chatKey = chatKey || waChatKey(true) + labelAssociationKey = labelAssociationKey || waLabelAssociationKey + const logger = _logger || DEFAULT_CONNECTION_CONFIG.logger.child({ stream: 'in-mem-store' }) + // const KeyedDB = require('@adiwajshing/keyed-db').default as new (...args: any[]) => KeyedDB + + const chats = new KeyedDB(chatKey, c => c.id) + const messages: { [_: string]: ReturnType } = {} + const contacts: { [_: string]: Contact } = {} + const groupMetadata: { [_: string]: GroupMetadata } = {} + const presences: { [id: string]: { [participant: string]: PresenceData } } = {} const state: ConnectionState = { connection: 'close' } + const labels = new ObjectRepository