Usync: Barebones Usync Protocol support (#960)

* feature(feature/usync-mex): initial commit

* chore: fix merge commit

* chore:lint
This commit is contained in:
Rajeh Taher
2024-12-22 23:38:41 +02:00
committed by GitHub
parent 8333a25fca
commit f1f49ad2c8
14 changed files with 525 additions and 147 deletions

View File

@@ -0,0 +1,32 @@
import { USyncQueryProtocol } from '../../Types/USync'
import { assertNodeErrorFree, BinaryNode } from '../../WABinary'
import { USyncUser } from '../USyncUser'
export class USyncContactProtocol implements USyncQueryProtocol {
name = 'contact'
getQueryElement(): BinaryNode {
return {
tag: 'contact',
attrs: {},
}
}
getUserElement(user: USyncUser): BinaryNode {
//TODO: Implement type / username fields (not yet supported)
return {
tag: 'contact',
attrs: {},
content: user.phone,
}
}
parser(node: BinaryNode): boolean {
if(node.tag === 'contact') {
assertNodeErrorFree(node)
return node?.attrs?.type === 'in'
}
return false
}
}

View File

@@ -0,0 +1,78 @@
import { USyncQueryProtocol } from '../../Types/USync'
import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild } from '../../WABinary'
//import { USyncUser } from '../USyncUser'
export type KeyIndexData = {
timestamp: number
signedKeyIndex?: Uint8Array
expectedTimestamp?: number
}
export type DeviceListData = {
id: number
keyIndex?: number
isHosted?: boolean
}
export type ParsedDeviceInfo = {
deviceList?: DeviceListData[]
keyIndex?: KeyIndexData
}
export class USyncDeviceProtocol implements USyncQueryProtocol {
name = 'devices'
getQueryElement(): BinaryNode {
return {
tag: 'devices',
attrs: {
version: '2',
},
}
}
getUserElement(/* user: USyncUser */): BinaryNode | null {
//TODO: Implement device phashing, ts and expectedTs
//TODO: if all are not present, return null <- current behavior
//TODO: otherwise return a node w tag 'devices' w those as attrs
return null
}
parser(node: BinaryNode): ParsedDeviceInfo {
const deviceList: DeviceListData[] = []
let keyIndex: KeyIndexData | undefined = undefined
if(node.tag === 'devices') {
assertNodeErrorFree(node)
const deviceListNode = getBinaryNodeChild(node, 'device-list')
const keyIndexNode = getBinaryNodeChild(node, 'key-index-list')
if(Array.isArray(deviceListNode?.content)) {
for(const { tag, attrs } of deviceListNode!.content) {
const id = +attrs.id
const keyIndex = +attrs['key-index']
if(tag === 'device') {
deviceList.push({
id,
keyIndex,
isHosted: !!(attrs['is_hosted'] && attrs['is_hosted'] === 'true')
})
}
}
}
if(keyIndexNode?.tag === 'key-index-list') {
keyIndex = {
timestamp: +keyIndexNode.attrs['ts'],
signedKeyIndex: keyIndexNode?.content as Uint8Array,
expectedTimestamp: keyIndexNode.attrs['expected_ts'] ? +keyIndexNode.attrs['expected_ts'] : undefined
}
}
}
return {
deviceList,
keyIndex
}
}
}

View File

@@ -0,0 +1,35 @@
import { USyncQueryProtocol } from '../../Types/USync'
import { assertNodeErrorFree, BinaryNode } from '../../WABinary'
export type DisappearingModeData = {
duration: number
setAt?: Date
}
export class USyncDisappearingModeProtocol implements USyncQueryProtocol {
name = 'disappearing_mode'
getQueryElement(): BinaryNode {
return {
tag: 'disappearing_mode',
attrs: {},
}
}
getUserElement(): null {
return null
}
parser(node: BinaryNode): DisappearingModeData | undefined {
if(node.tag === 'status') {
assertNodeErrorFree(node)
const duration: number = +node?.attrs.duration
const setAt = new Date(+(node?.attrs.t || 0) * 1000)
return {
duration,
setAt,
}
}
}
}

View File

@@ -0,0 +1,44 @@
import { USyncQueryProtocol } from '../../Types/USync'
import { assertNodeErrorFree, BinaryNode } from '../../WABinary'
export type StatusData = {
status?: string | null
setAt?: Date
}
export class USyncStatusProtocol implements USyncQueryProtocol {
name = 'status'
getQueryElement(): BinaryNode {
return {
tag: 'status',
attrs: {},
}
}
getUserElement(): null {
return null
}
parser(node: BinaryNode): StatusData | undefined {
if(node.tag === 'status') {
assertNodeErrorFree(node)
let status: string | null = node?.content!.toString()
const setAt = new Date(+(node?.attrs.t || 0) * 1000)
if(!status) {
if(+node.attrs?.code === 401) {
status = ''
} else {
status = null
}
} else if(typeof status === 'string' && status.length === 0) {
status = null
}
return {
status,
setAt,
}
}
}
}

View File

@@ -0,0 +1,4 @@
export * from './USyncDeviceProtocol'
export * from './USyncContactProtocol'
export * from './USyncStatusProtocol'
export * from './USyncDisappearingModeProtocol'

103
src/WAUSync/USyncQuery.ts Normal file
View File

@@ -0,0 +1,103 @@
import { USyncQueryProtocol } from '../Types/USync'
import { BinaryNode, getBinaryNodeChild } from '../WABinary'
import { USyncContactProtocol, USyncDeviceProtocol, USyncDisappearingModeProtocol, USyncStatusProtocol } from './Protocols'
import { USyncUser } from './USyncUser'
export type USyncQueryResultList = { [protocol: string]: unknown, id: string }
export type USyncQueryResult = {
list: USyncQueryResultList[]
sideList: USyncQueryResultList[]
}
export class USyncQuery {
protocols: USyncQueryProtocol[]
users: USyncUser[]
context: string
mode: string
constructor() {
this.protocols = []
this.users = []
this.context = 'interactive'
this.mode = 'query'
}
withMode(mode: string) {
this.mode = mode
return this
}
withContext(context: string) {
this.context = context
return this
}
withUser(user: USyncUser) {
this.users.push(user)
return this
}
parseUSyncQueryResult(result: BinaryNode): USyncQueryResult | undefined {
if(result.attrs.type !== 'result') {
return
}
const protocolMap = Object.fromEntries(this.protocols.map((protocol) => {
return [protocol.name, protocol.parser]
}))
const queryResult: USyncQueryResult = {
// TODO: implement errors etc.
list: [],
sideList: [],
}
const usyncNode = getBinaryNodeChild(result, 'usync')
//TODO: implement error backoff, refresh etc.
//TODO: see if there are any errors in the result node
//const resultNode = getBinaryNodeChild(usyncNode, 'result')
const listNode = getBinaryNodeChild(usyncNode, 'list')
if(Array.isArray(listNode?.content) && typeof listNode !== 'undefined') {
queryResult.list = listNode.content.map((node) => {
const id = node?.attrs.jid
const data = Array.isArray(node?.content) ? Object.fromEntries(node.content.map((content) => {
const protocol = content.tag
const parser = protocolMap[protocol]
if(parser) {
return [protocol, parser(content)]
} else {
return [protocol, null]
}
}).filter(([, b]) => b !== null) as [string, unknown][]) : {}
return { ...data, id }
})
}
//TODO: implement side list
//const sideListNode = getBinaryNodeChild(usyncNode, 'side_list')
return queryResult
}
withDeviceProtocol() {
this.protocols.push(new USyncDeviceProtocol())
return this
}
withContactProtocol() {
this.protocols.push(new USyncContactProtocol())
return this
}
withStatusProtocol() {
this.protocols.push(new USyncStatusProtocol())
return this
}
withDisappearingModeProtocol() {
this.protocols.push(new USyncDisappearingModeProtocol())
return this
}
}

27
src/WAUSync/USyncUser.ts Normal file
View File

@@ -0,0 +1,27 @@
export class USyncUser {
id: string
lid: string
phone: string
type: string
withId(id: string) {
this.id = id
return this
}
withLid(lid: string) {
this.lid = lid
return this
}
withPhone(phone: string) {
this.phone = phone
return this
}
withType(type: string) {
this.type = type
return this
}
}

3
src/WAUSync/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './Protocols'
export * from './USyncQuery'
export * from './USyncUser'