Skip to content

Commit

Permalink
Usync: Barebones Usync Protocol support (#960)
Browse files Browse the repository at this point in the history
* feature(feature/usync-mex): initial commit

* chore: fix merge commit

* chore:lint
  • Loading branch information
purpshell authored Dec 22, 2024
1 parent 8333a25 commit f1f49ad
Show file tree
Hide file tree
Showing 14 changed files with 521 additions and 143 deletions.
106 changes: 36 additions & 70 deletions src/Socket/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { chatModificationToAppPatch, ChatMutationMap, decodePatches, decodeSyncd
import { makeMutex } from '../Utils/make-mutex'
import processMessage from '../Utils/process-message'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary'
import { makeSocket } from './socket'
import { USyncQuery, USyncUser } from '../WAUSync'
import { makeUSyncSocket } from './usync'

const MAX_SYNC_ATTEMPTS = 2

Expand All @@ -21,7 +22,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
shouldIgnoreJid,
shouldSyncHistoryMessage,
} = config
const sock = makeSocket(config)
const sock = makeUSyncSocket(config)
const {
ev,
ws,
Expand Down Expand Up @@ -139,83 +140,47 @@ export const makeChatsSocket = (config: SocketConfig) => {
})
}

/** helper function to run a generic IQ query */
const interactiveQuery = async(userNodes: BinaryNode[], queryNode: BinaryNode) => {
const result = await query({
tag: 'iq',
attrs: {
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'usync',
},
content: [
{
tag: 'usync',
attrs: {
sid: generateMessageTag(),
mode: 'query',
last: 'true',
index: '0',
context: 'interactive',
},
content: [
{
tag: 'query',
attrs: {},
content: [queryNode]
},
{
tag: 'list',
attrs: {},
content: userNodes
}
]
}
],
})
const onWhatsApp = async(...jids: string[]) => {
const usyncQuery = new USyncQuery()
.withContactProtocol()

for(const jid of jids) {
const phone = `+${jid.replace('+', '').split('@')[0].split(':')[0]}`
usyncQuery.withUser(new USyncUser().withPhone(phone))
}

const usyncNode = getBinaryNodeChild(result, 'usync')
const listNode = getBinaryNodeChild(usyncNode, 'list')
const users = getBinaryNodeChildren(listNode, 'user')
const results = await sock.executeUSyncQuery(usyncQuery)

return users
if(results) {
return results.list.filter((a) => !!a.contact).map(({ contact, id }) => ({ jid: id, exists: contact }))
}
}

const onWhatsApp = async(...jids: string[]) => {
const query = { tag: 'contact', attrs: {} }
const list = jids.map((jid) => {
// insures only 1 + is there
const content = `+${jid.replace('+', '')}`
const fetchStatus = async(...jids: string[]) => {
const usyncQuery = new USyncQuery()
.withStatusProtocol()

return {
tag: 'user',
attrs: {},
content: [{
tag: 'contact',
attrs: {},
content,
}],
}
})
const results = await interactiveQuery(list, query)
for(const jid of jids) {
usyncQuery.withUser(new USyncUser().withId(jid))
}

return results.map(user => {
const contact = getBinaryNodeChild(user, 'contact')
return { exists: contact?.attrs.type === 'in', jid: user.attrs.jid }
}).filter(item => item.exists)
const result = await sock.executeUSyncQuery(usyncQuery)
if(result) {
return result.list
}
}

const fetchStatus = async(jid: string) => {
const [result] = await interactiveQuery(
[{ tag: 'user', attrs: { jid } }],
{ tag: 'status', attrs: {} }
)
const fetchDisappearingDuration = async(...jids: string[]) => {
const usyncQuery = new USyncQuery()
.withDisappearingModeProtocol()

for(const jid of jids) {
usyncQuery.withUser(new USyncUser().withId(jid))
}

const result = await sock.executeUSyncQuery(usyncQuery)
if(result) {
const status = getBinaryNodeChild(result, 'status')
return {
status: status?.content!.toString(),
setAt: new Date(+(status?.attrs.t || 0) * 1000)
}
return result.list
}
}

Expand Down Expand Up @@ -1021,6 +986,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
onWhatsApp,
fetchBlocklist,
fetchStatus,
fetchDisappearingDuration,
updateProfilePicture,
removeProfilePicture,
updateProfileStatus,
Expand Down
84 changes: 33 additions & 51 deletions src/Socket/messages-send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AnyMessageContent, MediaConnInfo, MessageReceiptType, MessageRelayOptio
import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils'
import { getUrlInfo } from '../Utils/link-preview'
import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, isJidUser, jidDecode, jidEncode, jidNormalizedUser, JidWithDevice, S_WHATSAPP_NET } from '../WABinary'
import { USyncQuery, USyncUser } from '../WAUSync'
import { makeGroupsSocket } from './groups'

export const makeMessagesSocket = (config: SocketConfig) => {
Expand All @@ -27,7 +28,6 @@ export const makeMessagesSocket = (config: SocketConfig) => {
upsertMessage,
query,
fetchPrivacySettings,
generateMessageTag,
sendNode,
groupMetadata,
groupToggleEphemeral,
Expand Down Expand Up @@ -144,72 +144,54 @@ export const makeMessagesSocket = (config: SocketConfig) => {
logger.debug('not using cache for devices')
}

const users: BinaryNode[] = []
const toFetch: string[] = []
jids = Array.from(new Set(jids))

for(let jid of jids) {
const user = jidDecode(jid)?.user
jid = jidNormalizedUser(jid)
if(useCache) {
const devices = userDevicesCache.get<JidWithDevice[]>(user!)
if(devices) {
deviceResults.push(...devices)

const devices = userDevicesCache.get<JidWithDevice[]>(user!)
if(devices && useCache) {
deviceResults.push(...devices)

logger.trace({ user }, 'using cache for devices')
logger.trace({ user }, 'using cache for devices')
} else {
toFetch.push(jid)
}
} else {
users.push({ tag: 'user', attrs: { jid } })
toFetch.push(jid)
}
}

if(!users.length) {
if(!toFetch.length) {
return deviceResults
}

const iq: BinaryNode = {
tag: 'iq',
attrs: {
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'usync',
},
content: [
{
tag: 'usync',
attrs: {
sid: generateMessageTag(),
mode: 'query',
last: 'true',
index: '0',
context: 'message',
},
content: [
{
tag: 'query',
attrs: { },
content: [
{
tag: 'devices',
attrs: { version: '2' }
}
]
},
{ tag: 'list', attrs: { }, content: users }
]
},
],
const query = new USyncQuery()
.withContext('message')
.withDeviceProtocol()

for(const jid of toFetch) {
query.withUser(new USyncUser().withId(jid))
}
const result = await query(iq)
const extracted = extractDeviceJids(result, authState.creds.me!.id, ignoreZeroDevices)
const deviceMap: { [_: string]: JidWithDevice[] } = {}

for(const item of extracted) {
deviceMap[item.user] = deviceMap[item.user] || []
deviceMap[item.user].push(item)
const result = await sock.executeUSyncQuery(query)

deviceResults.push(item)
}
if(result) {
const extracted = extractDeviceJids(result?.list, authState.creds.me!.id, ignoreZeroDevices)
const deviceMap: { [_: string]: JidWithDevice[] } = {}

for(const item of extracted) {
deviceMap[item.user] = deviceMap[item.user] || []
deviceMap[item.user].push(item)

for(const key in deviceMap) {
userDevicesCache.set(key, deviceMap[key])
deviceResults.push(item)
}

for(const key in deviceMap) {
userDevicesCache.set(key, deviceMap[key])
}
}

return deviceResults
Expand Down
81 changes: 81 additions & 0 deletions src/Socket/usync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Boom } from '@hapi/boom'
import { SocketConfig } from '../Types'
import { BinaryNode, S_WHATSAPP_NET } from '../WABinary'
import { USyncQuery } from '../WAUSync'
import { makeSocket } from './socket'

export const makeUSyncSocket = (config: SocketConfig) => {
const sock = makeSocket(config)

const {
generateMessageTag,
query,
} = sock

const executeUSyncQuery = async(usyncQuery: USyncQuery) => {
if(usyncQuery.protocols.length === 0) {
throw new Boom('USyncQuery must have at least one protocol')
}

// todo: validate users, throw WARNING on no valid users
// variable below has only validated users
const validUsers = usyncQuery.users

const userNodes = validUsers.map((user) => {
return {
tag: 'user',
attrs: {
jid: !user.phone ? user.id : undefined,
},
content: usyncQuery.protocols
.map((a) => a.getUserElement(user))
.filter(a => a !== null)
} as BinaryNode
})

const listNode: BinaryNode = {
tag: 'list',
attrs: {},
content: userNodes
}

const queryNode: BinaryNode = {
tag: 'query',
attrs: {},
content: usyncQuery.protocols.map((a) => a.getQueryElement())
}
const iq = {
tag: 'iq',
attrs: {
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'usync',
},
content: [
{
tag: 'usync',
attrs: {
context: usyncQuery.context,
mode: usyncQuery.mode,
sid: generateMessageTag(),
last: 'true',
index: '0',
},
content: [
queryNode,
listNode
]
}
],
}

const result = await query(iq)

return usyncQuery.parseUSyncQueryResult(result)
}

return {
...sock,
executeUSyncQuery,
}
}
27 changes: 27 additions & 0 deletions src/Types/USync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { BinaryNode } from '../WABinary'
import { USyncUser } from '../WAUSync'

/**
* Defines the interface for a USyncQuery protocol
*/
export interface USyncQueryProtocol {
/**
* The name of the protocol
*/
name: string
/**
* Defines what goes inside the query part of a USyncQuery
*/
getQueryElement: () => BinaryNode
/**
* Defines what goes inside the user part of a USyncQuery
*/
getUserElement: (user: USyncUser) => BinaryNode | null

/**
* Parse the result of the query
* @param data Data from the result
* @returns Whatever the protocol is supposed to return
*/
parser: (data: BinaryNode) => unknown
}
Loading

0 comments on commit f1f49ad

Please sign in to comment.