lid, wip: Support LIDs in Baileys (#1747)
* lid-mapping: get missing lid from usync * lid-mapping, jid-utils: change to isPnUser and store multiple mappings * process-message: parse protocolMsg mapping, and store from new msgs * types: lid-mapping event, addressing enum, alt, contact, group types * validate, decode: use lid for identity, better logic * lid: final commit * linting * linting * linting * linting * misc: fix testing and also remove version json * lint: IDE fucking up lint * lid-mapping: fix build error on NPM * message-retry: fix proto import
This commit is contained in:
+4
-19
@@ -53,7 +53,7 @@ import {
|
||||
S_WHATSAPP_NET
|
||||
} from '../WABinary'
|
||||
import { USyncQuery, USyncUser } from '../WAUSync'
|
||||
import { makeUSyncSocket } from './usync'
|
||||
import { makeSocket } from './socket.js'
|
||||
const MAX_SYNC_ATTEMPTS = 2
|
||||
|
||||
export const makeChatsSocket = (config: SocketConfig) => {
|
||||
@@ -65,8 +65,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
shouldIgnoreJid,
|
||||
shouldSyncHistoryMessage
|
||||
} = config
|
||||
const sock = makeUSyncSocket(config)
|
||||
const { ev, ws, authState, generateMessageTag, sendNode, query, onUnexpectedError } = sock
|
||||
const sock = makeSocket(config)
|
||||
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError } = sock
|
||||
|
||||
let privacySettings: { [_: string]: string } | undefined
|
||||
|
||||
@@ -221,21 +221,6 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
return botList
|
||||
}
|
||||
|
||||
const onWhatsApp = async (...jids: string[]) => {
|
||||
const usyncQuery = new USyncQuery().withContactProtocol().withLIDProtocol()
|
||||
|
||||
for (const jid of jids) {
|
||||
const phone = `+${jid.replace('+', '').split('@')[0]?.split(':')[0]}`
|
||||
usyncQuery.withUser(new USyncUser().withPhone(phone))
|
||||
}
|
||||
|
||||
const results = await sock.executeUSyncQuery(usyncQuery)
|
||||
|
||||
if (results) {
|
||||
return results.list.filter(a => !!a.contact).map(({ contact, id, lid }) => ({ jid: id, exists: contact, lid }))
|
||||
}
|
||||
}
|
||||
|
||||
const fetchStatus = async (...jids: string[]) => {
|
||||
const usyncQuery = new USyncQuery().withStatusProtocol()
|
||||
|
||||
@@ -1092,6 +1077,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
})(),
|
||||
processMessage(msg, {
|
||||
signalRepository,
|
||||
shouldProcessHistoryMsg,
|
||||
placeholderResendCache,
|
||||
ev,
|
||||
@@ -1195,7 +1181,6 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
sendPresenceUpdate,
|
||||
presenceSubscribe,
|
||||
profilePictureUrl,
|
||||
onWhatsApp,
|
||||
fetchBlocklist,
|
||||
fetchStatus,
|
||||
fetchDisappearingDuration,
|
||||
|
||||
+11
-10
@@ -1,14 +1,14 @@
|
||||
import { proto } from '../../WAProto/index.js'
|
||||
import type { GroupMetadata, GroupParticipant, ParticipantAction, SocketConfig, WAMessageKey } from '../Types'
|
||||
import { WAMessageStubType } from '../Types'
|
||||
import { WAMessageAddressingMode, WAMessageStubType } from '../Types'
|
||||
import { generateMessageIDV2, unixTimestampSeconds } from '../Utils'
|
||||
import {
|
||||
type BinaryNode,
|
||||
getBinaryNodeChild,
|
||||
getBinaryNodeChildren,
|
||||
getBinaryNodeChildString,
|
||||
isJidUser,
|
||||
isLidUser,
|
||||
isPnUser,
|
||||
jidEncode,
|
||||
jidNormalizedUser
|
||||
} from '../WABinary'
|
||||
@@ -305,12 +305,12 @@ export const extractGroupMetadata = (result: BinaryNode) => {
|
||||
let desc: string | undefined
|
||||
let descId: string | undefined
|
||||
let descOwner: string | undefined
|
||||
let descOwnerJid: string | undefined
|
||||
let descOwnerPn: string | undefined
|
||||
let descTime: number | undefined
|
||||
if (descChild) {
|
||||
desc = getBinaryNodeChildString(descChild, 'body')
|
||||
descOwner = descChild.attrs.participant ? jidNormalizedUser(descChild.attrs.participant) : undefined
|
||||
descOwnerJid = descChild.attrs.participant_pn ? jidNormalizedUser(descChild.attrs.participant_pn) : undefined
|
||||
descOwnerPn = descChild.attrs.participant_pn ? jidNormalizedUser(descChild.attrs.participant_pn) : undefined
|
||||
descTime = +descChild.attrs.t!
|
||||
descId = descChild.attrs.id
|
||||
}
|
||||
@@ -320,20 +320,21 @@ export const extractGroupMetadata = (result: BinaryNode) => {
|
||||
const memberAddMode = getBinaryNodeChildString(group, 'member_add_mode') === 'all_member_add'
|
||||
const metadata: GroupMetadata = {
|
||||
id: groupId!,
|
||||
addressingMode: group.attrs.addressing_mode as 'pn' | 'lid',
|
||||
notify: group.attrs.notify,
|
||||
addressingMode: group.attrs.addressing_mode === 'lid' ? WAMessageAddressingMode.LID : WAMessageAddressingMode.PN,
|
||||
subject: group.attrs.subject!,
|
||||
subjectOwner: group.attrs.s_o,
|
||||
subjectOwnerJid: group.attrs.s_o_pn,
|
||||
subjectOwnerPn: group.attrs.s_o_pn,
|
||||
subjectTime: +group.attrs.s_t!,
|
||||
size: group.attrs.size ? +group.attrs.size : getBinaryNodeChildren(group, 'participant').length,
|
||||
creation: +group.attrs.creation!,
|
||||
owner: group.attrs.creator ? jidNormalizedUser(group.attrs.creator) : undefined,
|
||||
ownerJid: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined,
|
||||
ownerPn: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined,
|
||||
owner_country_code: group.attrs.creator_country_code,
|
||||
desc,
|
||||
descId,
|
||||
descOwner,
|
||||
descOwnerJid,
|
||||
descOwnerPn,
|
||||
descTime,
|
||||
linkedParent: getBinaryNodeChild(group, 'linked_parent')?.attrs.jid || undefined,
|
||||
restrict: !!getBinaryNodeChild(group, 'locked'),
|
||||
@@ -345,8 +346,8 @@ export const extractGroupMetadata = (result: BinaryNode) => {
|
||||
participants: getBinaryNodeChildren(group, 'participant').map(({ attrs }) => {
|
||||
return {
|
||||
id: attrs.jid!,
|
||||
jid: isJidUser(attrs.jid) ? attrs.jid : jidNormalizedUser(attrs.phone_number),
|
||||
lid: isLidUser(attrs.jid) ? attrs.jid : attrs.lid,
|
||||
phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phoneNumber) ? attrs.phoneNumber : undefined,
|
||||
lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
|
||||
admin: (attrs.type || null) as GroupParticipant['admin']
|
||||
}
|
||||
}),
|
||||
|
||||
+39
-54
@@ -50,7 +50,6 @@ import {
|
||||
getBinaryNodeChildString,
|
||||
isJidGroup,
|
||||
isJidStatusBroadcast,
|
||||
isJidUser,
|
||||
isLidUser,
|
||||
jidDecode,
|
||||
jidNormalizedUser,
|
||||
@@ -549,6 +548,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
|
||||
const handleGroupNotification = (participant: string, child: BinaryNode, msg: Partial<proto.IWebMessageInfo>) => {
|
||||
const participantJid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || participant
|
||||
// TODO: Add participant LID
|
||||
switch (child?.tag) {
|
||||
case 'create':
|
||||
const metadata = extractGroupMetadata(child)
|
||||
@@ -697,11 +697,16 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
break
|
||||
case 'devices':
|
||||
const devices = getBinaryNodeChildren(child, 'device')
|
||||
if (areJidsSameUser(child!.attrs.jid, authState.creds.me!.id)) {
|
||||
const deviceJids = devices.map(d => d.attrs.jid)
|
||||
logger.info({ deviceJids }, 'got my own devices')
|
||||
if (
|
||||
areJidsSameUser(child!.attrs.jid, authState.creds.me!.id) ||
|
||||
areJidsSameUser(child!.attrs.lid, authState.creds.me!.lid)
|
||||
) {
|
||||
const deviceData = devices.map(d => ({ id: d.attrs.jid, lid: d.attrs.lid }))
|
||||
logger.info({ deviceData }, 'my own devices changed')
|
||||
}
|
||||
|
||||
//TODO: drop a new event, add hashes
|
||||
|
||||
break
|
||||
case 'server_sync':
|
||||
const update = getBinaryNodeChild(node, 'collection')
|
||||
@@ -1034,7 +1039,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
// correctly set who is asking for the retry
|
||||
key.participant = key.participant || attrs.from
|
||||
const retryNode = getBinaryNodeChild(node, 'retry')
|
||||
if (ids[0] && key.participant && willSendMessageAgain(ids[0], key.participant!)) {
|
||||
if (ids[0] && key.participant && willSendMessageAgain(ids[0], key.participant)) {
|
||||
if (key.fromMe) {
|
||||
try {
|
||||
updateSendMessageAgainCount(ids[0], key.participant)
|
||||
@@ -1142,56 +1147,36 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
msg.message?.protocolMessage?.type === proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER &&
|
||||
node.attrs.sender_pn
|
||||
) {
|
||||
ev.emit('chats.phoneNumberShare', { lid: node.attrs.from!, jid: node.attrs.sender_pn })
|
||||
const lid = jidNormalizedUser(node.attrs.from),
|
||||
pn = jidNormalizedUser(node.attrs.sender_pn)
|
||||
ev.emit('lid-mapping.update', { lid, pn })
|
||||
await signalRepository.storeLIDPNMapping(lid, pn)
|
||||
}
|
||||
|
||||
if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
|
||||
messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message!)
|
||||
logger.debug(
|
||||
{
|
||||
jid: msg.key.remoteJid,
|
||||
id: msg.key.id
|
||||
},
|
||||
'Added message to recent cache for retry receipts'
|
||||
)
|
||||
|
||||
if (msg.message?.protocolMessage?.lidMigrationMappingSyncMessage?.encodedMappingPayload) {
|
||||
try {
|
||||
const payload = msg.message.protocolMessage.lidMigrationMappingSyncMessage.encodedMappingPayload
|
||||
const decoded = proto.LIDMigrationMappingSyncPayload.decode(payload)
|
||||
|
||||
logger.debug(
|
||||
{
|
||||
mappingCount: decoded.pnToLidMappings?.length || 0,
|
||||
timestamp: decoded.chatDbMigrationTimestamp
|
||||
},
|
||||
'Received LID migration sync message from server'
|
||||
)
|
||||
|
||||
const lidMapping = signalRepository.getLIDMappingStore()
|
||||
if (decoded.pnToLidMappings && decoded.pnToLidMappings.length > 0) {
|
||||
for (const mapping of decoded.pnToLidMappings) {
|
||||
const pn = `${mapping.pn}@s.whatsapp.net`
|
||||
// Use latestLid if available, otherwise assignedLid (proper LID refresh)
|
||||
const lidValue = mapping.latestLid || mapping.assignedLid
|
||||
const lid = `${lidValue}@lid`
|
||||
|
||||
await lidMapping.storeLIDPNMapping(lid, pn)
|
||||
logger.debug(
|
||||
{
|
||||
pn,
|
||||
lid,
|
||||
assignedLid: mapping.assignedLid,
|
||||
latestLid: mapping.latestLid,
|
||||
usedLatest: !!mapping.latestLid
|
||||
},
|
||||
'Stored server-provided PN-LID mapping'
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to process LID migration sync message')
|
||||
const alt = msg.key.participantAlt || msg.key.remoteJidAlt
|
||||
// store new mappings we didn't have before
|
||||
if (!!alt) {
|
||||
const altServer = jidDecode(alt)?.server
|
||||
const lidMapping = signalRepository.getLIDMappingStore()
|
||||
if (altServer === 'lid') {
|
||||
if (!(await lidMapping.getPNForLID(alt))) {
|
||||
await lidMapping.storeLIDPNMapping(alt, msg.key.participant || msg.key.remoteJid!)
|
||||
}
|
||||
} else {
|
||||
if (!(await lidMapping.getLIDForPN(alt))) {
|
||||
await lidMapping.storeLIDPNMapping(msg.key.participant || msg.key.remoteJid!, alt)
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
|
||||
messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message!)
|
||||
logger.debug(
|
||||
{
|
||||
jid: msg.key.remoteJid,
|
||||
id: msg.key.id
|
||||
},
|
||||
'Added message to recent cache for retry receipts'
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1263,8 +1248,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
// message was sent by us from a different device
|
||||
type = 'sender'
|
||||
// need to specially handle this case
|
||||
if (isJidUser(msg.key.remoteJid!)) {
|
||||
participant = author
|
||||
if (isLidUser(msg.key.remoteJid!) || isLidUser(msg.key.remoteJidAlt)) {
|
||||
participant = author // TODO: investigate sending receipts to LIDs and not PNs
|
||||
}
|
||||
} else if (!sendActiveReceipts) {
|
||||
type = 'inactive'
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
SocketConfig,
|
||||
WAMessageKey
|
||||
} from '../Types'
|
||||
import { WAMessageAddressingMode } from '../Types'
|
||||
import {
|
||||
aggregateMessageKeysNotFromMe,
|
||||
assertMediaContent,
|
||||
@@ -40,16 +41,15 @@ import {
|
||||
getBinaryNodeChild,
|
||||
getBinaryNodeChildren,
|
||||
isJidGroup,
|
||||
isJidUser,
|
||||
isPnUser,
|
||||
jidDecode,
|
||||
jidEncode,
|
||||
jidNormalizedUser,
|
||||
type JidWithDevice,
|
||||
S_WHATSAPP_NET
|
||||
S_WHATSAPP_NET,
|
||||
transferDevice
|
||||
} from '../WABinary'
|
||||
import { USyncQuery, USyncUser } from '../WAUSync'
|
||||
import { makeGroupsSocket } from './groups'
|
||||
import type { NewsletterSocket } from './newsletter'
|
||||
import { makeNewsletterSocket } from './newsletter'
|
||||
|
||||
export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
@@ -63,7 +63,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
enableRecentMessageCache,
|
||||
maxMsgRetryCount
|
||||
} = config
|
||||
const sock: NewsletterSocket = makeNewsletterSocket(makeGroupsSocket(config))
|
||||
const sock = makeNewsletterSocket(config)
|
||||
const {
|
||||
ev,
|
||||
authState,
|
||||
@@ -147,7 +147,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
node.attrs.t = unixTimestampSeconds().toString()
|
||||
}
|
||||
|
||||
if (type === 'sender' && isJidUser(jid)) {
|
||||
if (type === 'sender' && isPnUser(jid)) {
|
||||
node.attrs.recipient = jid
|
||||
node.attrs.to = participant!
|
||||
} else {
|
||||
@@ -445,11 +445,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
if (!jid.includes('@s.whatsapp.net')) return
|
||||
|
||||
try {
|
||||
const jidDecoded = jidDecode(jid)
|
||||
const deviceId = jidDecoded?.device || 0
|
||||
const lidDecoded = jidDecode(lidForPN)
|
||||
const lidWithDevice = jidEncode(lidDecoded?.user!, 'lid', deviceId)
|
||||
|
||||
const lidWithDevice = transferDevice(jid, lidForPN)
|
||||
await signalRepository.migrateSession(jid, lidWithDevice)
|
||||
logger.debug({ fromJid: jid, toJid: lidWithDevice }, 'Migrated device session to LID')
|
||||
|
||||
@@ -904,7 +900,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
if (!isStatus) {
|
||||
const groupAddressingMode = groupData?.addressingMode || (isLid ? 'lid' : 'pn')
|
||||
const groupAddressingMode =
|
||||
groupData?.addressingMode || (isLid ? WAMessageAddressingMode.LID : WAMessageAddressingMode.PN)
|
||||
additionalAttributes = {
|
||||
...additionalAttributes,
|
||||
addressing_mode: groupAddressingMode
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { NewsletterCreateResponse, WAMediaUpload } from '../Types'
|
||||
import type { NewsletterCreateResponse, SocketConfig, WAMediaUpload } from '../Types'
|
||||
import type { NewsletterMetadata, NewsletterUpdate } from '../Types'
|
||||
import { QueryIds, XWAPaths } from '../Types'
|
||||
import { generateProfilePicture } from '../Utils/messages-media'
|
||||
import { getBinaryNodeChild } from '../WABinary'
|
||||
import type { GroupsSocket } from './groups'
|
||||
import { makeGroupsSocket } from './groups'
|
||||
import { executeWMexQuery as genericExecuteWMexQuery } from './mex'
|
||||
|
||||
const parseNewsletterCreateResponse = (response: NewsletterCreateResponse): NewsletterMetadata => {
|
||||
@@ -41,7 +41,8 @@ const parseNewsletterMetadata = (result: unknown): NewsletterMetadata | null =>
|
||||
return null
|
||||
}
|
||||
|
||||
export const makeNewsletterSocket = (sock: GroupsSocket) => {
|
||||
export const makeNewsletterSocket = (config: SocketConfig) => {
|
||||
const sock = makeGroupsSocket(config)
|
||||
const { query, generateMessageTag } = sock
|
||||
|
||||
const executeWMexQuery = <T>(variables: Record<string, unknown>, queryId: string, dataPath: string): Promise<T> => {
|
||||
|
||||
+170
-93
@@ -43,6 +43,7 @@ import {
|
||||
jidEncode,
|
||||
S_WHATSAPP_NET
|
||||
} from '../WABinary'
|
||||
import { USyncQuery, USyncUser } from '../WAUSync/'
|
||||
import { WebSocketClient } from './Client'
|
||||
|
||||
/**
|
||||
@@ -67,6 +68,9 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
makeSignalRepository
|
||||
} = config
|
||||
|
||||
const uqTagId = generateMdTagPrefix()
|
||||
const generateMessageTag = () => `${uqTagId}${epoch++}`
|
||||
|
||||
if (printQRInTerminal) {
|
||||
console.warn(
|
||||
'⚠️ The printQRInTerminal option has been deprecated. You will no longer receive QR codes in the terminal automatically. Please listen to the connection.update event yourself and handle the QR your way. You can remove this message by removing this opttion. This message will be removed in a future version.'
|
||||
@@ -83,98 +87,6 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'))
|
||||
}
|
||||
|
||||
const ws = new WebSocketClient(url, config)
|
||||
|
||||
ws.connect()
|
||||
|
||||
const ev = makeEventBuffer(logger)
|
||||
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
||||
const ephemeralKeyPair = Curve.generateKeyPair()
|
||||
/** WA noise protocol wrapper */
|
||||
const noise = makeNoiseHandler({
|
||||
keyPair: ephemeralKeyPair,
|
||||
NOISE_HEADER: NOISE_WA_HEADER,
|
||||
logger,
|
||||
routingInfo: authState?.creds?.routingInfo
|
||||
})
|
||||
|
||||
const { creds } = authState
|
||||
// add transaction capability
|
||||
const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
|
||||
const signalRepository = makeSignalRepository({ creds, keys })
|
||||
|
||||
let lastDateRecv: Date
|
||||
let epoch = 1
|
||||
let keepAliveReq: NodeJS.Timeout
|
||||
let qrTimer: NodeJS.Timeout
|
||||
let closed = false
|
||||
|
||||
const uqTagId = generateMdTagPrefix()
|
||||
const generateMessageTag = () => `${uqTagId}${epoch++}`
|
||||
|
||||
const sendPromise = promisify(ws.send)
|
||||
/** send a raw buffer */
|
||||
const sendRawMessage = async (data: Uint8Array | Buffer) => {
|
||||
if (!ws.isOpen) {
|
||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||
}
|
||||
|
||||
const bytes = noise.encodeFrame(data)
|
||||
await promiseTimeout<void>(connectTimeoutMs, async (resolve, reject) => {
|
||||
try {
|
||||
await sendPromise.call(ws, bytes)
|
||||
resolve()
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** send a binary node */
|
||||
const sendNode = (frame: BinaryNode) => {
|
||||
if (logger.level === 'trace') {
|
||||
logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' })
|
||||
}
|
||||
|
||||
const buff = encodeBinaryNode(frame)
|
||||
return sendRawMessage(buff)
|
||||
}
|
||||
|
||||
/** log & process any unexpected errors */
|
||||
const onUnexpectedError = (err: Error | Boom, msg: string) => {
|
||||
logger.error({ err }, `unexpected error in '${msg}'`)
|
||||
}
|
||||
|
||||
/** await the next incoming message */
|
||||
const awaitNextMessage = async <T>(sendMsg?: Uint8Array) => {
|
||||
if (!ws.isOpen) {
|
||||
throw new Boom('Connection Closed', {
|
||||
statusCode: DisconnectReason.connectionClosed
|
||||
})
|
||||
}
|
||||
|
||||
let onOpen: (data: T) => void
|
||||
let onClose: (err: Error) => void
|
||||
|
||||
const result = promiseTimeout<T>(connectTimeoutMs, (resolve, reject) => {
|
||||
onOpen = resolve
|
||||
onClose = mapWebSocketError(reject)
|
||||
ws.on('frame', onOpen)
|
||||
ws.on('close', onClose)
|
||||
ws.on('error', onClose)
|
||||
}).finally(() => {
|
||||
ws.off('frame', onOpen)
|
||||
ws.off('close', onClose)
|
||||
ws.off('error', onClose)
|
||||
})
|
||||
|
||||
if (sendMsg) {
|
||||
sendRawMessage(sendMsg).catch(onClose!)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a message with a certain tag to be received
|
||||
* @param msgId the message tag to await
|
||||
@@ -244,6 +156,169 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
return result
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const onWhatsApp = async (...jids: string[]) => {
|
||||
const usyncQuery = new USyncQuery().withContactProtocol().withLIDProtocol()
|
||||
|
||||
for (const jid of jids) {
|
||||
const phone = `+${jid.replace('+', '').split('@')[0]?.split(':')[0]}`
|
||||
usyncQuery.withUser(new USyncUser().withPhone(phone))
|
||||
}
|
||||
|
||||
const results = await executeUSyncQuery(usyncQuery)
|
||||
|
||||
if (results) {
|
||||
return results.list
|
||||
.filter(a => !!a.contact)
|
||||
.map(({ contact, id, lid }) => ({ jid: id, exists: contact as boolean, lid: lid as string }))
|
||||
}
|
||||
}
|
||||
|
||||
const ws = new WebSocketClient(url, config)
|
||||
|
||||
ws.connect()
|
||||
|
||||
const ev = makeEventBuffer(logger)
|
||||
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
||||
const ephemeralKeyPair = Curve.generateKeyPair()
|
||||
/** WA noise protocol wrapper */
|
||||
const noise = makeNoiseHandler({
|
||||
keyPair: ephemeralKeyPair,
|
||||
NOISE_HEADER: NOISE_WA_HEADER,
|
||||
logger,
|
||||
routingInfo: authState?.creds?.routingInfo
|
||||
})
|
||||
|
||||
const { creds } = authState
|
||||
// add transaction capability
|
||||
const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
|
||||
const signalRepository = makeSignalRepository({ creds, keys }, onWhatsApp)
|
||||
|
||||
let lastDateRecv: Date
|
||||
let epoch = 1
|
||||
let keepAliveReq: NodeJS.Timeout
|
||||
let qrTimer: NodeJS.Timeout
|
||||
let closed = false
|
||||
|
||||
const sendPromise = promisify(ws.send)
|
||||
/** send a raw buffer */
|
||||
const sendRawMessage = async (data: Uint8Array | Buffer) => {
|
||||
if (!ws.isOpen) {
|
||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||
}
|
||||
|
||||
const bytes = noise.encodeFrame(data)
|
||||
await promiseTimeout<void>(connectTimeoutMs, async (resolve, reject) => {
|
||||
try {
|
||||
await sendPromise.call(ws, bytes)
|
||||
resolve()
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** send a binary node */
|
||||
const sendNode = (frame: BinaryNode) => {
|
||||
if (logger.level === 'trace') {
|
||||
logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' })
|
||||
}
|
||||
|
||||
const buff = encodeBinaryNode(frame)
|
||||
return sendRawMessage(buff)
|
||||
}
|
||||
|
||||
/** log & process any unexpected errors */
|
||||
const onUnexpectedError = (err: Error | Boom, msg: string) => {
|
||||
logger.error({ err }, `unexpected error in '${msg}'`)
|
||||
}
|
||||
|
||||
/** await the next incoming message */
|
||||
const awaitNextMessage = async <T>(sendMsg?: Uint8Array) => {
|
||||
if (!ws.isOpen) {
|
||||
throw new Boom('Connection Closed', {
|
||||
statusCode: DisconnectReason.connectionClosed
|
||||
})
|
||||
}
|
||||
|
||||
let onOpen: (data: T) => void
|
||||
let onClose: (err: Error) => void
|
||||
|
||||
const result = promiseTimeout<T>(connectTimeoutMs, (resolve, reject) => {
|
||||
onOpen = resolve
|
||||
onClose = mapWebSocketError(reject)
|
||||
ws.on('frame', onOpen)
|
||||
ws.on('close', onClose)
|
||||
ws.on('error', onClose)
|
||||
}).finally(() => {
|
||||
ws.off('frame', onOpen)
|
||||
ws.off('close', onClose)
|
||||
ws.off('error', onClose)
|
||||
})
|
||||
|
||||
if (sendMsg) {
|
||||
sendRawMessage(sendMsg).catch(onClose!)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/** connection handshake */
|
||||
const validateConnection = async () => {
|
||||
let helloMsg: proto.IHandshakeMessage = {
|
||||
@@ -878,7 +953,9 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
requestPairingCode,
|
||||
/** Waits for the connection to WA to reach a state */
|
||||
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
|
||||
sendWAMBuffer
|
||||
sendWAMBuffer,
|
||||
executeUSyncQuery,
|
||||
onWhatsApp
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import type { SocketConfig } from '../Types'
|
||||
import { type 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user