diff --git a/jest.config.ts b/jest.config.ts index 614d9e13..8e9cf43d 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -2,12 +2,31 @@ import type { Config } from 'jest' const config: Config = { - roots: ['/src'], - testMatch: ['**.test.ts'], + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', + roots: ['/src', '/WAProto'], + testMatch: ['/src/**/*.test.ts'], extensionsToTreatAsEsm: ['.ts'], - transform: { - '^.+\\.(ts|tsx)$': ['ts-jest', { useESM: true }] + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', }, + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + module: 'esnext', + verbatimModuleSyntax: false, + allowImportingTsExtensions: false, + }, + }, + ], + '^.+\\.js$': ['ts-jest', { useESM: true }], + }, + transformIgnorePatterns: [ + 'node_modules/(?!(protobufjs|long|@protobufjs|@types/long)/)', + ], } -export default config \ No newline at end of file +export default config diff --git a/package.json b/package.json index 92c86f4f..999c05c5 100644 --- a/package.json +++ b/package.json @@ -57,12 +57,12 @@ "@types/jest": "^30.0.0", "@types/node": "^20.9.0", "@types/ws": "^8.0.0", - "@typescript-eslint/eslint-plugin": "^8.32.0", - "@typescript-eslint/parser": "^8.32.0", + "@typescript-eslint/eslint-plugin": "^8", + "@typescript-eslint/parser": "^8", "@whiskeysockets/eslint-config": "^1.0.0", "conventional-changelog-cli": "^2.2.2", "esbuild-register": "^3.6.0", - "eslint": "^9.31.0", + "eslint": "^9", "eslint-config-prettier": "^10.1.2", "eslint-plugin-prettier": "^5.4.0", "jest": "^30.0.5", diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json deleted file mode 100644 index 0590b2e6..00000000 --- a/src/Defaults/baileys-version.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "version": [2, 3000, 1023223821] -} diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index 4d2a0a1d..fec700e7 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -3,9 +3,8 @@ import { makeLibSignalRepository } from '../Signal/libsignal' import type { AuthenticationState, SocketConfig, WAVersion } from '../Types' import { Browsers } from '../Utils' import logger from '../Utils/logger' -import defaultVersion from './baileys-version.json' with { type: 'json' } -const { version } = defaultVersion +const version = [2, 3000, 1023223821] export const UNAUTHORIZED_CODES = [401, 403, 419] diff --git a/src/Signal/libsignal.ts b/src/Signal/libsignal.ts index 2e256ac6..5ee09fa9 100644 --- a/src/Signal/libsignal.ts +++ b/src/Signal/libsignal.ts @@ -12,8 +12,18 @@ import { SenderKeyRecord } from './Group/sender-key-record' import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage } from './Group' import { LIDMappingStore } from './lid-mapping' -export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository { - const lidMapping = new LIDMappingStore(auth.keys as SignalKeyStoreWithTransaction) +export function makeLibSignalRepository( + auth: SignalAuthState, + onWhatsAppFunc?: (...jids: string[]) => Promise< + | { + jid: string + exists: boolean + lid: string + }[] + | undefined + > +): SignalRepository { + const lidMapping = new LIDMappingStore(auth.keys as SignalKeyStoreWithTransaction, onWhatsAppFunc) const storage = signalStorage(auth, lidMapping) const parsedKeys = auth.keys as SignalKeyStoreWithTransaction diff --git a/src/Signal/lid-mapping.ts b/src/Signal/lid-mapping.ts index 3dc635ba..a031ce3f 100644 --- a/src/Signal/lid-mapping.ts +++ b/src/Signal/lid-mapping.ts @@ -1,53 +1,84 @@ import type { SignalKeyStoreWithTransaction } from '../Types' import logger from '../Utils/logger' -import { isJidUser, isLidUser, jidDecode } from '../WABinary' +import { isLidUser, isPnUser, jidDecode } from '../WABinary' +//TODO: Caching export class LIDMappingStore { private readonly keys: SignalKeyStoreWithTransaction + private onWhatsAppFunc?: (...jids: string[]) => Promise< + | { + jid: string + exists: boolean + lid: string + }[] + | undefined + > - constructor(keys: SignalKeyStoreWithTransaction) { + constructor( + keys: SignalKeyStoreWithTransaction, + onWhatsAppFunc?: (...jids: string[]) => Promise< + | { + jid: string + exists: boolean + lid: string + }[] + | undefined + > + ) { this.keys = keys + this.onWhatsAppFunc = onWhatsAppFunc // needed to get LID from PN if not found } /** * Store LID-PN mapping - USER LEVEL */ async storeLIDPNMapping(lid: string, pn: string): Promise { + return this.storeLIDPNMappings([{ lid, pn }]) + } + + /** + * Store LID-PN mapping - USER LEVEL + */ + async storeLIDPNMappings(pairs: { lid: string; pn: string }[]): Promise { // Validate inputs - if (!((isLidUser(lid) && isJidUser(pn)) || (isJidUser(lid) && isLidUser(pn)))) { - logger.warn(`Invalid LID-PN mapping: ${lid}, ${pn}`) - return + const pairMap: { [_: string]: string } = {} + for (const { lid, pn } of pairs) { + if (!((isLidUser(lid) && isPnUser(pn)) || (isPnUser(lid) && isLidUser(pn)))) { + logger.warn(`Invalid LID-PN mapping: ${lid}, ${pn}`) + continue + } + + const [lidJid, pnJid] = isLidUser(lid) ? [lid, pn] : [pn, lid] + + const lidDecoded = jidDecode(lidJid) + const pnDecoded = jidDecode(pnJid) + + if (!lidDecoded || !pnDecoded) return + + const pnUser = pnDecoded.user + const lidUser = lidDecoded.user + pairMap[pnUser] = lidUser } - const [lidJid, pnJid] = isLidUser(lid) ? [lid, pn] : [pn, lid] - - const lidDecoded = jidDecode(lidJid) - const pnDecoded = jidDecode(pnJid) - - if (!lidDecoded || !pnDecoded) return - - const pnUser = pnDecoded.user - const lidUser = lidDecoded.user - - logger.trace(`Storing USER LID mapping: PN ${pnUser} → LID ${lidUser}`) + logger.trace({ pairMap }, `Storing ${Object.keys(pairMap).length} pn mappings`) await this.keys.transaction(async () => { - await this.keys.set({ - 'lid-mapping': { - [pnUser]: lidUser, // "554396160286" -> "102765716062358" - [`${lidUser}_reverse`]: pnUser // "102765716062358_reverse" -> "554396160286" - } - }) + for (const [pnUser, lidUser] of Object.entries(pairMap)) { + await this.keys.set({ + 'lid-mapping': { + [pnUser]: lidUser, // "554396160286" -> "102765716062358" + [`${lidUser}_reverse`]: pnUser // "102765716062358_reverse" -> "554396160286" + } + }) + } }, 'lid-mapping') - - logger.trace(`USER LID mapping stored: PN ${pnUser} → LID ${lidUser}`) } /** * Get LID for PN - Returns device-specific LID based on user mapping */ async getLIDForPN(pn: string): Promise { - if (!isJidUser(pn)) return null + if (!isPnUser(pn)) return null const decoded = jidDecode(pn) if (!decoded) return null @@ -55,11 +86,16 @@ export class LIDMappingStore { // Look up user-level mapping (whatsmeow approach) const pnUser = decoded.user const stored = await this.keys.get('lid-mapping', [pnUser]) - const lidUser = stored[pnUser] + let lidUser = stored[pnUser] if (!lidUser) { - logger.trace(`No LID mapping found for PN user ${pnUser}`) - return null + logger.trace(`No LID mapping found for PN user ${pnUser}; getting from USync`) + const { exists, lid } = (await this.onWhatsAppFunc?.(pn))?.[0]! + if (exists) { + lidUser = jidDecode(lid)?.user + } else { + return null + } } if (typeof lidUser !== 'string') return null diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index f84f91ec..5b11df78 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -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, diff --git a/src/Socket/groups.ts b/src/Socket/groups.ts index b7b4ab8f..b811e72d 100644 --- a/src/Socket/groups.ts +++ b/src/Socket/groups.ts @@ -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'] } }), diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index a95eedd3..265808e3 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -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) => { 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' diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 0f56958c..742efb14 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -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 diff --git a/src/Socket/newsletter.ts b/src/Socket/newsletter.ts index 1c8b81f5..00c8555c 100644 --- a/src/Socket/newsletter.ts +++ b/src/Socket/newsletter.ts @@ -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 = (variables: Record, queryId: string, dataPath: string): Promise => { diff --git a/src/Socket/socket.ts b/src/Socket/socket.ts index 53486902..d6b83496 100644 --- a/src/Socket/socket.ts +++ b/src/Socket/socket.ts @@ -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(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 (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(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(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 (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(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 } } diff --git a/src/Socket/usync.ts b/src/Socket/usync.ts deleted file mode 100644 index 111dd640..00000000 --- a/src/Socket/usync.ts +++ /dev/null @@ -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 - } -} diff --git a/src/Types/Contact.ts b/src/Types/Contact.ts index 8d5cfca1..315f7e15 100644 --- a/src/Types/Contact.ts +++ b/src/Types/Contact.ts @@ -1,10 +1,10 @@ export interface Contact { - /** ID either in lid or jid format **/ + /** ID either in lid or jid format (preferred) **/ id: string - /** ID in Lid (anonymous) format (@lid) **/ + /** ID in LID format (@lid) **/ lid?: string - /** ID in Phone Number format (@s.whatsapp.net) **/ - jid?: string + /** ID in PN format (@s.whatsapp.net) **/ + phoneNumber?: string /** name of the contact, you have saved on your WA */ name?: string /** name of the contact, the contact has set on their own on WA */ diff --git a/src/Types/Events.ts b/src/Types/Events.ts index 9afe972a..0158fbc1 100644 --- a/src/Types/Events.ts +++ b/src/Types/Events.ts @@ -30,7 +30,7 @@ export type BaileysEventMap = { 'chats.upsert': Chat[] /** update the given chats */ 'chats.update': ChatUpdate[] - 'chats.phoneNumberShare': { lid: string; jid: string } + 'lid-mapping.update': { lid: string; pn: string } /** delete chats with given ID */ 'chats.delete': string[] /** presence of contact in a chat updated */ diff --git a/src/Types/GroupMetadata.ts b/src/Types/GroupMetadata.ts index 36d0ed9c..ab7a354f 100644 --- a/src/Types/GroupMetadata.ts +++ b/src/Types/GroupMetadata.ts @@ -1,4 +1,5 @@ import type { Contact } from './Contact' +import type { WAMessageAddressingMode } from './Message' export type GroupParticipant = Contact & { isAdmin?: boolean @@ -14,21 +15,22 @@ export type RequestJoinMethod = 'invite_link' | 'linked_group_join' | 'non_admin export interface GroupMetadata { id: string + notify?: string /** group uses 'lid' or 'pn' to send messages */ - addressingMode: 'pn' | 'lid' + addressingMode?: WAMessageAddressingMode owner: string | undefined - ownerJid?: string | undefined + ownerPn?: string | undefined owner_country_code?: string | undefined subject: string /** group subject owner */ subjectOwner?: string - subjectOwnerJid?: string + subjectOwnerPn?: string /** group subject modification date */ subjectTime?: number creation?: number desc?: string descOwner?: string - descOwnerJid?: string + descOwnerPn?: string descId?: string descTime?: number /** if this group is part of a community, it returns the jid of the community to which it belongs */ diff --git a/src/Types/Message.ts b/src/Types/Message.ts index bf1af218..57322691 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -14,12 +14,10 @@ export type WAMessageContent = proto.IMessage export type WAContactMessage = proto.Message.IContactMessage export type WAContactsArrayMessage = proto.Message.IContactsArrayMessage export type WAMessageKey = proto.IMessageKey & { - senderLid?: string + remoteJidAlt?: string + participantAlt?: string server_id?: string - senderPn?: string - participantLid?: string - participantPn?: string - isViewOnce?: boolean + isViewOnce?: boolean // TODO: remove out of the message key, place in WebMessageInfo } export type WATextMessage = proto.Message.IExtendedTextMessage export type WAContextInfo = proto.IContextInfo @@ -39,6 +37,11 @@ export type WAMediaUpload = Buffer | WAMediaPayloadStream | WAMediaPayloadURL /** Set of message types that are supported by the library */ export type MessageType = keyof proto.Message +export enum WAMessageAddressingMode { + PN = 'pn', + LID = 'lid' +} + export type MessageWithContextInfo = | 'imageMessage' | 'contactMessage' diff --git a/src/Types/Socket.ts b/src/Types/Socket.ts index 03e721f7..7dcec711 100644 --- a/src/Types/Socket.ts +++ b/src/Types/Socket.ts @@ -137,5 +137,15 @@ export type SocketConfig = { /** cached group metadata, use to prevent redundant requests to WA & speed up msg sending */ cachedGroupMetadata: (jid: string) => Promise - makeSignalRepository: (auth: SignalAuthState) => SignalRepository + makeSignalRepository: ( + auth: SignalAuthState, + onWhatsAppFunc?: (...jids: string[]) => Promise< + | { + jid: string + exists: boolean + lid: string + }[] + | undefined + > + ) => SignalRepository } diff --git a/src/Utils/chat-utils.ts b/src/Utils/chat-utils.ts index 73772df7..845b5ce4 100644 --- a/src/Utils/chat-utils.ts +++ b/src/Utils/chat-utils.ts @@ -19,14 +19,7 @@ import { LabelAssociationType, type MessageLabelAssociation } from '../Types/LabelAssociation' -import { - type BinaryNode, - getBinaryNodeChild, - getBinaryNodeChildren, - isJidGroup, - isJidUser, - jidNormalizedUser -} from '../WABinary' +import { type BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary' import { aesDecrypt, aesEncrypt, hkdf, hmacSign } from './crypto' import { toNumber } from './generics' import type { ILogger } from './logger' @@ -845,7 +838,7 @@ export const processSyncAction = ( id: id!, name: action.contactAction.fullName!, lid: action.contactAction.lidJid || undefined, - jid: isJidUser(id) ? id : undefined + phoneNumber: action.contactAction.pnJid || undefined } ]) } else if (action?.pushNameSetting) { diff --git a/src/Utils/decode-wa-message.ts b/src/Utils/decode-wa-message.ts index 52042e02..4afe13d1 100644 --- a/src/Utils/decode-wa-message.ts +++ b/src/Utils/decode-wa-message.ts @@ -6,14 +6,12 @@ import { type BinaryNode, isJidBroadcast, isJidGroup, - isJidMetaIa, + isJidMetaAI, isJidNewsletter, isJidStatusBroadcast, - isJidUser, isLidUser, - jidDecode, - jidEncode, - jidNormalizedUser + isPnUser, + transferDevice } from '../WABinary' import { unpadRandomMax16 } from './generics' import type { ILogger } from './logger' @@ -23,17 +21,7 @@ const getDecryptionJid = async (sender: string, repository: SignalRepository): P return sender } - const lidMapping = repository.getLIDMappingStore() - const normalizedSender = jidNormalizedUser(sender) - const lidForPN = await lidMapping.getLIDForPN(normalizedSender) - - if (lidForPN?.includes('@lid')) { - const senderDecoded = jidDecode(sender) - const deviceId = senderDecoded?.device || 0 - return jidEncode(jidDecode(lidForPN)!.user, 'lid', deviceId) - } - - return sender + return (await repository.getLIDMappingStore().getLIDForPN(sender))! } const storeMappingFromEnvelope = async ( @@ -45,7 +33,7 @@ const storeMappingFromEnvelope = async ( ): Promise => { const { senderAlt } = extractAddressingContext(stanza) - if (senderAlt && isLidUser(senderAlt) && isJidUser(sender) && decryptionJid === sender) { + if (senderAlt && isLidUser(senderAlt) && isPnUser(sender) && decryptionJid === sender) { try { await repository.storeLIDPNMapping(senderAlt, sender) logger.debug({ sender, senderAlt }, 'Stored LID mapping from envelope') @@ -95,14 +83,23 @@ export const extractAddressingContext = (stanza: BinaryNode) => { let senderAlt: string | undefined let recipientAlt: string | undefined + const sender = stanza.attrs.participant || stanza.attrs.from + if (addressingMode === 'lid') { // Message is LID-addressed: sender is LID, extract corresponding PN - senderAlt = stanza.attrs.participant_pn || stanza.attrs.sender_pn + // without device data + senderAlt = stanza.attrs.participant_pn || stanza.attrs.sender_pn || stanza.attrs.peer_recipient_pn recipientAlt = stanza.attrs.recipient_pn + // with device data + if (sender && senderAlt) senderAlt = transferDevice(sender, senderAlt) } else { // Message is PN-addressed: sender is PN, extract corresponding LID - senderAlt = stanza.attrs.participant_lid || stanza.attrs.sender_lid + // without device data + senderAlt = stanza.attrs.participant_lid || stanza.attrs.sender_lid || stanza.attrs.peer_recipient_lid recipientAlt = stanza.attrs.recipient_lid + + //with device data + if (sender && senderAlt) senderAlt = transferDevice(sender, senderAlt) } return { @@ -126,11 +123,13 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin const participant: string | undefined = stanza.attrs.participant const recipient: string | undefined = stanza.attrs.recipient + const addressingContext = extractAddressingContext(stanza) + const isMe = (jid: string) => areJidsSameUser(jid, meId) const isMeLid = (jid: string) => areJidsSameUser(jid, meLid) - if (isJidUser(from) || isLidUser(from)) { - if (recipient && !isJidMetaIa(recipient)) { + if (isPnUser(from) || isLidUser(from)) { + if (recipient && !isJidMetaAI(recipient)) { if (!isMe(from!) && !isMeLid(from!)) { throw new Boom('receipient present, but msg not from me', { data: stanza }) } @@ -177,13 +176,11 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin const key: WAMessageKey = { remoteJid: chatId, + remoteJidAlt: !isJidGroup(chatId) ? addressingContext.senderAlt : undefined, fromMe, id: msgId, - senderLid: stanza?.attrs?.sender_lid, - senderPn: stanza?.attrs?.sender_pn || stanza?.attrs?.peer_recipient_pn, participant, - participantPn: stanza?.attrs?.participant_pn, - participantLid: stanza?.attrs?.participant_lid, + participantAlt: isJidGroup(chatId) ? addressingContext.senderAlt : undefined, ...(msgType === 'newsletter' && stanza.attrs.server_id ? { server_id: stanza.attrs.server_id } : {}) } @@ -228,7 +225,7 @@ export const decryptMessageNode = ( } if (tag === 'unavailable' && attrs.type === 'view_once') { - fullMessage.key.isViewOnce = true + fullMessage.key.isViewOnce = true // TODO: remove from here and add a STUB TYPE } if (tag !== 'enc' && tag !== 'plaintext') { @@ -243,6 +240,12 @@ export const decryptMessageNode = ( let msgBuffer: Uint8Array + const user = isPnUser(sender) ? sender : author // TODO: flaky logic + const decryptionJid = await getDecryptionJid(user, repository) + if (tag !== 'plaintext') { + await storeMappingFromEnvelope(stanza, user, decryptionJid, repository, logger) + } + try { const e2eType = tag === 'plaintext' ? 'plaintext' : attrs.type @@ -256,16 +259,11 @@ export const decryptMessageNode = ( break case 'pkmsg': case 'msg': - const user = isJidUser(sender) ? sender : author - const decryptionJid = await getDecryptionJid(user, repository) - msgBuffer = await repository.decryptMessage({ jid: decryptionJid, type: e2eType, ciphertext: content }) - - await storeMappingFromEnvelope(stanza, user, decryptionJid, repository, logger) break case 'plaintext': msgBuffer = content diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index 13facf88..612100f4 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -3,8 +3,7 @@ import axios, { type AxiosRequestConfig } from 'axios' import { createHash, randomBytes } from 'crypto' import { platform, release } from 'os' import { proto } from '../../WAProto/index.js' -import version from '../Defaults/baileys-version.json' with { type: 'json' } -const baileysVersion = version.version +const baileysVersion = [2, 3000, 1023223821] import type { BaileysEventEmitter, BaileysEventMap, diff --git a/src/Utils/history.ts b/src/Utils/history.ts index 83ca5c83..b2cbe036 100644 --- a/src/Utils/history.ts +++ b/src/Utils/history.ts @@ -4,7 +4,6 @@ import { inflate } from 'zlib' import { proto } from '../../WAProto/index.js' import type { Chat, Contact } from '../Types' import { WAMessageStubType } from '../Types' -import { isJidUser } from '../WABinary' import { toNumber } from './generics' import { normalizeMessageContent } from './messages' import { downloadContentFromMessage } from './messages-media' @@ -42,7 +41,7 @@ export const processHistoryMessage = (item: proto.IHistorySync) => { id: chat.id, name: chat.name || undefined, lid: chat.lidJid || undefined, - jid: isJidUser(chat.id) ? chat.id : undefined + phoneNumber: chat.pnJid || undefined }) const msgs = chat.messages || [] diff --git a/src/Utils/message-retry-manager.ts b/src/Utils/message-retry-manager.ts index 5de5bde6..b2a47bc4 100644 --- a/src/Utils/message-retry-manager.ts +++ b/src/Utils/message-retry-manager.ts @@ -1,5 +1,5 @@ import { LRUCache } from 'lru-cache' -import { proto } from '../../WAProto' +import type { proto } from '../../WAProto/index.js' import type { ILogger } from './logger' /** Number of sent messages to cache in memory for handling retry receipts */ diff --git a/src/Utils/process-message.ts b/src/Utils/process-message.ts index dfd62211..73bedc3a 100644 --- a/src/Utils/process-message.ts +++ b/src/Utils/process-message.ts @@ -9,7 +9,8 @@ import type { ParticipantAction, RequestJoinAction, RequestJoinMethod, - SignalKeyStoreWithTransaction + SignalKeyStoreWithTransaction, + SignalRepository } from '../Types' import { WAMessageStubType } from '../Types' import { getContentType, normalizeMessageContent } from '../Utils/messages' @@ -27,6 +28,7 @@ type ProcessMessageContext = { ev: BaileysEventEmitter logger?: ILogger options: AxiosRequestConfig<{}> + signalRepository: SignalRepository } const REAL_MSG_STUB_TYPES = new Set([ @@ -145,7 +147,16 @@ export function decryptPollVote( const processMessage = async ( message: proto.IWebMessageInfo, - { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, keyStore, logger, options }: ProcessMessageContext + { + shouldProcessHistoryMsg, + placeholderResendCache, + ev, + creds, + signalRepository, + keyStore, + logger, + options + }: ProcessMessageContext ) => { const meId = creds.me!.id const { accountSettings } = creds @@ -292,6 +303,19 @@ const processMessage = async ( } ]) break + case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC: + const lidMappingStore = signalRepository.getLIDMappingStore() + const encodedPayload = protocolMsg.lidMigrationMappingSyncMessage?.encodedMappingPayload! + const { pnToLidMappings, chatDbMigrationTimestamp } = + proto.LIDMigrationMappingSyncPayload.decode(encodedPayload) + logger?.debug({ pnToLidMappings, chatDbMigrationTimestamp }, 'got lid mappings and chat db migration timestamp') + const pairs = [] + for (const { pn, latestLid, assignedLid } of pnToLidMappings) { + const lid = latestLid || assignedLid + pairs.push({ lid: `${lid}@lid`, pn: `${pn}@s.whatsapp.net` }) + } + + await lidMappingStore.storeLIDPNMappings(pairs) } } else if (content?.reactionMessage) { const reaction: proto.IReaction = { diff --git a/src/Utils/validate-connection.ts b/src/Utils/validate-connection.ts index cfa4b6ce..9406922d 100644 --- a/src/Utils/validate-connection.ts +++ b/src/Utils/validate-connection.ts @@ -133,6 +133,7 @@ export const configureSuccessfulPairing = ( const bizName = businessNode?.attrs.name const jid = deviceNode.attrs.jid + const lid = deviceNode.attrs.lid const { details, hmac, accountType } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityNode.content as Buffer) const isHostedAccount = accountType !== undefined && accountType === proto.ADVEncryptionType.HOSTED @@ -154,7 +155,7 @@ export const configureSuccessfulPairing = ( const deviceMsg = Buffer.concat([devicePrefix, deviceDetails, signedIdentityKey.public, accountSignatureKey]) account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg) - const identity = createSignalIdentity(jid!, accountSignatureKey) + const identity = createSignalIdentity(lid!, accountSignatureKey) const accountEnc = encodeSignedDeviceIdentity(account, false) const deviceIdentity = proto.ADVDeviceIdentity.decode(account.details) @@ -183,7 +184,7 @@ export const configureSuccessfulPairing = ( const authUpdate: Partial = { account, - me: { id: jid!, name: bizName }, + me: { id: jid!, name: bizName, lid }, signalIdentities: [...(signalIdentities || []), identity], platform: platformNode?.attrs.name } diff --git a/src/WABinary/jid-utils.ts b/src/WABinary/jid-utils.ts index 624218df..74e61cb2 100644 --- a/src/WABinary/jid-utils.ts +++ b/src/WABinary/jid-utils.ts @@ -44,11 +44,11 @@ export const jidDecode = (jid: string | undefined): FullJid | undefined => { /** is the jid a user */ export const areJidsSameUser = (jid1: string | undefined, jid2: string | undefined) => jidDecode(jid1)?.user === jidDecode(jid2)?.user -/** is the jid Meta IA */ -export const isJidMetaIa = (jid: string | undefined) => jid?.endsWith('@bot') -/** is the jid a user */ -export const isJidUser = (jid: string | undefined) => jid?.endsWith('@s.whatsapp.net') -/** is the jid a group */ +/** is the jid Meta AI */ +export const isJidMetaAI = (jid: string | undefined) => jid?.endsWith('@bot') +/** is the jid a PN user */ +export const isPnUser = (jid: string | undefined) => jid?.endsWith('@s.whatsapp.net') +/** is the jid a LID */ export const isLidUser = (jid: string | undefined) => jid?.endsWith('@lid') /** is the jid a broadcast */ export const isJidBroadcast = (jid: string | undefined) => jid?.endsWith('@broadcast') @@ -72,3 +72,10 @@ export const jidNormalizedUser = (jid: string | undefined) => { const { user, server } = result return jidEncode(user, server === 'c.us' ? 's.whatsapp.net' : (server as JidServer)) } + +export const transferDevice = (fromJid: string, toJid: string) => { + const fromDecoded = jidDecode(fromJid) + const deviceId = fromDecoded?.device || 0 + const { server, user } = jidDecode(toJid)! + return jidEncode(user, server, deviceId) +} diff --git a/yarn.lock b/yarn.lock index fe902ad0..21b430eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -622,7 +622,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.7.0": +"@eslint-community/eslint-utils@npm:^4.7.0": version: 4.7.0 resolution: "@eslint-community/eslint-utils@npm:4.7.0" dependencies: @@ -633,6 +633,17 @@ __metadata: languageName: node linkType: hard +"@eslint-community/eslint-utils@npm:^4.8.0": + version: 4.8.0 + resolution: "@eslint-community/eslint-utils@npm:4.8.0" + dependencies: + eslint-visitor-keys: "npm:^3.4.3" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10c0/33b93d2a4e9d5fe4c11d02d0fc5ed69e12fcb1e7ca031ded0d6adb24e768c36df77288ed79eecc784f9db34219816247db27688dfe869fb7fbf096840a097d7a + languageName: node + linkType: hard + "@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1": version: 4.12.1 resolution: "@eslint-community/regexpp@npm:4.12.1" @@ -651,19 +662,19 @@ __metadata: languageName: node linkType: hard -"@eslint/config-helpers@npm:^0.3.0": - version: 0.3.0 - resolution: "@eslint/config-helpers@npm:0.3.0" - checksum: 10c0/013ae7b189eeae8b30cc2ee87bc5c9c091a9cd615579003290eb28bebad5d78806a478e74ba10b3fe08ed66975b52af7d2cd4b4b43990376412b14e5664878c8 +"@eslint/config-helpers@npm:^0.3.1": + version: 0.3.1 + resolution: "@eslint/config-helpers@npm:0.3.1" + checksum: 10c0/f6c5b3a0b76a0d7d84cc93e310c259e6c3e0792ddd0a62c5fc0027796ffae44183432cb74b2c2b1162801ee1b1b34a6beb5d90a151632b4df7349f994146a856 languageName: node linkType: hard -"@eslint/core@npm:^0.15.0, @eslint/core@npm:^0.15.1": - version: 0.15.1 - resolution: "@eslint/core@npm:0.15.1" +"@eslint/core@npm:^0.15.2": + version: 0.15.2 + resolution: "@eslint/core@npm:0.15.2" dependencies: "@types/json-schema": "npm:^7.0.15" - checksum: 10c0/abaf641940776638b8c15a38d99ce0dac551a8939310ec81b9acd15836a574cf362588eaab03ab11919bc2a0f9648b19ea8dee33bf12675eb5b6fd38bda6f25e + checksum: 10c0/c17a6dc4f5a6006ecb60165cc38bcd21fefb4a10c7a2578a0cfe5813bbd442531a87ed741da5adab5eb678e8e693fda2e2b14555b035355537e32bcec367ea17 languageName: node linkType: hard @@ -684,7 +695,14 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.31.0, @eslint/js@npm:^9.31.0": +"@eslint/js@npm:9.35.0": + version: 9.35.0 + resolution: "@eslint/js@npm:9.35.0" + checksum: 10c0/d40fe38724bc76c085c0b753cdf937fa35c0d6807ae76b2632e3f5f66c3040c91adcf1aff2ce70b4f45752e60629fadc415eeec9af3be3c274bae1cac54b9840 + languageName: node + linkType: hard + +"@eslint/js@npm:^9.31.0": version: 9.31.0 resolution: "@eslint/js@npm:9.31.0" checksum: 10c0/f9d4c73d0fafe70679a418cbb25ab7ebcc8f1dba6c32456d6f8ba5a137d583ecff233cfe10f61f41d7d4d2220e94cff1f39fc7ed1fa3819d1888dee1cad678ea @@ -698,13 +716,13 @@ __metadata: languageName: node linkType: hard -"@eslint/plugin-kit@npm:^0.3.1": - version: 0.3.3 - resolution: "@eslint/plugin-kit@npm:0.3.3" +"@eslint/plugin-kit@npm:^0.3.5": + version: 0.3.5 + resolution: "@eslint/plugin-kit@npm:0.3.5" dependencies: - "@eslint/core": "npm:^0.15.1" + "@eslint/core": "npm:^0.15.2" levn: "npm:^0.4.1" - checksum: 10c0/c61888eb8757abc0d25a53c1832f85521c2f347126c475eb32d3596be3505e8619e0ceddee7346d195089a2eb1633b61e6127a5772b8965a85eb9f55b8b1cebe + checksum: 10c0/c178c1b58c574200c0fd125af3e4bc775daba7ce434ba6d1eeaf9bcb64b2e9fea75efabffb3ed3ab28858e55a016a5efa95f509994ee4341b341199ca630b89e languageName: node linkType: hard @@ -2162,7 +2180,28 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^8.32.0, @typescript-eslint/eslint-plugin@npm:^8.37.0": +"@typescript-eslint/eslint-plugin@npm:^8": + version: 8.42.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.42.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.10.0" + "@typescript-eslint/scope-manager": "npm:8.42.0" + "@typescript-eslint/type-utils": "npm:8.42.0" + "@typescript-eslint/utils": "npm:8.42.0" + "@typescript-eslint/visitor-keys": "npm:8.42.0" + graphemer: "npm:^1.4.0" + ignore: "npm:^7.0.0" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^2.1.0" + peerDependencies: + "@typescript-eslint/parser": ^8.42.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/835fd7497f0e4eaef55dc3d94079acc0ad1dc74735916915f160419b1e7f44d04fbce683b4871148d1af33046bd5ae3fed59103d4c49460776b560c42173bbff + languageName: node + linkType: hard + +"@typescript-eslint/eslint-plugin@npm:^8.37.0": version: 8.37.0 resolution: "@typescript-eslint/eslint-plugin@npm:8.37.0" dependencies: @@ -2183,7 +2222,23 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^8.32.0, @typescript-eslint/parser@npm:^8.37.0": +"@typescript-eslint/parser@npm:^8": + version: 8.42.0 + resolution: "@typescript-eslint/parser@npm:8.42.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:8.42.0" + "@typescript-eslint/types": "npm:8.42.0" + "@typescript-eslint/typescript-estree": "npm:8.42.0" + "@typescript-eslint/visitor-keys": "npm:8.42.0" + debug: "npm:^4.3.4" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/f071154bce7f874449236919a7367d977317959fe6d454fe5369ca54dee7d057fe3b8b250c5990ea4205a9c52fd59702da63d1721895c72d745168aa31532112 + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:^8.37.0": version: 8.37.0 resolution: "@typescript-eslint/parser@npm:8.37.0" dependencies: @@ -2212,6 +2267,19 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/project-service@npm:8.42.0": + version: 8.42.0 + resolution: "@typescript-eslint/project-service@npm:8.42.0" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.42.0" + "@typescript-eslint/types": "npm:^8.42.0" + debug: "npm:^4.3.4" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/788b0bc52683be376cd768a4fed3202cdaccc86f231ec94a0f6bbb1389fdfd0e14c505f03015cefb73869de63c8089b78a169ed957048a1e5ee1b6250ec19604 + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.37.0": version: 8.37.0 resolution: "@typescript-eslint/scope-manager@npm:8.37.0" @@ -2222,6 +2290,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.42.0": + version: 8.42.0 + resolution: "@typescript-eslint/scope-manager@npm:8.42.0" + dependencies: + "@typescript-eslint/types": "npm:8.42.0" + "@typescript-eslint/visitor-keys": "npm:8.42.0" + checksum: 10c0/caca15f2124909c588ed3e48fe0769ad8baa296a0b229f724ec94f5f746e486e08dd49eeddd66d01f09e2ddaed03f9e18d7b535a44196d413f283e22f929f623 + languageName: node + linkType: hard + "@typescript-eslint/tsconfig-utils@npm:8.37.0, @typescript-eslint/tsconfig-utils@npm:^8.37.0": version: 8.37.0 resolution: "@typescript-eslint/tsconfig-utils@npm:8.37.0" @@ -2231,6 +2309,15 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/tsconfig-utils@npm:8.42.0, @typescript-eslint/tsconfig-utils@npm:^8.42.0": + version: 8.42.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.42.0" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/03882eeee279fafa2cb4ee3154742417fd29395b3bfe3f867d9d4cb9cb68d1200c885c35b96dd558a1aff8561ac3700cff8ca7680a5cf34e5e0e136a6ee3c30c + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.37.0": version: 8.37.0 resolution: "@typescript-eslint/type-utils@npm:8.37.0" @@ -2247,6 +2334,22 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/type-utils@npm:8.42.0": + version: 8.42.0 + resolution: "@typescript-eslint/type-utils@npm:8.42.0" + dependencies: + "@typescript-eslint/types": "npm:8.42.0" + "@typescript-eslint/typescript-estree": "npm:8.42.0" + "@typescript-eslint/utils": "npm:8.42.0" + debug: "npm:^4.3.4" + ts-api-utils: "npm:^2.1.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/47e5f7276cafd7719d3e2f2e456fa988927e658d15c2c188a692d9c639f9d76f582a6c133cb1bf01eba9027e1022eb6b79b57861a96302460e5e847c2b536afa + languageName: node + linkType: hard + "@typescript-eslint/types@npm:8.37.0, @typescript-eslint/types@npm:^8.37.0": version: 8.37.0 resolution: "@typescript-eslint/types@npm:8.37.0" @@ -2254,6 +2357,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.42.0, @typescript-eslint/types@npm:^8.42.0": + version: 8.42.0 + resolution: "@typescript-eslint/types@npm:8.42.0" + checksum: 10c0/d585dff5005328282cc59f9402e886a3db64727906ad3e68b49d7ef73bc07bef3ed569287ba826ebaa07b69be42a72232a38529951d64c28cebd83db0892cd33 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.37.0": version: 8.37.0 resolution: "@typescript-eslint/typescript-estree@npm:8.37.0" @@ -2274,6 +2384,26 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.42.0": + version: 8.42.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.42.0" + dependencies: + "@typescript-eslint/project-service": "npm:8.42.0" + "@typescript-eslint/tsconfig-utils": "npm:8.42.0" + "@typescript-eslint/types": "npm:8.42.0" + "@typescript-eslint/visitor-keys": "npm:8.42.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^2.1.0" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/2d3354d780421cfa90f812048984c43cd47aabecef7a5c0f56ad0b91331cb369d1c8366da90bf9a8f6df47df3741f9e16897e998f16270ac55376f519b775c23 + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:8.37.0": version: 8.37.0 resolution: "@typescript-eslint/utils@npm:8.37.0" @@ -2289,6 +2419,21 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:8.42.0": + version: 8.42.0 + resolution: "@typescript-eslint/utils@npm:8.42.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.7.0" + "@typescript-eslint/scope-manager": "npm:8.42.0" + "@typescript-eslint/types": "npm:8.42.0" + "@typescript-eslint/typescript-estree": "npm:8.42.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/acf30019023669ddae00c02cabfa74fc12defccd4703e552ab5115edbeceaaf1688c1586873bf66aefeb3f03eb1ed456905403303913c724db38bf030e40a700 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.37.0": version: 8.37.0 resolution: "@typescript-eslint/visitor-keys@npm:8.37.0" @@ -2299,6 +2444,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.42.0": + version: 8.42.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.42.0" + dependencies: + "@typescript-eslint/types": "npm:8.42.0" + eslint-visitor-keys: "npm:^4.2.1" + checksum: 10c0/22c942f2a100d71c08f952b976446e824ddf227d4ac02b7016e12d4a33804ab06072d570695baed3565d0a08a1d3fa6ff3ccf97a122d63e65780f871d597f1b1 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.3.0": version: 1.3.0 resolution: "@ungap/structured-clone@npm:1.3.0" @@ -2849,14 +3004,14 @@ __metadata: "@types/jest": "npm:^30.0.0" "@types/node": "npm:^20.9.0" "@types/ws": "npm:^8.0.0" - "@typescript-eslint/eslint-plugin": "npm:^8.32.0" - "@typescript-eslint/parser": "npm:^8.32.0" + "@typescript-eslint/eslint-plugin": "npm:^8" + "@typescript-eslint/parser": "npm:^8" "@whiskeysockets/eslint-config": "npm:^1.0.0" async-mutex: "npm:^0.5.0" axios: "npm:^1.6.0" conventional-changelog-cli: "npm:^2.2.2" esbuild-register: "npm:^3.6.0" - eslint: "npm:^9.31.0" + eslint: "npm:^9" eslint-config-prettier: "npm:^10.1.2" eslint-plugin-prettier: "npm:^5.4.0" jest: "npm:^30.0.5" @@ -4513,18 +4668,18 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^9.31.0": - version: 9.31.0 - resolution: "eslint@npm:9.31.0" +"eslint@npm:^9": + version: 9.35.0 + resolution: "eslint@npm:9.35.0" dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" + "@eslint-community/eslint-utils": "npm:^4.8.0" "@eslint-community/regexpp": "npm:^4.12.1" "@eslint/config-array": "npm:^0.21.0" - "@eslint/config-helpers": "npm:^0.3.0" - "@eslint/core": "npm:^0.15.0" + "@eslint/config-helpers": "npm:^0.3.1" + "@eslint/core": "npm:^0.15.2" "@eslint/eslintrc": "npm:^3.3.1" - "@eslint/js": "npm:9.31.0" - "@eslint/plugin-kit": "npm:^0.3.1" + "@eslint/js": "npm:9.35.0" + "@eslint/plugin-kit": "npm:^0.3.5" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" "@humanwhocodes/retry": "npm:^0.4.2" @@ -4559,7 +4714,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/3fd1cd5b38b907ecb3f5e7537ab91204efb38bc1ad0ca6e46fc4112f13b594272ff56e641b41580049bc333fbcb5b1b99ca9a542e8406e7da5e951068cbaec77 + checksum: 10c0/798c527520ccf62106f8cd210bd1db1f8eb1b0e7a56feb0a8b322bf3a1e6a0bc6dc3a414542c22b1b393d58d5e3cd0252c44c023049de9067b836450503a2f03 languageName: node linkType: hard