feat(labels): modify chat utils

This commit is contained in:
Rafael Garcia
2023-05-14 12:44:19 -05:00
parent 00a7b48749
commit 38f285760d
4 changed files with 362 additions and 208 deletions

View File

@@ -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
}
}