fix: send message speed, lid logic, remove messages-send useless functions (#1794)
* fix: remove redundant migration * fix: remove deduplicatelidpnjids * fix: assertsessions complexity * fix: assertsessions call * fix: remove getencryptionjid * fix: add wirejid to injecte2esession, remove libsignal lid migration * fix: logger lid-mapping * fix: injecte2esession decoded approach * fix: changes made by @AstroX11 * fix: lint * fix: lint * fix: lint * Revert "fix: injecte2esession decoded approach" This reverts commit 4e368296ed084398b8a173ec117dc2478e481748. * fix: centralize getlidforpn calls in messages-send * fix: add resolvechunklids to signal * fix: remove useless arrays * fix: assertsessions logic * fix: lint * Revert "fix: assertsessions logic" This reverts commit 65b0730891c91573edcab1c96af010db54382fba. * fix: remove onwhatsapp fallback to prevent rate limit, migrate sessions when lid-map is created, new migration logic and user devices key * fix: migrate session devices, socket creation * fix: add back decryptionjid validation * fix: add resolveSignalAddress to get lid * fix: type error migration * fix: lint * fix: device level cache, proposed changes
This commit is contained in:
+136
-107
@@ -1,8 +1,10 @@
|
|||||||
/* @ts-ignore */
|
/* @ts-ignore */
|
||||||
import * as libsignal from 'libsignal'
|
import * as libsignal from 'libsignal'
|
||||||
|
import { LRUCache } from 'lru-cache'
|
||||||
import type { SignalAuthState, SignalKeyStoreWithTransaction } from '../Types'
|
import type { SignalAuthState, SignalKeyStoreWithTransaction } from '../Types'
|
||||||
import type { SignalRepositoryWithLIDStore } from '../Types/Signal'
|
import type { SignalRepositoryWithLIDStore } from '../Types/Signal'
|
||||||
import { generateSignalPubKey } from '../Utils'
|
import { generateSignalPubKey } from '../Utils'
|
||||||
|
import type { ILogger } from '../Utils/logger'
|
||||||
import { jidDecode, transferDevice } from '../WABinary'
|
import { jidDecode, transferDevice } from '../WABinary'
|
||||||
import type { SenderKeyStore } from './Group/group_cipher'
|
import type { SenderKeyStore } from './Group/group_cipher'
|
||||||
import { SenderKeyName } from './Group/sender-key-name'
|
import { SenderKeyName } from './Group/sender-key-name'
|
||||||
@@ -10,21 +12,16 @@ import { SenderKeyRecord } from './Group/sender-key-record'
|
|||||||
import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage } from './Group'
|
import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage } from './Group'
|
||||||
import { LIDMappingStore } from './lid-mapping'
|
import { LIDMappingStore } from './lid-mapping'
|
||||||
|
|
||||||
export function makeLibSignalRepository(
|
export function makeLibSignalRepository(auth: SignalAuthState, logger: ILogger): SignalRepositoryWithLIDStore {
|
||||||
auth: SignalAuthState,
|
const lidMapping = new LIDMappingStore(auth.keys as SignalKeyStoreWithTransaction, logger)
|
||||||
onWhatsAppFunc?: (...jids: string[]) => Promise<
|
|
||||||
| {
|
|
||||||
jid: string
|
|
||||||
exists: boolean
|
|
||||||
lid: string
|
|
||||||
}[]
|
|
||||||
| undefined
|
|
||||||
>
|
|
||||||
): SignalRepositoryWithLIDStore {
|
|
||||||
const lidMapping = new LIDMappingStore(auth.keys as SignalKeyStoreWithTransaction, onWhatsAppFunc)
|
|
||||||
const storage = signalStorage(auth, lidMapping)
|
const storage = signalStorage(auth, lidMapping)
|
||||||
|
|
||||||
const parsedKeys = auth.keys as SignalKeyStoreWithTransaction
|
const parsedKeys = auth.keys as SignalKeyStoreWithTransaction
|
||||||
|
const migratedSessionCache = new LRUCache<string, true>({
|
||||||
|
ttl: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||||
|
ttlAutopurge: true,
|
||||||
|
updateAgeOnGet: true
|
||||||
|
})
|
||||||
|
|
||||||
function isLikelySyncMessage(addr: libsignal.ProtocolAddress): boolean {
|
function isLikelySyncMessage(addr: libsignal.ProtocolAddress): boolean {
|
||||||
const key = addr.toString()
|
const key = addr.toString()
|
||||||
@@ -110,34 +107,7 @@ export function makeLibSignalRepository(
|
|||||||
},
|
},
|
||||||
|
|
||||||
async encryptMessage({ jid, data }) {
|
async encryptMessage({ jid, data }) {
|
||||||
// LID SINGLE SOURCE OF TRUTH: Always prefer LID when available
|
const addr = jidToSignalProtocolAddress(jid)
|
||||||
let encryptionJid = jid
|
|
||||||
|
|
||||||
// Check for LID mapping and use it if session exists
|
|
||||||
if (jid.includes('@s.whatsapp.net')) {
|
|
||||||
const lidForPN = await lidMapping.getLIDForPN(jid)
|
|
||||||
if (lidForPN?.includes('@lid')) {
|
|
||||||
const lidAddr = jidToSignalProtocolAddress(lidForPN)
|
|
||||||
const { [lidAddr.toString()]: lidSession } = await auth.keys.get('session', [lidAddr.toString()])
|
|
||||||
|
|
||||||
if (lidSession) {
|
|
||||||
// LID session exists, use it
|
|
||||||
encryptionJid = lidForPN
|
|
||||||
} else {
|
|
||||||
// Try to migrate if PN session exists
|
|
||||||
const pnAddr = jidToSignalProtocolAddress(jid)
|
|
||||||
const { [pnAddr.toString()]: pnSession } = await auth.keys.get('session', [pnAddr.toString()])
|
|
||||||
|
|
||||||
if (pnSession) {
|
|
||||||
// Migrate PN to LID
|
|
||||||
await repository.migrateSession([jid], lidForPN)
|
|
||||||
encryptionJid = lidForPN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addr = jidToSignalProtocolAddress(encryptionJid)
|
|
||||||
const cipher = new libsignal.SessionCipher(storage, addr)
|
const cipher = new libsignal.SessionCipher(storage, addr)
|
||||||
|
|
||||||
// Use transaction to ensure atomicity
|
// Use transaction to ensure atomicity
|
||||||
@@ -218,27 +188,80 @@ export function makeLibSignalRepository(
|
|||||||
},
|
},
|
||||||
|
|
||||||
async migrateSession(
|
async migrateSession(
|
||||||
fromJids: string[],
|
fromJid: string,
|
||||||
toJid: string
|
toJid: string
|
||||||
): Promise<{ migrated: number; skipped: number; total: number }> {
|
): Promise<{ migrated: number; skipped: number; total: number }> {
|
||||||
if (!fromJids.length || !toJid.includes('@lid')) return { migrated: 0, skipped: 0, total: 0 }
|
if (!fromJid || !toJid.includes('@lid')) return { migrated: 0, skipped: 0, total: 0 }
|
||||||
|
|
||||||
// Filter valid PN JIDs
|
// Only support PN to LID migration
|
||||||
const validJids = fromJids.filter(jid => jid.includes('@s.whatsapp.net'))
|
if (!fromJid.includes('@s.whatsapp.net')) {
|
||||||
if (!validJids.length) return { migrated: 0, skipped: 0, total: fromJids.length }
|
return { migrated: 0, skipped: 0, total: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
// Single optimized transaction for all migrations
|
const { user } = jidDecode(fromJid)!
|
||||||
|
|
||||||
|
logger.debug({ fromJid }, 'bulk device migration - loading all user devices')
|
||||||
|
|
||||||
|
// Get user's device list from storage
|
||||||
|
const { [user]: userDevices } = await parsedKeys.get('device-list', [user])
|
||||||
|
if (!userDevices) {
|
||||||
|
return { migrated: 0, skipped: 0, total: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const { device: fromDevice } = jidDecode(fromJid)!
|
||||||
|
const fromDeviceStr = fromDevice?.toString() || '0'
|
||||||
|
if (!userDevices.includes(fromDeviceStr)) {
|
||||||
|
userDevices.push(fromDeviceStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out cached devices before database fetch
|
||||||
|
const uncachedDevices = userDevices.filter(device => {
|
||||||
|
const deviceKey = `${user}.${device}`
|
||||||
|
return !migratedSessionCache.has(deviceKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bulk check session existence only for uncached devices
|
||||||
|
const deviceSessionKeys = uncachedDevices.map(device => `${user}.${device}`)
|
||||||
|
const existingSessions = await parsedKeys.get('session', deviceSessionKeys)
|
||||||
|
|
||||||
|
// Step 3: Convert existing sessions to JIDs (only migrate sessions that exist)
|
||||||
|
const deviceJids: string[] = []
|
||||||
|
for (const [sessionKey, sessionData] of Object.entries(existingSessions)) {
|
||||||
|
if (sessionData) {
|
||||||
|
// Session exists in storage
|
||||||
|
const deviceStr = sessionKey.split('.')[1]
|
||||||
|
if (!deviceStr) continue
|
||||||
|
const deviceNum = parseInt(deviceStr)
|
||||||
|
const jid = deviceNum === 0 ? `${user}@s.whatsapp.net` : `${user}:${deviceNum}@s.whatsapp.net`
|
||||||
|
deviceJids.push(jid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
fromJid,
|
||||||
|
totalDevices: userDevices.length,
|
||||||
|
devicesWithSessions: deviceJids.length,
|
||||||
|
devices: deviceJids
|
||||||
|
},
|
||||||
|
'bulk device migration complete - all user devices processed'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Single transaction for all migrations
|
||||||
return parsedKeys.transaction(
|
return parsedKeys.transaction(
|
||||||
async (): Promise<{ migrated: number; skipped: number; total: number }> => {
|
async (): Promise<{ migrated: number; skipped: number; total: number }> => {
|
||||||
// 1. Batch store all LID mappings
|
// Prepare migration operations with addressing metadata
|
||||||
const mappings = validJids.map(jid => ({
|
type MigrationOp = {
|
||||||
lid: transferDevice(jid, toJid),
|
fromJid: string
|
||||||
pn: jid
|
toJid: string
|
||||||
}))
|
pnUser: string
|
||||||
await lidMapping.storeLIDPNMappings(mappings)
|
lidUser: string
|
||||||
|
deviceId: number
|
||||||
|
fromAddr: libsignal.ProtocolAddress
|
||||||
|
toAddr: libsignal.ProtocolAddress
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Prepare migration operations
|
const migrationOps: MigrationOp[] = deviceJids.map(jid => {
|
||||||
const migrationOps = validJids.map(jid => {
|
|
||||||
const lidWithDevice = transferDevice(jid, toJid)
|
const lidWithDevice = transferDevice(jid, toJid)
|
||||||
const fromDecoded = jidDecode(jid)!
|
const fromDecoded = jidDecode(jid)!
|
||||||
const toDecoded = jidDecode(lidWithDevice)!
|
const toDecoded = jidDecode(lidWithDevice)!
|
||||||
@@ -254,44 +277,53 @@ export function makeLibSignalRepository(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. Batch check which LID sessions already exist
|
const totalOps = migrationOps.length
|
||||||
const lidAddrs = migrationOps.map(op => op.toAddr.toString())
|
let migratedCount = 0
|
||||||
const existingSessions = await auth.keys.get('session', lidAddrs)
|
|
||||||
|
|
||||||
// 4. Filter out sessions that already have LID sessions
|
// Bulk fetch PN sessions - already exist (verified during device discovery)
|
||||||
const opsToMigrate = migrationOps.filter(op => !existingSessions[op.toAddr.toString()])
|
const pnAddrStrings = Array.from(new Set(migrationOps.map(op => op.fromAddr.toString())))
|
||||||
const skippedCount = migrationOps.length - opsToMigrate.length
|
const pnSessions = await parsedKeys.get('session', pnAddrStrings)
|
||||||
|
|
||||||
if (!opsToMigrate.length) {
|
// Prepare bulk session updates (PN → LID migration + deletion)
|
||||||
return { migrated: 0, skipped: skippedCount, total: validJids.length }
|
const sessionUpdates: { [key: string]: Uint8Array | null } = {}
|
||||||
|
|
||||||
|
for (const op of migrationOps) {
|
||||||
|
const pnAddrStr = op.fromAddr.toString()
|
||||||
|
const lidAddrStr = op.toAddr.toString()
|
||||||
|
|
||||||
|
const pnSession = pnSessions[pnAddrStr]
|
||||||
|
if (pnSession) {
|
||||||
|
// Session exists (guaranteed from device discovery)
|
||||||
|
const fromSession = libsignal.SessionRecord.deserialize(pnSession)
|
||||||
|
if (fromSession.haveOpenSession()) {
|
||||||
|
// Queue for bulk update: copy to LID, delete from PN
|
||||||
|
sessionUpdates[lidAddrStr] = fromSession.serialize()
|
||||||
|
sessionUpdates[pnAddrStr] = null
|
||||||
|
|
||||||
|
migratedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Execute all migrations in parallel
|
// Single bulk session update for all migrations
|
||||||
await Promise.all(
|
if (Object.keys(sessionUpdates).length > 0) {
|
||||||
opsToMigrate.map(async op => {
|
await parsedKeys.set({ session: sessionUpdates })
|
||||||
const fromSession = await storage.loadSession(op.fromAddr.toString())
|
logger.debug({ migratedSessions: migratedCount }, 'bulk session migration complete')
|
||||||
|
|
||||||
if (fromSession?.haveOpenSession()) {
|
// Cache device-level migrations
|
||||||
// Copy session to LID address
|
for (const op of migrationOps) {
|
||||||
const sessionBytes = fromSession.serialize()
|
if (sessionUpdates[op.toAddr.toString()]) {
|
||||||
const copiedSession = libsignal.SessionRecord.deserialize(sessionBytes)
|
const deviceKey = `${op.pnUser}.${op.deviceId}`
|
||||||
await storage.storeSession(op.toAddr.toString(), copiedSession)
|
migratedSessionCache.set(deviceKey, true)
|
||||||
|
|
||||||
// Delete PN session
|
|
||||||
await auth.keys.set({ session: { [op.fromAddr.toString()]: null } })
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
return { migrated: opsToMigrate.length, skipped: skippedCount, total: validJids.length }
|
const skippedCount = totalOps - migratedCount
|
||||||
|
return { migrated: migratedCount, skipped: skippedCount, total: totalOps }
|
||||||
},
|
},
|
||||||
`migrate-${validJids.length}-sessions-${jidDecode(toJid)?.user}`
|
`migrate-${deviceJids.length}-sessions-${jidDecode(toJid)?.user}`
|
||||||
)
|
)
|
||||||
},
|
|
||||||
|
|
||||||
async encryptMessageWithWire({ encryptionJid, wireJid, data }) {
|
|
||||||
const result = await repository.encryptMessage({ jid: encryptionJid, data })
|
|
||||||
return { ...result, wireJid }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,32 +355,28 @@ function signalStorage(
|
|||||||
{ creds, keys }: SignalAuthState,
|
{ creds, keys }: SignalAuthState,
|
||||||
lidMapping: LIDMappingStore
|
lidMapping: LIDMappingStore
|
||||||
): SenderKeyStore & libsignal.SignalStorage {
|
): SenderKeyStore & libsignal.SignalStorage {
|
||||||
|
// Shared function to resolve PN signal address to LID if mapping exists
|
||||||
|
const resolveSignalAddress = async (id: string): Promise<string> => {
|
||||||
|
if (id.includes('.') && !id.includes('_1')) {
|
||||||
|
const parts = id.split('.')
|
||||||
|
const device = parts[1] || '0'
|
||||||
|
const pnJid = device === '0' ? `${parts[0]}@s.whatsapp.net` : `${parts[0]}:${device}@s.whatsapp.net`
|
||||||
|
|
||||||
|
const lidForPN = await lidMapping.getLIDForPN(pnJid)
|
||||||
|
if (lidForPN?.includes('@lid')) {
|
||||||
|
const lidAddr = jidToSignalProtocolAddress(lidForPN)
|
||||||
|
return lidAddr.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loadSession: async (id: string) => {
|
loadSession: async (id: string) => {
|
||||||
try {
|
try {
|
||||||
// LID SINGLE SOURCE OF TRUTH: Auto-redirect PN to LID if mapping exists
|
const wireJid = await resolveSignalAddress(id)
|
||||||
let actualId = id
|
const { [wireJid]: sess } = await keys.get('session', [wireJid])
|
||||||
if (id.includes('.') && !id.includes('_1')) {
|
|
||||||
// This is a PN signal address format (e.g., "1234567890.0")
|
|
||||||
// Convert back to JID to check for LID mapping
|
|
||||||
const parts = id.split('.')
|
|
||||||
const device = parts[1] || '0'
|
|
||||||
const pnJid = device === '0' ? `${parts[0]}@s.whatsapp.net` : `${parts[0]}:${device}@s.whatsapp.net`
|
|
||||||
|
|
||||||
const lidForPN = await lidMapping.getLIDForPN(pnJid)
|
|
||||||
if (lidForPN?.includes('@lid')) {
|
|
||||||
const lidAddr = jidToSignalProtocolAddress(lidForPN)
|
|
||||||
const lidId = lidAddr.toString()
|
|
||||||
|
|
||||||
// Check if LID session exists
|
|
||||||
const { [lidId]: lidSession } = await keys.get('session', [lidId])
|
|
||||||
if (lidSession) {
|
|
||||||
actualId = lidId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { [actualId]: sess } = await keys.get('session', [actualId])
|
|
||||||
|
|
||||||
if (sess) {
|
if (sess) {
|
||||||
return libsignal.SessionRecord.deserialize(sess)
|
return libsignal.SessionRecord.deserialize(sess)
|
||||||
@@ -360,7 +388,8 @@ function signalStorage(
|
|||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
storeSession: async (id: string, session: libsignal.SessionRecord) => {
|
storeSession: async (id: string, session: libsignal.SessionRecord) => {
|
||||||
await keys.set({ session: { [id]: session.serialize() } })
|
const wireJid = await resolveSignalAddress(id)
|
||||||
|
await keys.set({ session: { [wireJid]: session.serialize() } })
|
||||||
},
|
},
|
||||||
isTrustedIdentity: () => {
|
isTrustedIdentity: () => {
|
||||||
return true
|
return true
|
||||||
|
|||||||
+19
-47
@@ -1,6 +1,6 @@
|
|||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from 'lru-cache'
|
||||||
import type { SignalKeyStoreWithTransaction } from '../Types'
|
import type { SignalKeyStoreWithTransaction } from '../Types'
|
||||||
import logger from '../Utils/logger'
|
import type { ILogger } from '../Utils/logger'
|
||||||
import { isLidUser, isPnUser, jidDecode } from '../WABinary'
|
import { isLidUser, isPnUser, jidDecode } from '../WABinary'
|
||||||
|
|
||||||
export class LIDMappingStore {
|
export class LIDMappingStore {
|
||||||
@@ -10,28 +10,11 @@ export class LIDMappingStore {
|
|||||||
updateAgeOnGet: true
|
updateAgeOnGet: true
|
||||||
})
|
})
|
||||||
private readonly keys: SignalKeyStoreWithTransaction
|
private readonly keys: SignalKeyStoreWithTransaction
|
||||||
private onWhatsAppFunc?: (...jids: string[]) => Promise<
|
private readonly logger: ILogger
|
||||||
| {
|
|
||||||
jid: string
|
|
||||||
exists: boolean
|
|
||||||
lid: string
|
|
||||||
}[]
|
|
||||||
| undefined
|
|
||||||
>
|
|
||||||
|
|
||||||
constructor(
|
constructor(keys: SignalKeyStoreWithTransaction, logger: ILogger) {
|
||||||
keys: SignalKeyStoreWithTransaction,
|
|
||||||
onWhatsAppFunc?: (...jids: string[]) => Promise<
|
|
||||||
| {
|
|
||||||
jid: string
|
|
||||||
exists: boolean
|
|
||||||
lid: string
|
|
||||||
}[]
|
|
||||||
| undefined
|
|
||||||
>
|
|
||||||
) {
|
|
||||||
this.keys = keys
|
this.keys = keys
|
||||||
this.onWhatsAppFunc = onWhatsAppFunc // needed to get LID from PN if not found
|
this.logger = logger
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,7 +25,7 @@ export class LIDMappingStore {
|
|||||||
const pairMap: { [_: string]: string } = {}
|
const pairMap: { [_: string]: string } = {}
|
||||||
for (const { lid, pn } of pairs) {
|
for (const { lid, pn } of pairs) {
|
||||||
if (!((isLidUser(lid) && isPnUser(pn)) || (isPnUser(lid) && isLidUser(pn)))) {
|
if (!((isLidUser(lid) && isPnUser(pn)) || (isPnUser(lid) && isLidUser(pn)))) {
|
||||||
logger.warn(`Invalid LID-PN mapping: ${lid}, ${pn}`)
|
this.logger.warn(`Invalid LID-PN mapping: ${lid}, ${pn}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +37,9 @@ export class LIDMappingStore {
|
|||||||
const pnUser = pnDecoded.user
|
const pnUser = pnDecoded.user
|
||||||
const lidUser = lidDecoded.user
|
const lidUser = lidDecoded.user
|
||||||
|
|
||||||
// Check if mapping already exists (cache first, then database)
|
|
||||||
let existingLidUser = this.mappingCache.get(`pn:${pnUser}`)
|
let existingLidUser = this.mappingCache.get(`pn:${pnUser}`)
|
||||||
if (!existingLidUser) {
|
if (!existingLidUser) {
|
||||||
// Cache miss - check database
|
this.logger.trace(`Cache miss for PN user ${pnUser}; checking database`)
|
||||||
const stored = await this.keys.get('lid-mapping', [pnUser])
|
const stored = await this.keys.get('lid-mapping', [pnUser])
|
||||||
existingLidUser = stored[pnUser]
|
existingLidUser = stored[pnUser]
|
||||||
if (existingLidUser) {
|
if (existingLidUser) {
|
||||||
@@ -68,25 +50,24 @@ export class LIDMappingStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existingLidUser === lidUser) {
|
if (existingLidUser === lidUser) {
|
||||||
logger.debug({ pnUser, lidUser }, 'LID mapping already exists, skipping')
|
this.logger.debug({ pnUser, lidUser }, 'LID mapping already exists, skipping')
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
pairMap[pnUser] = lidUser
|
pairMap[pnUser] = lidUser
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.trace({ pairMap }, `Storing ${Object.keys(pairMap).length} pn mappings`)
|
this.logger.trace({ pairMap }, `Storing ${Object.keys(pairMap).length} pn mappings`)
|
||||||
|
|
||||||
await this.keys.transaction(async () => {
|
await this.keys.transaction(async () => {
|
||||||
for (const [pnUser, lidUser] of Object.entries(pairMap)) {
|
for (const [pnUser, lidUser] of Object.entries(pairMap)) {
|
||||||
await this.keys.set({
|
await this.keys.set({
|
||||||
'lid-mapping': {
|
'lid-mapping': {
|
||||||
[pnUser]: lidUser, // "554396160286" -> "102765716062358"
|
[pnUser]: lidUser,
|
||||||
[`${lidUser}_reverse`]: pnUser // "102765716062358_reverse" -> "554396160286"
|
[`${lidUser}_reverse`]: pnUser
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update cache with both directions
|
|
||||||
this.mappingCache.set(`pn:${pnUser}`, lidUser)
|
this.mappingCache.set(`pn:${pnUser}`, lidUser)
|
||||||
this.mappingCache.set(`lid:${lidUser}`, pnUser)
|
this.mappingCache.set(`lid:${lidUser}`, pnUser)
|
||||||
}
|
}
|
||||||
@@ -112,26 +93,17 @@ export class LIDMappingStore {
|
|||||||
lidUser = stored[pnUser]
|
lidUser = stored[pnUser]
|
||||||
|
|
||||||
if (lidUser) {
|
if (lidUser) {
|
||||||
// Cache the database result
|
|
||||||
this.mappingCache.set(`pn:${pnUser}`, lidUser)
|
this.mappingCache.set(`pn:${pnUser}`, lidUser)
|
||||||
|
this.mappingCache.set(`lid:${lidUser}`, pnUser)
|
||||||
} else {
|
} else {
|
||||||
// Not in database - try USync
|
this.logger.trace(`No LID mapping found for PN user ${pnUser}`)
|
||||||
logger.trace(`No LID mapping found for PN user ${pnUser}; getting from USync`)
|
return null
|
||||||
const { exists, lid } = (await this.onWhatsAppFunc?.(pn))?.[0]! // this function already adds LIDs to mapping
|
|
||||||
if (exists && lid) {
|
|
||||||
lidUser = jidDecode(lid)?.user
|
|
||||||
if (lidUser) {
|
|
||||||
// Cache the USync result
|
|
||||||
this.mappingCache.set(`pn:${pnUser}`, lidUser)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof lidUser !== 'string' || !lidUser) {
|
lidUser = lidUser.toString()
|
||||||
logger.warn(`Invalid or empty LID user for PN ${pn}: lidUser = "${lidUser}"`)
|
if (!lidUser) {
|
||||||
|
this.logger.warn(`Invalid or empty LID user for PN ${pn}: lidUser = "${lidUser}"`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +111,7 @@ export class LIDMappingStore {
|
|||||||
const pnDevice = decoded.device !== undefined ? decoded.device : 0
|
const pnDevice = decoded.device !== undefined ? decoded.device : 0
|
||||||
const deviceSpecificLid = `${lidUser}:${pnDevice}@lid`
|
const deviceSpecificLid = `${lidUser}:${pnDevice}@lid`
|
||||||
|
|
||||||
logger.trace(`getLIDForPN: ${pn} → ${deviceSpecificLid} (user mapping with device ${pnDevice})`)
|
this.logger.trace(`getLIDForPN: ${pn} → ${deviceSpecificLid} (user mapping with device ${pnDevice})`)
|
||||||
return deviceSpecificLid
|
return deviceSpecificLid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +134,7 @@ export class LIDMappingStore {
|
|||||||
pnUser = stored[`${lidUser}_reverse`]
|
pnUser = stored[`${lidUser}_reverse`]
|
||||||
|
|
||||||
if (!pnUser || typeof pnUser !== 'string') {
|
if (!pnUser || typeof pnUser !== 'string') {
|
||||||
logger.trace(`No reverse mapping found for LID user: ${lidUser}`)
|
this.logger.trace(`No reverse mapping found for LID user: ${lidUser}`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +145,7 @@ export class LIDMappingStore {
|
|||||||
const lidDevice = decoded.device !== undefined ? decoded.device : 0
|
const lidDevice = decoded.device !== undefined ? decoded.device : 0
|
||||||
const pnJid = `${pnUser}:${lidDevice}@s.whatsapp.net`
|
const pnJid = `${pnUser}:${lidDevice}@s.whatsapp.net`
|
||||||
|
|
||||||
logger.trace(`Found reverse mapping: ${lid} → ${pnJid}`)
|
this.logger.trace(`Found reverse mapping: ${lid} → ${pnJid}`)
|
||||||
return pnJid
|
return pnJid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -939,7 +939,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await assertSessions([participant], shouldRecreateSession)
|
await assertSessions([participant])
|
||||||
|
|
||||||
if (isJidGroup(remoteJid)) {
|
if (isJidGroup(remoteJid)) {
|
||||||
await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } })
|
await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } })
|
||||||
@@ -1130,23 +1130,23 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
pn = jidNormalizedUser(node.attrs.sender_pn)
|
pn = jidNormalizedUser(node.attrs.sender_pn)
|
||||||
ev.emit('lid-mapping.update', { lid, pn })
|
ev.emit('lid-mapping.update', { lid, pn })
|
||||||
await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }])
|
await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }])
|
||||||
|
await signalRepository.migrateSession(pn, lid)
|
||||||
}
|
}
|
||||||
|
|
||||||
const alt = msg.key.participantAlt || msg.key.remoteJidAlt
|
const alt = msg.key.participantAlt || msg.key.remoteJidAlt
|
||||||
// store new mappings we didn't have before
|
// store new mappings we didn't have before
|
||||||
if (!!alt) {
|
if (!!alt) {
|
||||||
const altServer = jidDecode(alt)?.server
|
const altServer = jidDecode(alt)?.server
|
||||||
|
const primaryJid = msg.key.participant || msg.key.remoteJid!
|
||||||
if (altServer === 'lid') {
|
if (altServer === 'lid') {
|
||||||
if (typeof (await signalRepository.lidMapping.getPNForLID(alt)) === 'string') {
|
if (typeof (await signalRepository.lidMapping.getPNForLID(alt)) === 'string') {
|
||||||
await signalRepository.lidMapping.storeLIDPNMappings([
|
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }])
|
||||||
{ lid: alt, pn: msg.key.participant || msg.key.remoteJid! }
|
await signalRepository.migrateSession(primaryJid, alt)
|
||||||
])
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (typeof (await signalRepository.lidMapping.getLIDForPN(alt)) === 'string') {
|
if (typeof (await signalRepository.lidMapping.getLIDForPN(alt)) === 'string') {
|
||||||
await signalRepository.lidMapping.storeLIDPNMappings([
|
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }])
|
||||||
{ lid: msg.key.participant || msg.key.remoteJid!, pn: alt }
|
await signalRepository.migrateSession(alt, primaryJid)
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-336
@@ -84,6 +84,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
useClones: false
|
useClones: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const peerSessionsCache = new NodeCache<boolean>({
|
||||||
|
stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES,
|
||||||
|
useClones: false
|
||||||
|
})
|
||||||
|
|
||||||
// Initialize message retry manager if enabled
|
// Initialize message retry manager if enabled
|
||||||
const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
|
const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
|
||||||
|
|
||||||
@@ -197,39 +202,35 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
/** Device info with wire JID format for envelope addressing */
|
/** Device info with wire JID format for envelope addressing */
|
||||||
type DeviceWithWireJid = JidWithDevice & {
|
type DeviceWithWireJid = JidWithDevice & {
|
||||||
wireJid: string // The exact JID format that should be used in wire protocol (envelope addressing)
|
wireJid: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const resolveSessionJids = async (jids: string[]): Promise<Map<string, string>> => {
|
||||||
* Deduplicate JIDs when both LID and PN versions exist for same user
|
const uniquePnJids = Array.from(new Set(jids.filter(isPnUser)))
|
||||||
* Prefers LID over PN to maintain single encryption layer
|
if (!uniquePnJids.length) {
|
||||||
*/
|
return new Map()
|
||||||
const deduplicateLidPnJids = (jids: string[]): string[] => {
|
|
||||||
const lidUsers = new Set<string>()
|
|
||||||
const filteredJids: string[] = []
|
|
||||||
|
|
||||||
// Collect all LID users
|
|
||||||
for (const jid of jids) {
|
|
||||||
if (jid.includes('@lid')) {
|
|
||||||
const user = jidDecode(jid)?.user
|
|
||||||
if (user) lidUsers.add(user)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out PN versions when LID exists
|
const lookups = await Promise.all(
|
||||||
for (const jid of jids) {
|
uniquePnJids.map(async pnJid => {
|
||||||
if (jid.includes('@s.whatsapp.net')) {
|
try {
|
||||||
const user = jidDecode(jid)?.user
|
const resolved = await signalRepository.lidMapping.getLIDForPN(pnJid)
|
||||||
if (user && lidUsers.has(user)) {
|
return resolved ? ([pnJid, resolved] as const) : [pnJid, pnJid]
|
||||||
logger.debug({ jid }, 'Skipping PN - LID version exists')
|
} catch (error) {
|
||||||
continue
|
logger.warn({ pnJid, error }, 'Failed to resolve LID mapping for PN JID')
|
||||||
|
return [pnJid, pnJid]
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
)
|
||||||
|
|
||||||
filteredJids.push(jid)
|
const sessionMap = new Map<string, string>()
|
||||||
|
for (const entry of lookups) {
|
||||||
|
if (entry) {
|
||||||
|
sessionMap.set(entry[0], entry[1])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredJids
|
return sessionMap
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch all the devices we've to send a message to */
|
/** Fetch all the devices we've to send a message to */
|
||||||
@@ -245,7 +246,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toFetch: string[] = []
|
const toFetch: string[] = []
|
||||||
jids = deduplicateLidPnJids(Array.from(new Set(jids)))
|
|
||||||
const jidsWithUser = jids
|
const jidsWithUser = jids
|
||||||
.map(jid => {
|
.map(jid => {
|
||||||
const decoded = jidDecode(jid)
|
const decoded = jidDecode(jid)
|
||||||
@@ -360,170 +361,57 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
|
if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userDeviceUpdates: { [userId: string]: string[] } = {}
|
||||||
|
for (const [userId, devices] of Object.entries(deviceMap)) {
|
||||||
|
if (devices && devices.length > 0) {
|
||||||
|
userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(userDeviceUpdates).length > 0) {
|
||||||
|
try {
|
||||||
|
await authState.keys.set({ 'device-list': userDeviceUpdates })
|
||||||
|
logger.debug(
|
||||||
|
{ userCount: Object.keys(userDeviceUpdates).length },
|
||||||
|
'stored user device lists for bulk migration'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn({ error }, 'failed to store user device lists')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return deviceResults
|
return deviceResults
|
||||||
}
|
}
|
||||||
|
|
||||||
const assertSessions = async (jids: string[], force: boolean) => {
|
const assertSessions = async (jids: string[]) => {
|
||||||
let didFetchNewSession = false
|
let didFetchNewSession = false
|
||||||
|
const uniqueJids = [...new Set(jids)] // Deduplicate JIDs
|
||||||
const jidsRequiringFetch: string[] = []
|
const jidsRequiringFetch: string[] = []
|
||||||
|
|
||||||
// Apply same deduplication as in getUSyncDevices
|
// Check peerSessionsCache and authState.keys
|
||||||
jids = deduplicateLidPnJids(jids)
|
for (const jid of uniqueJids) {
|
||||||
|
const signalId = signalRepository.jidToSignalProtocolAddress(jid)
|
||||||
if (force) {
|
const cachedSession = peerSessionsCache.get(signalId)
|
||||||
// Check which sessions are missing (with LID migration check)
|
if (cachedSession !== undefined) {
|
||||||
const addrs = jids.map(jid => signalRepository.jidToSignalProtocolAddress(jid))
|
if (cachedSession) {
|
||||||
const sessions = await authState.keys.get('session', addrs)
|
continue // Session exists in cache
|
||||||
|
}
|
||||||
// Simplified: Check session existence directly
|
} else {
|
||||||
const checkJidSession = (jid: string) => {
|
const sessions = await authState.keys.get('session', [signalId])
|
||||||
const signalId = signalRepository.jidToSignalProtocolAddress(jid)
|
|
||||||
const hasSession = !!sessions[signalId]
|
const hasSession = !!sessions[signalId]
|
||||||
|
peerSessionsCache.set(signalId, hasSession)
|
||||||
// Add to fetch list if no session exists
|
if (hasSession) {
|
||||||
// Session type selection (LID vs PN) is handled in encryptMessage
|
continue
|
||||||
if (!hasSession) {
|
|
||||||
if (jid.includes('@lid')) {
|
|
||||||
logger.debug({ jid }, 'No LID session found, will create new LID session')
|
|
||||||
}
|
|
||||||
|
|
||||||
jidsRequiringFetch.push(jid)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process all JIDs
|
jidsRequiringFetch.push(jid)
|
||||||
for (const jid of jids) {
|
|
||||||
checkJidSession(jid)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const addrs = jids.map(jid => signalRepository.jidToSignalProtocolAddress(jid))
|
|
||||||
const sessions = await authState.keys.get('session', addrs)
|
|
||||||
|
|
||||||
// Group JIDs by user for bulk migration
|
|
||||||
const userGroups = new Map<string, string[]>()
|
|
||||||
for (const jid of jids) {
|
|
||||||
const user = jidNormalizedUser(jid)
|
|
||||||
if (!userGroups.has(user)) {
|
|
||||||
userGroups.set(user, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
userGroups.get(user)!.push(jid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to check LID mapping for a user
|
|
||||||
const checkUserLidMapping = async (user: string, userJids: string[]) => {
|
|
||||||
if (!userJids.some(jid => jid.includes('@s.whatsapp.net'))) {
|
|
||||||
return { shouldMigrate: false, lidForPN: undefined }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Convert user to proper PN JID format for getLIDForPN
|
|
||||||
const pnJid = `${user}@s.whatsapp.net`
|
|
||||||
const mapping = await signalRepository.lidMapping.getLIDForPN(pnJid)
|
|
||||||
if (mapping?.includes('@lid')) {
|
|
||||||
logger.debug(
|
|
||||||
{ user, lidForPN: mapping, deviceCount: userJids.length },
|
|
||||||
'User has LID mapping - preparing bulk migration'
|
|
||||||
)
|
|
||||||
return { shouldMigrate: true, lidForPN: mapping }
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug({ user, error }, 'Failed to check LID mapping for user')
|
|
||||||
}
|
|
||||||
|
|
||||||
return { shouldMigrate: false, lidForPN: undefined }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process each user group for potential bulk LID migration
|
|
||||||
for (const [user, userJids] of userGroups) {
|
|
||||||
const mappingResult = await checkUserLidMapping(user, userJids)
|
|
||||||
const shouldMigrateUser = mappingResult.shouldMigrate
|
|
||||||
const lidForPN = mappingResult.lidForPN
|
|
||||||
|
|
||||||
// Migrate all devices for this user if LID mapping exists
|
|
||||||
if (shouldMigrateUser && lidForPN) {
|
|
||||||
// Bulk migrate all user devices in single transaction
|
|
||||||
const migrationResult = await signalRepository.migrateSession(userJids, lidForPN)
|
|
||||||
|
|
||||||
if (migrationResult.migrated > 0) {
|
|
||||||
logger.info(
|
|
||||||
{
|
|
||||||
user,
|
|
||||||
lidMapping: lidForPN,
|
|
||||||
migrated: migrationResult.migrated,
|
|
||||||
skipped: migrationResult.skipped,
|
|
||||||
total: migrationResult.total
|
|
||||||
},
|
|
||||||
'Completed bulk migration for user devices'
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
logger.debug(
|
|
||||||
{
|
|
||||||
user,
|
|
||||||
lidMapping: lidForPN,
|
|
||||||
skipped: migrationResult.skipped,
|
|
||||||
total: migrationResult.total
|
|
||||||
},
|
|
||||||
'All user device sessions already migrated'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct bulk session check with LID single source of truth
|
|
||||||
const addMissingSessionsToFetchList = (jid: string) => {
|
|
||||||
const signalId = signalRepository.jidToSignalProtocolAddress(jid)
|
|
||||||
if (sessions[signalId]) return
|
|
||||||
|
|
||||||
// Determine correct JID to fetch (LID if mapping exists, otherwise original)
|
|
||||||
if (jid.includes('@s.whatsapp.net') && shouldMigrateUser && lidForPN) {
|
|
||||||
const decoded = jidDecode(jid)!
|
|
||||||
const lidDeviceJid =
|
|
||||||
decoded.device !== undefined ? `${jidDecode(lidForPN)!.user}:${decoded.device}@lid` : lidForPN
|
|
||||||
jidsRequiringFetch.push(lidDeviceJid)
|
|
||||||
logger.debug({ pnJid: jid, lidJid: lidDeviceJid }, 'Adding LID JID to fetch list (conversion)')
|
|
||||||
} else {
|
|
||||||
jidsRequiringFetch.push(jid)
|
|
||||||
logger.debug({ jid }, 'Adding JID to fetch list')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
userJids.forEach(addMissingSessionsToFetchList)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jidsRequiringFetch.length) {
|
if (jidsRequiringFetch.length) {
|
||||||
logger.debug({ jidsRequiringFetch }, 'fetching sessions')
|
logger.debug({ jidsRequiringFetch }, 'fetching sessions')
|
||||||
|
|
||||||
// DEBUG: Check if there are PN versions of LID users being fetched
|
|
||||||
const lidUsersBeingFetched = new Set<string>()
|
|
||||||
const pnUsersBeingFetched = new Set<string>()
|
|
||||||
|
|
||||||
for (const jid of jidsRequiringFetch) {
|
|
||||||
const user = jidDecode(jid)?.user
|
|
||||||
if (user) {
|
|
||||||
if (jid.includes('@lid')) {
|
|
||||||
lidUsersBeingFetched.add(user)
|
|
||||||
} else if (jid.includes('@s.whatsapp.net')) {
|
|
||||||
pnUsersBeingFetched.add(user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find overlaps
|
|
||||||
const overlapping = Array.from(pnUsersBeingFetched).filter(user => lidUsersBeingFetched.has(user))
|
|
||||||
if (overlapping.length > 0) {
|
|
||||||
logger.warn(
|
|
||||||
{
|
|
||||||
overlapping,
|
|
||||||
lidUsersBeingFetched: Array.from(lidUsersBeingFetched),
|
|
||||||
pnUsersBeingFetched: Array.from(pnUsersBeingFetched)
|
|
||||||
},
|
|
||||||
'Fetching both LID and PN sessions for same users'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await query({
|
const result = await query({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -543,8 +431,13 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
await parseAndInjectE2ESessions(result, signalRepository)
|
await parseAndInjectE2ESessions(result, signalRepository)
|
||||||
|
|
||||||
didFetchNewSession = true
|
didFetchNewSession = true
|
||||||
|
|
||||||
|
// Cache fetched sessions
|
||||||
|
for (const jid of jidsRequiringFetch) {
|
||||||
|
const signalId = signalRepository.jidToSignalProtocolAddress(jid)
|
||||||
|
peerSessionsCache.set(signalId, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return didFetchNewSession
|
return didFetchNewSession
|
||||||
@@ -578,133 +471,58 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
return msgId
|
return msgId
|
||||||
}
|
}
|
||||||
|
|
||||||
const createParticipantNodes = async (
|
const createParticipantNodesWithSessionMap = async (
|
||||||
jids: string[],
|
recipientWireJids: string[],
|
||||||
|
sessionMap: Map<string, string>,
|
||||||
message: proto.IMessage,
|
message: proto.IMessage,
|
||||||
extraAttrs?: BinaryNode['attrs'],
|
extraAttrs?: BinaryNode['attrs'],
|
||||||
dsmMessage?: proto.IMessage
|
dsmMessage?: proto.IMessage
|
||||||
) => {
|
) => {
|
||||||
let patched = await patchMessageBeforeSending(message, jids)
|
if (!recipientWireJids.length) {
|
||||||
if (!Array.isArray(patched)) {
|
return { nodes: [] as BinaryNode[], shouldIncludeDeviceIdentity: false }
|
||||||
patched = jids ? jids.map(jid => ({ recipientJid: jid, ...patched })) : [patched]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let shouldIncludeDeviceIdentity = false
|
const patched = await patchMessageBeforeSending(message, recipientWireJids)
|
||||||
|
const patchedMessages = Array.isArray(patched)
|
||||||
|
? patched
|
||||||
|
: recipientWireJids.map(jid => ({ recipientJid: jid, message: patched }))
|
||||||
|
|
||||||
|
let shouldIncludeDeviceIdentity = false
|
||||||
const meId = authState.creds.me!.id
|
const meId = authState.creds.me!.id
|
||||||
const meLid = authState.creds.me?.lid
|
const meLid = authState.creds.me?.lid
|
||||||
const meLidUser = meLid ? jidDecode(meLid)?.user : null
|
const meLidUser = meLid ? jidDecode(meLid)?.user : null
|
||||||
|
|
||||||
const devicesByUser = new Map<string, Array<{ recipientJid: string; patchedMessage: proto.IMessage }>>()
|
const encryptionPromises = (patchedMessages as any).map(
|
||||||
|
async ({ recipientJid: wireJid, message: patchedMessage }: any) => {
|
||||||
for (const patchedMessageWithJid of patched) {
|
if (!wireJid) return null
|
||||||
const { recipientJid: wireJid, ...patchedMessage } = patchedMessageWithJid
|
wireJid = sessionMap.get(wireJid) ?? wireJid
|
||||||
if (!wireJid) continue
|
let msgToEncrypt = patchedMessage
|
||||||
|
if (dsmMessage) {
|
||||||
// Extract user from JID for grouping
|
const { user: targetUser } = jidDecode(wireJid)!
|
||||||
const decoded = jidDecode(wireJid)
|
const { user: ownPnUser } = jidDecode(meId)!
|
||||||
const user = decoded?.user
|
const ownLidUser = meLidUser
|
||||||
|
const isOwnUser = targetUser === ownPnUser || (ownLidUser && targetUser === ownLidUser)
|
||||||
if (!user) continue
|
const isExactSenderDevice = wireJid === meId || (meLid && wireJid === meLid)
|
||||||
|
if (isOwnUser && !isExactSenderDevice) {
|
||||||
if (!devicesByUser.has(user)) {
|
msgToEncrypt = dsmMessage
|
||||||
devicesByUser.set(user, [])
|
logger.debug({ wireJid, targetUser }, 'Using DSM for own device')
|
||||||
}
|
|
||||||
|
|
||||||
devicesByUser.get(user)!.push({ recipientJid: wireJid, patchedMessage })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process each user's devices sequentially, but different users in parallel
|
|
||||||
const userEncryptionPromises = Array.from(devicesByUser.entries()).map(([user, userDevices]) =>
|
|
||||||
encryptionMutex.mutex(user, async () => {
|
|
||||||
logger.debug({ user, deviceCount: userDevices.length }, 'Acquiring encryption lock for user devices')
|
|
||||||
|
|
||||||
const userNodes: BinaryNode[] = []
|
|
||||||
|
|
||||||
// Helper to get encryption JID with LID migration
|
|
||||||
const getEncryptionJid = async (wireJid: string) => {
|
|
||||||
if (!wireJid.includes('@s.whatsapp.net')) return wireJid
|
|
||||||
|
|
||||||
try {
|
|
||||||
const lidForPN = await signalRepository.lidMapping.getLIDForPN(wireJid)
|
|
||||||
|
|
||||||
if (!lidForPN?.includes('@lid')) return wireJid
|
|
||||||
|
|
||||||
// Preserve device ID from original wire JID
|
|
||||||
const wireDecoded = jidDecode(wireJid)
|
|
||||||
const deviceId = wireDecoded?.device || 0
|
|
||||||
const lidDecoded = jidDecode(lidForPN)
|
|
||||||
const lidWithDevice = jidEncode(lidDecoded?.user!, 'lid', deviceId)
|
|
||||||
|
|
||||||
// Migrate session to LID for unified encryption layer
|
|
||||||
try {
|
|
||||||
const migrationResult = await signalRepository.migrateSession([wireJid], lidWithDevice)
|
|
||||||
const recipientUser = jidNormalizedUser(wireJid)
|
|
||||||
const ownPnUser = jidNormalizedUser(meId)
|
|
||||||
const isOwnDevice = recipientUser === ownPnUser
|
|
||||||
logger.info({ wireJid, lidWithDevice, isOwnDevice }, 'Migrated to LID encryption')
|
|
||||||
|
|
||||||
// Delete PN session after successful migration
|
|
||||||
try {
|
|
||||||
if (migrationResult.migrated) {
|
|
||||||
await signalRepository.deleteSession([wireJid])
|
|
||||||
logger.debug({ deletedPNSession: wireJid }, 'Deleted PN session')
|
|
||||||
}
|
|
||||||
} catch (deleteError) {
|
|
||||||
logger.warn({ wireJid, error: deleteError }, 'Failed to delete PN session')
|
|
||||||
}
|
|
||||||
|
|
||||||
return lidWithDevice
|
|
||||||
} catch (migrationError) {
|
|
||||||
logger.warn({ wireJid, error: migrationError }, 'Failed to migrate session')
|
|
||||||
return wireJid
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug({ wireJid, error }, 'Failed to check LID mapping')
|
|
||||||
return wireJid
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt to this user's devices sequentially to prevent session corruption
|
const bytes = encodeWAMessage(msgToEncrypt)
|
||||||
for (const { recipientJid: wireJid, patchedMessage } of userDevices) {
|
const mutexKey = wireJid
|
||||||
// DSM logic: Use DSM for own other devices (following whatsmeow implementation)
|
const node = await encryptionMutex.mutex(mutexKey, async () => {
|
||||||
let messageToEncrypt = patchedMessage
|
|
||||||
if (dsmMessage) {
|
|
||||||
const { user: targetUser } = jidDecode(wireJid)!
|
|
||||||
const { user: ownPnUser } = jidDecode(meId)!
|
|
||||||
const ownLidUser = meLidUser
|
|
||||||
|
|
||||||
// Check if this is our device (same user, different device)
|
|
||||||
const isOwnUser = targetUser === ownPnUser || (ownLidUser && targetUser === ownLidUser)
|
|
||||||
|
|
||||||
// Exclude exact sender device (whatsmeow: if jid == ownJID || jid == ownLID { continue })
|
|
||||||
const isExactSenderDevice =
|
|
||||||
wireJid === meId || (authState.creds.me?.lid && wireJid === authState.creds.me.lid)
|
|
||||||
|
|
||||||
if (isOwnUser && !isExactSenderDevice) {
|
|
||||||
messageToEncrypt = dsmMessage
|
|
||||||
logger.debug({ wireJid, targetUser }, 'Using DSM for own device')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bytes = encodeWAMessage(messageToEncrypt)
|
|
||||||
|
|
||||||
// Get encryption JID with LID migration
|
|
||||||
const encryptionJid = await getEncryptionJid(wireJid)
|
|
||||||
|
|
||||||
// ENCRYPT: Use the determined encryption identity (prefers migrated LID)
|
|
||||||
const { type, ciphertext } = await signalRepository.encryptMessage({
|
const { type, ciphertext } = await signalRepository.encryptMessage({
|
||||||
jid: encryptionJid, // Unified encryption layer (LID when available)
|
jid: wireJid,
|
||||||
data: bytes
|
data: bytes
|
||||||
})
|
})
|
||||||
|
|
||||||
if (type === 'pkmsg') {
|
if (type === 'pkmsg') {
|
||||||
shouldIncludeDeviceIdentity = true
|
shouldIncludeDeviceIdentity = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const node: BinaryNode = {
|
return {
|
||||||
tag: 'to',
|
tag: 'to',
|
||||||
attrs: { jid: wireJid }, // Always use original wire identity in envelope
|
attrs: { jid: wireJid },
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'enc',
|
tag: 'enc',
|
||||||
@@ -717,20 +535,25 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
userNodes.push(node)
|
})
|
||||||
}
|
return node
|
||||||
|
}
|
||||||
logger.debug({ user, nodesCreated: userNodes.length }, 'Releasing encryption lock for user devices')
|
|
||||||
return userNodes
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Wait for all users to complete (users are processed in parallel)
|
const nodes = (await Promise.all(encryptionPromises)).filter(node => node !== null) as BinaryNode[]
|
||||||
const userNodesArrays = await Promise.all(userEncryptionPromises)
|
|
||||||
const nodes = userNodesArrays.flat()
|
|
||||||
return { nodes, shouldIncludeDeviceIdentity }
|
return { nodes, shouldIncludeDeviceIdentity }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createParticipantNodes = async (
|
||||||
|
recipientWireJids: string[],
|
||||||
|
message: proto.IMessage,
|
||||||
|
extraAttrs?: BinaryNode['attrs'],
|
||||||
|
dsmMessage?: proto.IMessage
|
||||||
|
) => {
|
||||||
|
const sessionMap = await resolveSessionJids(recipientWireJids)
|
||||||
|
return createParticipantNodesWithSessionMap(recipientWireJids, sessionMap, message, extraAttrs, dsmMessage)
|
||||||
|
}
|
||||||
|
|
||||||
const relayMessage = async (
|
const relayMessage = async (
|
||||||
jid: string,
|
jid: string,
|
||||||
message: proto.IMessage,
|
message: proto.IMessage,
|
||||||
@@ -746,19 +569,14 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
) => {
|
) => {
|
||||||
const meId = authState.creds.me!.id
|
const meId = authState.creds.me!.id
|
||||||
const meLid = authState.creds.me?.lid
|
const meLid = authState.creds.me?.lid
|
||||||
|
|
||||||
// ADDRESSING CONSISTENCY: Keep envelope addressing as user provided, handle LID migration in encryption
|
|
||||||
|
|
||||||
let shouldIncludeDeviceIdentity = false
|
let shouldIncludeDeviceIdentity = false
|
||||||
|
const statusJid = 'status@broadcast'
|
||||||
|
|
||||||
const { user, server } = jidDecode(jid)!
|
const { user, server } = jidDecode(jid)!
|
||||||
const statusJid = 'status@broadcast'
|
|
||||||
const isGroup = server === 'g.us'
|
const isGroup = server === 'g.us'
|
||||||
const isStatus = jid === statusJid
|
const isStatus = jid === statusJid
|
||||||
const isLid = server === 'lid'
|
const isLid = server === 'lid'
|
||||||
const isNewsletter = server === 'newsletter'
|
const isNewsletter = server === 'newsletter'
|
||||||
|
|
||||||
// Keep user's original JID choice for envelope addressing
|
|
||||||
const finalJid = jid
|
const finalJid = jid
|
||||||
|
|
||||||
// ADDRESSING CONSISTENCY: Match own identity to conversation context
|
// ADDRESSING CONSISTENCY: Match own identity to conversation context
|
||||||
@@ -777,7 +595,6 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
const participants: BinaryNode[] = []
|
const participants: BinaryNode[] = []
|
||||||
const destinationJid = !isStatus ? finalJid : statusJid
|
const destinationJid = !isStatus ? finalJid : statusJid
|
||||||
|
|
||||||
const binaryNodeContent: BinaryNode[] = []
|
const binaryNodeContent: BinaryNode[] = []
|
||||||
const devices: DeviceWithWireJid[] = []
|
const devices: DeviceWithWireJid[] = []
|
||||||
|
|
||||||
@@ -792,18 +609,15 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
const extraAttrs: BinaryNodeAttributes = {}
|
const extraAttrs: BinaryNodeAttributes = {}
|
||||||
|
|
||||||
if (participant) {
|
if (participant) {
|
||||||
// when the retry request is not for a group
|
|
||||||
// only send to the specific device that asked for a retry
|
|
||||||
// otherwise the message is sent out to every device that should be a recipient
|
|
||||||
if (!isGroup && !isStatus) {
|
if (!isGroup && !isStatus) {
|
||||||
additionalAttributes = { ...additionalAttributes, device_fanout: 'false' }
|
additionalAttributes = { ...additionalAttributes, device_fanout: 'false' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user, device } = jidDecode(participant.jid)! // rajeh: how does this even make sense TODO check out
|
const { user, device } = jidDecode(participant.jid)!
|
||||||
devices.push({
|
devices.push({
|
||||||
user,
|
user,
|
||||||
device,
|
device,
|
||||||
wireJid: participant.jid // Use the participant JID as wire JID
|
wireJid: participant.jid
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,7 +628,6 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isNewsletter) {
|
if (isNewsletter) {
|
||||||
// Patch message if needed, then encode as plaintext
|
|
||||||
const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
|
const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
|
||||||
const bytes = encodeNewsletterMessage(patched as proto.IMessage)
|
const bytes = encodeNewsletterMessage(patched as proto.IMessage)
|
||||||
binaryNodeContent.push({
|
binaryNodeContent.push({
|
||||||
@@ -883,14 +696,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const patched = await patchMessageBeforeSending(message)
|
const patched = await patchMessageBeforeSending(message)
|
||||||
|
|
||||||
if (Array.isArray(patched)) {
|
if (Array.isArray(patched)) {
|
||||||
throw new Boom('Per-jid patching is not supported in groups')
|
throw new Boom('Per-jid patching is not supported in groups')
|
||||||
}
|
}
|
||||||
|
|
||||||
const bytes = encodeWAMessage(patched)
|
const bytes = encodeWAMessage(patched)
|
||||||
|
|
||||||
// This should match the group's addressing mode and conversation context
|
|
||||||
const groupAddressingMode = groupData?.addressingMode || (isLid ? 'lid' : 'pn')
|
const groupAddressingMode = groupData?.addressingMode || (isLid ? 'lid' : 'pn')
|
||||||
const groupSenderIdentity = groupAddressingMode === 'lid' && meLid ? meLid : meId
|
const groupSenderIdentity = groupAddressingMode === 'lid' && meLid ? meLid : meId
|
||||||
|
|
||||||
@@ -900,23 +710,19 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
meId: groupSenderIdentity
|
meId: groupSenderIdentity
|
||||||
})
|
})
|
||||||
|
|
||||||
const senderKeyJids: string[] = []
|
const deviceSessionMap = await resolveSessionJids(devices.map(d => d.wireJid))
|
||||||
// ensure a connection is established with every device
|
const senderKeyRecipients: string[] = []
|
||||||
for (const device of devices) {
|
for (const device of devices) {
|
||||||
// This preserves the LID migration results from getUSyncDevices
|
|
||||||
const deviceJid = device.wireJid
|
const deviceJid = device.wireJid
|
||||||
const hasKey = !!senderKeyMap[deviceJid]
|
const hasKey = !!senderKeyMap[deviceJid]
|
||||||
if (!hasKey || !!participant) {
|
if (!hasKey || !!participant) {
|
||||||
senderKeyJids.push(deviceJid)
|
senderKeyRecipients.push(deviceJid)
|
||||||
// store that this person has had the sender keys sent to them
|
|
||||||
senderKeyMap[deviceJid] = true
|
senderKeyMap[deviceJid] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there are some participants with whom the session has not been established
|
if (senderKeyRecipients.length) {
|
||||||
// if there are, we re-send the senderkey
|
logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending new sender key')
|
||||||
if (senderKeyJids.length) {
|
|
||||||
logger.debug({ senderKeyJids }, 'sending new sender key')
|
|
||||||
|
|
||||||
const senderKeyMsg: proto.IMessage = {
|
const senderKeyMsg: proto.IMessage = {
|
||||||
senderKeyDistributionMessage: {
|
senderKeyDistributionMessage: {
|
||||||
@@ -925,9 +731,15 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await assertSessions(senderKeyJids, false)
|
const senderKeySessionTargets = senderKeyRecipients.map(jid => deviceSessionMap.get(jid) ?? jid)
|
||||||
|
await assertSessions(senderKeySessionTargets)
|
||||||
|
|
||||||
const result = await createParticipantNodes(senderKeyJids, senderKeyMsg, extraAttrs)
|
const result = await createParticipantNodesWithSessionMap(
|
||||||
|
senderKeyRecipients,
|
||||||
|
deviceSessionMap,
|
||||||
|
senderKeyMsg,
|
||||||
|
extraAttrs
|
||||||
|
)
|
||||||
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
|
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
|
||||||
|
|
||||||
participants.push(...result.nodes)
|
participants.push(...result.nodes)
|
||||||
@@ -974,7 +786,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
: jidEncode(jidDecode(meId)?.user!, 's.whatsapp.net', undefined)
|
: jidEncode(jidDecode(meId)?.user!, 's.whatsapp.net', undefined)
|
||||||
|
|
||||||
// Enumerate devices for sender and target with consistent addressing
|
// Enumerate devices for sender and target with consistent addressing
|
||||||
const sessionDevices = await getUSyncDevices([senderIdentity, jid], false, false)
|
const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
|
||||||
devices.push(...sessionDevices)
|
devices.push(...sessionDevices)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -987,9 +799,9 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allJids: string[] = []
|
const allRecipients: string[] = []
|
||||||
const meJids: string[] = []
|
const meRecipients: string[] = []
|
||||||
const otherJids: string[] = []
|
const otherRecipients: string[] = []
|
||||||
const { user: mePnUser } = jidDecode(meId)!
|
const { user: mePnUser } = jidDecode(meId)!
|
||||||
const { user: meLidUser } = meLid ? jidDecode(meLid)! : { user: null }
|
const { user: meLidUser } = meLid ? jidDecode(meLid)! : { user: null }
|
||||||
|
|
||||||
@@ -1003,32 +815,32 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
// Check if this is our device (could match either PN or LID user)
|
// Check if this is our device (could match either PN or LID user)
|
||||||
const isMe = user === mePnUser || (meLidUser && user === meLidUser)
|
const isMe = user === mePnUser || (meLidUser && user === meLidUser)
|
||||||
|
|
||||||
const jid = wireJid
|
|
||||||
|
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
meJids.push(jid)
|
meRecipients.push(wireJid)
|
||||||
} else {
|
} else {
|
||||||
otherJids.push(jid)
|
otherRecipients.push(wireJid)
|
||||||
}
|
}
|
||||||
|
|
||||||
allJids.push(jid)
|
allRecipients.push(wireJid)
|
||||||
}
|
}
|
||||||
|
|
||||||
await assertSessions([...otherJids, ...meJids], false)
|
const deviceSessionMap = await resolveSessionJids(devices.map(d => d.wireJid))
|
||||||
|
const sessionTargets = allRecipients.map(jid => deviceSessionMap.get(jid) ?? jid)
|
||||||
|
await assertSessions(sessionTargets)
|
||||||
|
|
||||||
const [
|
const [
|
||||||
{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 },
|
{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 },
|
||||||
{ nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }
|
{ nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
// For own devices: use DSM if available (1:1 chats only)
|
// For own devices: use DSM if available (1:1 chats only)
|
||||||
createParticipantNodes(meJids, meMsg || message, extraAttrs),
|
createParticipantNodesWithSessionMap(meRecipients, deviceSessionMap, meMsg || message, extraAttrs),
|
||||||
createParticipantNodes(otherJids, message, extraAttrs, meMsg)
|
createParticipantNodesWithSessionMap(otherRecipients, deviceSessionMap, message, extraAttrs, meMsg)
|
||||||
])
|
])
|
||||||
participants.push(...meNodes)
|
participants.push(...meNodes)
|
||||||
participants.push(...otherNodes)
|
participants.push(...otherNodes)
|
||||||
|
|
||||||
if (meJids.length > 0 || otherJids.length > 0) {
|
if (meRecipients.length > 0 || otherRecipients.length > 0) {
|
||||||
extraAttrs['phash'] = generateParticipantHashV2([...meJids, ...otherJids])
|
extraAttrs['phash'] = generateParticipantHashV2([...meRecipients, ...otherRecipients])
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
|
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
|
||||||
@@ -1325,12 +1137,6 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
} as BinaryNode)
|
} as BinaryNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('cachedGroupMetadata' in options) {
|
|
||||||
console.warn(
|
|
||||||
'cachedGroupMetadata in sendMessage are deprecated, now cachedGroupMetadata is part of the socket config.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await relayMessage(jid, fullMsg.message!, {
|
await relayMessage(jid, fullMsg.message!, {
|
||||||
messageId: fullMsg.key.id!,
|
messageId: fullMsg.key.id!,
|
||||||
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
||||||
|
|||||||
+19
-6
@@ -42,6 +42,7 @@ import {
|
|||||||
getBinaryNodeChild,
|
getBinaryNodeChild,
|
||||||
getBinaryNodeChildren,
|
getBinaryNodeChildren,
|
||||||
isLidUser,
|
isLidUser,
|
||||||
|
jidDecode,
|
||||||
jidEncode,
|
jidEncode,
|
||||||
S_WHATSAPP_NET
|
S_WHATSAPP_NET
|
||||||
} from '../WABinary'
|
} from '../WABinary'
|
||||||
@@ -272,9 +273,13 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
const results = await executeUSyncQuery(usyncQuery)
|
const results = await executeUSyncQuery(usyncQuery)
|
||||||
|
|
||||||
if (results) {
|
if (results) {
|
||||||
if (results.list.filter(a => !!a.lid).length > 0) {
|
const lidOnly = results.list.filter(a => !!a.lid)
|
||||||
const lidOnly = results.list.filter(a => !!a.lid)
|
if (lidOnly.length > 0) {
|
||||||
await signalRepository.lidMapping.storeLIDPNMappings(lidOnly.map(a => ({ pn: a.id, lid: a.lid as string })))
|
const pairs = lidOnly.map(a => ({ pn: a.id, lid: a.lid as string }))
|
||||||
|
await signalRepository.lidMapping.storeLIDPNMappings(pairs)
|
||||||
|
for (const { pn, lid } of pairs) {
|
||||||
|
await signalRepository.migrateSession(pn, lid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.list
|
return results.list
|
||||||
@@ -288,7 +293,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
const { creds } = authState
|
const { creds } = authState
|
||||||
// add transaction capability
|
// add transaction capability
|
||||||
const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
|
const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
|
||||||
const signalRepository = makeSignalRepository({ creds, keys }, onWhatsApp)
|
const signalRepository = makeSignalRepository({ creds, keys }, logger, onWhatsApp)
|
||||||
|
|
||||||
let lastDateRecv: Date
|
let lastDateRecv: Date
|
||||||
let epoch = 1
|
let epoch = 1
|
||||||
@@ -853,8 +858,16 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
// Store our own LID-PN mapping
|
// Store our own LID-PN mapping
|
||||||
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }])
|
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }])
|
||||||
|
|
||||||
// Create LID session for ourselves (whatsmeow pattern)
|
// Create device list for our own user (needed for bulk migration)
|
||||||
await signalRepository.migrateSession([myPN], myLID)
|
const { user, device } = jidDecode(myPN)!
|
||||||
|
await authState.keys.set({
|
||||||
|
'device-list': {
|
||||||
|
[user]: [device?.toString() || '0']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// migrate our own session
|
||||||
|
await signalRepository.migrateSession(myPN, myLID)
|
||||||
|
|
||||||
logger.info({ myPN, myLID }, 'Own LID session created successfully')
|
logger.info({ myPN, myLID }, 'Own LID session created successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export type SignalDataTypeMap = {
|
|||||||
'app-state-sync-key': proto.Message.IAppStateSyncKeyData
|
'app-state-sync-key': proto.Message.IAppStateSyncKeyData
|
||||||
'app-state-sync-version': LTHashState
|
'app-state-sync-version': LTHashState
|
||||||
'lid-mapping': string
|
'lid-mapping': string
|
||||||
|
'device-list': string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignalDataSet = { [T in keyof SignalDataTypeMap]?: { [id: string]: SignalDataTypeMap[T] | null } }
|
export type SignalDataSet = { [T in keyof SignalDataTypeMap]?: { [id: string]: SignalDataTypeMap[T] | null } }
|
||||||
|
|||||||
+1
-12
@@ -23,12 +23,6 @@ type EncryptMessageOpts = {
|
|||||||
data: Uint8Array
|
data: Uint8Array
|
||||||
}
|
}
|
||||||
|
|
||||||
type EncryptMessageWithWireOpts = {
|
|
||||||
encryptionJid: string // JID used for session lookup (LID)
|
|
||||||
wireJid: string // JID used for envelope (PN)
|
|
||||||
data: Uint8Array
|
|
||||||
}
|
|
||||||
|
|
||||||
type EncryptGroupMessageOpts = {
|
type EncryptGroupMessageOpts = {
|
||||||
group: string
|
group: string
|
||||||
data: Uint8Array
|
data: Uint8Array
|
||||||
@@ -64,11 +58,6 @@ export type SignalRepository = {
|
|||||||
type: 'pkmsg' | 'msg'
|
type: 'pkmsg' | 'msg'
|
||||||
ciphertext: Uint8Array
|
ciphertext: Uint8Array
|
||||||
}>
|
}>
|
||||||
encryptMessageWithWire(opts: EncryptMessageWithWireOpts): Promise<{
|
|
||||||
type: 'pkmsg' | 'msg'
|
|
||||||
ciphertext: Uint8Array
|
|
||||||
wireJid: string // Return the wire JID for envelope
|
|
||||||
}>
|
|
||||||
encryptGroupMessage(opts: EncryptGroupMessageOpts): Promise<{
|
encryptGroupMessage(opts: EncryptGroupMessageOpts): Promise<{
|
||||||
senderKeyDistributionMessage: Uint8Array
|
senderKeyDistributionMessage: Uint8Array
|
||||||
ciphertext: Uint8Array
|
ciphertext: Uint8Array
|
||||||
@@ -76,7 +65,7 @@ export type SignalRepository = {
|
|||||||
injectE2ESession(opts: E2ESessionOpts): Promise<void>
|
injectE2ESession(opts: E2ESessionOpts): Promise<void>
|
||||||
validateSession(jid: string): Promise<{ exists: boolean; reason?: string }>
|
validateSession(jid: string): Promise<{ exists: boolean; reason?: string }>
|
||||||
jidToSignalProtocolAddress(jid: string): string
|
jidToSignalProtocolAddress(jid: string): string
|
||||||
migrateSession(fromJids: string[], toJid: string): Promise<{ migrated: number; skipped: number; total: number }>
|
migrateSession(fromJid: string, toJid: string): Promise<{ migrated: number; skipped: number; total: number }>
|
||||||
validateSession(jid: string): Promise<{ exists: boolean; reason?: string }>
|
validateSession(jid: string): Promise<{ exists: boolean; reason?: string }>
|
||||||
deleteSession(jids: string[]): Promise<void>
|
deleteSession(jids: string[]): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export type SocketConfig = {
|
|||||||
|
|
||||||
makeSignalRepository: (
|
makeSignalRepository: (
|
||||||
auth: SignalAuthState,
|
auth: SignalAuthState,
|
||||||
|
logger: ILogger,
|
||||||
onWhatsAppFunc?: (...jids: string[]) => Promise<
|
onWhatsAppFunc?: (...jids: string[]) => Promise<
|
||||||
| {
|
| {
|
||||||
jid: string
|
jid: string
|
||||||
|
|||||||
@@ -23,14 +23,15 @@ const getDecryptionJid = async (sender: string, repository: SignalRepositoryWith
|
|||||||
return sender
|
return sender
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await repository.lidMapping.getLIDForPN(sender))!
|
const mapped = await repository.lidMapping.getLIDForPN(sender)
|
||||||
|
return mapped || sender
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeMappingFromEnvelope = async (
|
const storeMappingFromEnvelope = async (
|
||||||
stanza: BinaryNode,
|
stanza: BinaryNode,
|
||||||
sender: string,
|
sender: string,
|
||||||
decryptionJid: string,
|
|
||||||
repository: SignalRepositoryWithLIDStore,
|
repository: SignalRepositoryWithLIDStore,
|
||||||
|
decryptionJid: string,
|
||||||
logger: ILogger
|
logger: ILogger
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const { senderAlt } = extractAddressingContext(stanza)
|
const { senderAlt } = extractAddressingContext(stanza)
|
||||||
@@ -38,6 +39,7 @@ const storeMappingFromEnvelope = async (
|
|||||||
if (senderAlt && isLidUser(senderAlt) && isPnUser(sender) && decryptionJid === sender) {
|
if (senderAlt && isLidUser(senderAlt) && isPnUser(sender) && decryptionJid === sender) {
|
||||||
try {
|
try {
|
||||||
await repository.lidMapping.storeLIDPNMappings([{ lid: senderAlt, pn: sender }])
|
await repository.lidMapping.storeLIDPNMappings([{ lid: senderAlt, pn: sender }])
|
||||||
|
await repository.migrateSession(sender, senderAlt)
|
||||||
logger.debug({ sender, senderAlt }, 'Stored LID mapping from envelope')
|
logger.debug({ sender, senderAlt }, 'Stored LID mapping from envelope')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn({ sender, senderAlt, error }, 'Failed to store LID mapping')
|
logger.warn({ sender, senderAlt, error }, 'Failed to store LID mapping')
|
||||||
@@ -243,9 +245,11 @@ export const decryptMessageNode = (
|
|||||||
let msgBuffer: Uint8Array
|
let msgBuffer: Uint8Array
|
||||||
|
|
||||||
const user = isPnUser(sender) ? sender : author // TODO: flaky logic
|
const user = isPnUser(sender) ? sender : author // TODO: flaky logic
|
||||||
|
|
||||||
const decryptionJid = await getDecryptionJid(user, repository)
|
const decryptionJid = await getDecryptionJid(user, repository)
|
||||||
|
|
||||||
if (tag !== 'plaintext') {
|
if (tag !== 'plaintext') {
|
||||||
await storeMappingFromEnvelope(stanza, user, decryptionJid, repository, logger)
|
await storeMappingFromEnvelope(stanza, user, repository, decryptionJid, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -318,6 +318,11 @@ const processMessage = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
await signalRepository.lidMapping.storeLIDPNMappings(pairs)
|
await signalRepository.lidMapping.storeLIDPNMappings(pairs)
|
||||||
|
if (pairs.length) {
|
||||||
|
for (const { pn, lid } of pairs) {
|
||||||
|
await signalRepository.migrateSession(pn, lid)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (content?.reactionMessage) {
|
} else if (content?.reactionMessage) {
|
||||||
const reaction: proto.IReaction = {
|
const reaction: proto.IReaction = {
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export const parseAndInjectE2ESessions = async (node: BinaryNode, repository: Si
|
|||||||
// It's rare case when you need to E2E sessions for so many users, but it's possible
|
// It's rare case when you need to E2E sessions for so many users, but it's possible
|
||||||
const chunkSize = 100
|
const chunkSize = 100
|
||||||
const chunks = chunk(nodes, chunkSize)
|
const chunks = chunk(nodes, chunkSize)
|
||||||
|
|
||||||
for (const nodesChunk of chunks) {
|
for (const nodesChunk of chunks) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
nodesChunk.map(async (node: BinaryNode) => {
|
nodesChunk.map(async (node: BinaryNode) => {
|
||||||
@@ -113,6 +114,7 @@ export const parseAndInjectE2ESessions = async (node: BinaryNode, repository: Si
|
|||||||
const key = getBinaryNodeChild(node, 'key')!
|
const key = getBinaryNodeChild(node, 'key')!
|
||||||
const identity = getBinaryNodeChildBuffer(node, 'identity')!
|
const identity = getBinaryNodeChildBuffer(node, 'identity')!
|
||||||
const jid = node.attrs.jid!
|
const jid = node.attrs.jid!
|
||||||
|
|
||||||
const registrationId = getBinaryNodeChildUInt(node, 'registration', 4)
|
const registrationId = getBinaryNodeChildUInt(node, 'registration', 4)
|
||||||
|
|
||||||
await repository.injectE2ESession({
|
await repository.injectE2ESession({
|
||||||
|
|||||||
Reference in New Issue
Block a user