Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usync Protocol with Mex support (soon) #960

Merged
merged 4 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading