diff --git a/Example/example.ts b/Example/example.ts index eb6fd15..33db559 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -3,7 +3,7 @@ import NodeCache from 'node-cache' import makeWASocket, { AnyMessageContent, delay, DisconnectReason, fetchLatestBaileysVersion, getAggregateVotesInPollMessage, makeCacheableSignalKeyStore, makeInMemoryStore, proto, useMultiFileAuthState, WAMessageContent, WAMessageKey } from '../src' import MAIN_LOGGER from '../src/Utils/logger' -const logger = MAIN_LOGGER.child({ }) +const logger = MAIN_LOGGER.child({}) logger.level = 'trace' const useStore = !process.argv.includes('--no-store') @@ -88,6 +88,15 @@ const startSock = async() => { await saveCreds() } + if(events['labels.association']) { + console.log(events['labels.association']) + } + + + if(events['labels.edit']) { + console.log(events['labels.edit']) + } + if(events.call) { console.log('recv call event', events.call) } diff --git a/README.md b/README.md index c0047f7..a5a8821 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,8 @@ export type BaileysEventMap = { 'chats.update': Partial[] /** delete chats with given ID */ 'chats.delete': string[] + 'labels.association': LabelAssociation + 'labels.edit': Label /** presence of contact in a chat updated */ 'presence.update': { id: string, presences: { [participant: string]: PresenceData } } diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index 35cb8e3..1a88161 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -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') @@ -118,7 +118,7 @@ export const makeChatsSocket = (config: SocketConfig) => { content: [{ tag: 'disappearing_mode', attrs: { - duration : duration.toString() + duration: duration.toString() } }] }) @@ -146,12 +146,12 @@ export const makeChatsSocket = (config: SocketConfig) => { content: [ { tag: 'query', - attrs: { }, - content: [ queryNode ] + attrs: {}, + content: [queryNode] }, { tag: 'list', - attrs: { }, + attrs: {}, content: userNodes } ] @@ -171,17 +171,17 @@ export const makeChatsSocket = (config: SocketConfig) => { [ { tag: 'user', - attrs: { }, + attrs: {}, content: jids.map( jid => ({ tag: 'contact', - attrs: { }, + attrs: {}, content: `+${jid}` }) ) } ], - { tag: 'contact', attrs: { } } + { tag: 'contact', attrs: {} } ) return results.map(user => { @@ -193,7 +193,7 @@ export const makeChatsSocket = (config: SocketConfig) => { const fetchStatus = async(jid: string) => { const [result] = await interactiveQuery( [{ tag: 'user', attrs: { jid } }], - { tag: 'status', attrs: { } } + { tag: 'status', attrs: {} } ) if(result) { const status = getBinaryNodeChild(result, 'status') @@ -248,7 +248,7 @@ export const makeChatsSocket = (config: SocketConfig) => { content: [ { tag: 'status', - attrs: { }, + attrs: {}, content: Buffer.from(status, 'utf-8') } ] @@ -379,19 +379,19 @@ export const makeChatsSocket = (config: SocketConfig) => { 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() => { 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 } + const states = {} as { [T in WAPatchName]: LTHashState } const nodes: BinaryNode[] = [] for(const name of collectionsToHandle) { @@ -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 } ] @@ -516,10 +516,10 @@ export const makeChatsSocket = (config: SocketConfig) => { }) /** - * fetch the profile picture of a user/group - * type = "preview" for a low res picture - * type = "image for the high res picture" - */ + * 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({ @@ -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 } ] @@ -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() } ] @@ -731,7 +731,7 @@ export const makeChatsSocket = (config: SocketConfig) => { const propsNode = getBinaryNodeChild(abtNode, 'props') - let props: { [_: string]: string } = { } + let props: { [_: string]: string } = {} if(propsNode) { props = reduceBinaryNodeToDictionary(propsNode, 'prop') } @@ -751,13 +751,13 @@ export const makeChatsSocket = (config: SocketConfig) => { type: 'get', }, content: [ - { tag: 'props', attrs: { } } + { tag: 'props', attrs: {} } ] }) const propsNode = getBinaryNodeChild(resultNode, 'props') - let props: { [_: string]: string } = { } + let props: { [_: string]: string } = {} if(propsNode) { props = reduceBinaryNodeToDictionary(propsNode, 'prop') } @@ -768,15 +768,61 @@ 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 @@ -945,6 +991,10 @@ export const makeChatsSocket = (config: SocketConfig) => { updateDefaultDisappearingMode, getBusinessProfile, resyncAppState, - chatModify + chatModify, + addChatLabel, + removeChatLabel, + addMessageLabel, + removeMessageLabel } } diff --git a/src/Store/make-in-memory-store.ts b/src/Store/make-in-memory-store.ts index 477be1d..8830911 100644 --- a/src/Store/make-in-memory-store.ts +++ b/src/Store/make-in-memory-store.ts @@ -1,43 +1,94 @@ -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