cleaning up after AI slop!
This commit is contained in:
+1
-22
@@ -13,11 +13,9 @@ import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage } from '
|
|||||||
import { LIDMappingStore } from './lid-mapping'
|
import { LIDMappingStore } from './lid-mapping'
|
||||||
|
|
||||||
export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository {
|
export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository {
|
||||||
|
|
||||||
const lidMapping = new LIDMappingStore(auth.keys as SignalKeyStoreWithTransaction)
|
const lidMapping = new LIDMappingStore(auth.keys as SignalKeyStoreWithTransaction)
|
||||||
const storage = signalStorage(auth, lidMapping)
|
const storage = signalStorage(auth, lidMapping)
|
||||||
|
|
||||||
|
|
||||||
const parsedKeys = auth.keys as SignalKeyStoreWithTransaction
|
const parsedKeys = auth.keys as SignalKeyStoreWithTransaction
|
||||||
|
|
||||||
function isLikelySyncMessage(addr: any): boolean {
|
function isLikelySyncMessage(addr: any): boolean {
|
||||||
@@ -28,7 +26,7 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
return (
|
return (
|
||||||
key.includes('@lid.whatsapp.net') || // WhatsApp system messages
|
key.includes('@lid.whatsapp.net') || // WhatsApp system messages
|
||||||
key.includes('@broadcast') || // Broadcast messages
|
key.includes('@broadcast') || // Broadcast messages
|
||||||
key.includes('@newsletter')
|
key.includes('@newsletter')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +37,6 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
})
|
})
|
||||||
|
|
||||||
const repository: SignalRepository = {
|
const repository: SignalRepository = {
|
||||||
|
|
||||||
decryptGroupMessage({ group, authorJid, msg }) {
|
decryptGroupMessage({ group, authorJid, msg }) {
|
||||||
const senderName = jidToSignalSenderKeyName(group, authorJid)
|
const senderName = jidToSignalSenderKeyName(group, authorJid)
|
||||||
const cipher = new GroupCipher(storage, senderName)
|
const cipher = new GroupCipher(storage, senderName)
|
||||||
@@ -176,24 +173,6 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
await cipher.initOutgoing(session)
|
await cipher.initOutgoing(session)
|
||||||
}, jid)
|
}, jid)
|
||||||
},
|
},
|
||||||
async validateSession(jid: string) {
|
|
||||||
try {
|
|
||||||
const addr = jidToSignalProtocolAddress(jid)
|
|
||||||
const session = await storage.loadSession(addr.toString())
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return { exists: false, reason: 'no session' }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session.haveOpenSession()) {
|
|
||||||
return { exists: false, reason: 'no open session' }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { exists: true }
|
|
||||||
} catch (error) {
|
|
||||||
return { exists: false, reason: 'validation error' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
jidToSignalProtocolAddress(jid) {
|
jidToSignalProtocolAddress(jid) {
|
||||||
return jidToSignalProtocolAddress(jid).toString()
|
return jidToSignalProtocolAddress(jid).toString()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export class LIDMappingStore {
|
|||||||
[`${lidUser}_reverse`]: pnUser // "102765716062358_reverse" -> "554396160286"
|
[`${lidUser}_reverse`]: pnUser // "102765716062358_reverse" -> "554396160286"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}, 'lid-mapping')
|
||||||
|
|
||||||
logger.trace(`USER LID mapping stored: PN ${pnUser} → LID ${lidUser}`)
|
logger.trace(`USER LID mapping stored: PN ${pnUser} → LID ${lidUser}`)
|
||||||
}
|
}
|
||||||
|
|||||||
+330
-329
@@ -107,6 +107,219 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
let sendActiveReceipts = false
|
let sendActiveReceipts = false
|
||||||
|
|
||||||
|
const fetchMessageHistory = async (
|
||||||
|
count: number,
|
||||||
|
oldestMsgKey: WAMessageKey,
|
||||||
|
oldestMsgTimestamp: number | Long
|
||||||
|
): Promise<string> => {
|
||||||
|
if (!authState.creds.me?.id) {
|
||||||
|
throw new Boom('Not authenticated')
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdoMessage: proto.Message.IPeerDataOperationRequestMessage = {
|
||||||
|
historySyncOnDemandRequest: {
|
||||||
|
chatJid: oldestMsgKey.remoteJid,
|
||||||
|
oldestMsgFromMe: oldestMsgKey.fromMe,
|
||||||
|
oldestMsgId: oldestMsgKey.id,
|
||||||
|
oldestMsgTimestampMs: oldestMsgTimestamp,
|
||||||
|
onDemandMsgCount: count
|
||||||
|
},
|
||||||
|
peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendPeerDataOperationMessage(pdoMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestPlaceholderResend = async (messageKey: WAMessageKey): Promise<string | undefined> => {
|
||||||
|
if (!authState.creds.me?.id) {
|
||||||
|
throw new Boom('Not authenticated')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeholderResendCache.get(messageKey?.id!)) {
|
||||||
|
logger.debug({ messageKey }, 'already requested resend')
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
placeholderResendCache.set(messageKey?.id!, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
await delay(5000)
|
||||||
|
|
||||||
|
if (!placeholderResendCache.get(messageKey?.id!)) {
|
||||||
|
logger.debug({ messageKey }, 'message received while resend requested')
|
||||||
|
return 'RESOLVED'
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdoMessage = {
|
||||||
|
placeholderMessageResendRequest: [
|
||||||
|
{
|
||||||
|
messageKey
|
||||||
|
}
|
||||||
|
],
|
||||||
|
peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (placeholderResendCache.get(messageKey?.id!)) {
|
||||||
|
logger.debug({ messageKey }, 'PDO message without response after 15 seconds. Phone possibly offline')
|
||||||
|
placeholderResendCache.del(messageKey?.id!)
|
||||||
|
}
|
||||||
|
}, 15_000)
|
||||||
|
|
||||||
|
return sendPeerDataOperationMessage(pdoMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles mex newsletter notifications
|
||||||
|
const handleMexNewsletterNotification = async (node: BinaryNode) => {
|
||||||
|
const mexNode = getBinaryNodeChild(node, 'mex')
|
||||||
|
if (!mexNode?.content) {
|
||||||
|
logger.warn({ node }, 'Invalid mex newsletter notification')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: any
|
||||||
|
try {
|
||||||
|
data = JSON.parse(mexNode.content.toString())
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ err: error, node }, 'Failed to parse mex newsletter notification')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const operation = data?.operation
|
||||||
|
const updates = data?.updates
|
||||||
|
|
||||||
|
if (!updates || !operation) {
|
||||||
|
logger.warn({ data }, 'Invalid mex newsletter notification content')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({ operation, updates }, 'got mex newsletter notification')
|
||||||
|
|
||||||
|
switch (operation) {
|
||||||
|
case 'NotificationNewsletterUpdate':
|
||||||
|
for (const update of updates) {
|
||||||
|
if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
|
||||||
|
ev.emit('newsletter-settings.update', {
|
||||||
|
id: update.jid,
|
||||||
|
update: update.settings
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'NotificationNewsletterAdminPromote':
|
||||||
|
for (const update of updates) {
|
||||||
|
if (update.jid && update.user) {
|
||||||
|
ev.emit('newsletter-participants.update', {
|
||||||
|
id: update.jid,
|
||||||
|
author: node.attrs.from!,
|
||||||
|
user: update.user,
|
||||||
|
new_role: 'ADMIN',
|
||||||
|
action: 'promote'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.info({ operation, data }, 'Unhandled mex newsletter notification')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles newsletter notifications
|
||||||
|
const handleNewsletterNotification = async (node: BinaryNode) => {
|
||||||
|
const from = node.attrs.from!
|
||||||
|
const child = getAllBinaryNodeChildren(node)[0]!
|
||||||
|
const author = node.attrs.participant!
|
||||||
|
|
||||||
|
logger.info({ from, child }, 'got newsletter notification')
|
||||||
|
|
||||||
|
switch (child.tag) {
|
||||||
|
case 'reaction':
|
||||||
|
const reactionUpdate = {
|
||||||
|
id: from,
|
||||||
|
server_id: child.attrs.message_id!,
|
||||||
|
reaction: {
|
||||||
|
code: getBinaryNodeChildString(child, 'reaction'),
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ev.emit('newsletter.reaction', reactionUpdate)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'view':
|
||||||
|
const viewUpdate = {
|
||||||
|
id: from,
|
||||||
|
server_id: child.attrs.message_id!,
|
||||||
|
count: parseInt(child.content?.toString() || '0', 10)
|
||||||
|
}
|
||||||
|
ev.emit('newsletter.view', viewUpdate)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'participant':
|
||||||
|
const participantUpdate = {
|
||||||
|
id: from,
|
||||||
|
author,
|
||||||
|
user: child.attrs.jid!,
|
||||||
|
action: child.attrs.action!,
|
||||||
|
new_role: child.attrs.role!
|
||||||
|
}
|
||||||
|
ev.emit('newsletter-participants.update', participantUpdate)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'update':
|
||||||
|
const settingsNode = getBinaryNodeChild(child, 'settings')
|
||||||
|
if (settingsNode) {
|
||||||
|
const update: Record<string, any> = {}
|
||||||
|
const nameNode = getBinaryNodeChild(settingsNode, 'name')
|
||||||
|
if (nameNode?.content) update.name = nameNode.content.toString()
|
||||||
|
|
||||||
|
const descriptionNode = getBinaryNodeChild(settingsNode, 'description')
|
||||||
|
if (descriptionNode?.content) update.description = descriptionNode.content.toString()
|
||||||
|
|
||||||
|
ev.emit('newsletter-settings.update', {
|
||||||
|
id: from,
|
||||||
|
update
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'message':
|
||||||
|
const plaintextNode = getBinaryNodeChild(child, 'plaintext')
|
||||||
|
if (plaintextNode?.content) {
|
||||||
|
try {
|
||||||
|
const contentBuf =
|
||||||
|
typeof plaintextNode.content === 'string'
|
||||||
|
? Buffer.from(plaintextNode.content, 'binary')
|
||||||
|
: Buffer.from(plaintextNode.content as Uint8Array)
|
||||||
|
const messageProto = proto.Message.decode(contentBuf)
|
||||||
|
const fullMessage = proto.WebMessageInfo.create({
|
||||||
|
key: {
|
||||||
|
remoteJid: from,
|
||||||
|
id: child.attrs.message_id || child.attrs.server_id,
|
||||||
|
fromMe: false
|
||||||
|
},
|
||||||
|
message: messageProto,
|
||||||
|
messageTimestamp: +child.attrs.t!
|
||||||
|
})
|
||||||
|
await upsertMessage(fullMessage, 'append')
|
||||||
|
logger.info('Processed plaintext newsletter message')
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, 'Failed to decode plaintext newsletter message')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.warn({ node }, 'Unknown newsletter notification')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sendMessageAck = async ({ tag, attrs, content }: BinaryNode, errorCode?: number) => {
|
const sendMessageAck = async ({ tag, attrs, content }: BinaryNode, errorCode?: number) => {
|
||||||
const stanza: BinaryNode = {
|
const stanza: BinaryNode = {
|
||||||
tag: 'ack',
|
tag: 'ack',
|
||||||
@@ -941,203 +1154,143 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
},
|
},
|
||||||
'Added message to recent cache for retry receipts'
|
'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(
|
if (msg.message?.protocolMessage?.lidMigrationMappingSyncMessage?.encodedMappingPayload) {
|
||||||
{
|
try {
|
||||||
mappingCount: decoded.pnToLidMappings?.length || 0,
|
const payload = msg.message.protocolMessage.lidMigrationMappingSyncMessage.encodedMappingPayload
|
||||||
timestamp: decoded.chatDbMigrationTimestamp
|
const decoded = proto.LIDMigrationMappingSyncPayload.decode(payload)
|
||||||
},
|
|
||||||
'Received LID migration sync message from server'
|
|
||||||
)
|
|
||||||
|
|
||||||
const lidMapping = signalRepository.getLIDMappingStore()
|
logger.debug(
|
||||||
if (decoded.pnToLidMappings && decoded.pnToLidMappings.length > 0) {
|
{
|
||||||
for (const mapping of decoded.pnToLidMappings) {
|
mappingCount: decoded.pnToLidMappings?.length || 0,
|
||||||
const pn = `${mapping.pn}@s.whatsapp.net`
|
timestamp: decoded.chatDbMigrationTimestamp
|
||||||
// Use latestLid if available, otherwise assignedLid (proper LID refresh)
|
},
|
||||||
const lidValue = mapping.latestLid || mapping.assignedLid
|
'Received LID migration sync message from server'
|
||||||
const lid = `${lidValue}@lid`
|
)
|
||||||
|
|
||||||
await lidMapping.storeLIDPNMapping(lid, pn)
|
const lidMapping = signalRepository.getLIDMappingStore()
|
||||||
logger.debug(
|
if (decoded.pnToLidMappings && decoded.pnToLidMappings.length > 0) {
|
||||||
{
|
for (const mapping of decoded.pnToLidMappings) {
|
||||||
pn,
|
const pn = `${mapping.pn}@s.whatsapp.net`
|
||||||
lid,
|
// Use latestLid if available, otherwise assignedLid (proper LID refresh)
|
||||||
assignedLid: mapping.assignedLid,
|
const lidValue = mapping.latestLid || mapping.assignedLid
|
||||||
latestLid: mapping.latestLid,
|
const lid = `${lidValue}@lid`
|
||||||
usedLatest: !!mapping.latestLid
|
|
||||||
},
|
|
||||||
'Stored server-provided PN-LID mapping'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ error }, 'Failed to process LID migration sync message')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
await lidMapping.storeLIDPNMapping(lid, pn)
|
||||||
await Promise.all([
|
logger.debug(
|
||||||
processingMutex.mutex(async () => {
|
{
|
||||||
await decrypt()
|
pn,
|
||||||
// message failed to decrypt
|
lid,
|
||||||
if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
|
assignedLid: mapping.assignedLid,
|
||||||
if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
|
latestLid: mapping.latestLid,
|
||||||
return sendMessageAck(node, NACK_REASONS.ParsingError)
|
usedLatest: !!mapping.latestLid
|
||||||
|
},
|
||||||
|
'Stored server-provided PN-LID mapping'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, 'Failed to process LID migration sync message')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const errorMessage = msg?.messageStubParameters?.[0] || ''
|
try {
|
||||||
const isPreKeyError = errorMessage.includes('PreKey')
|
await Promise.all([
|
||||||
|
processingMutex.mutex(async () => {
|
||||||
|
await decrypt()
|
||||||
|
// message failed to decrypt
|
||||||
|
if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
|
||||||
|
if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
|
||||||
|
return sendMessageAck(node, NACK_REASONS.ParsingError)
|
||||||
|
}
|
||||||
|
|
||||||
console.debug(`[handleMessage] Attempting retry request for failed decryption`)
|
const errorMessage = msg?.messageStubParameters?.[0] || ''
|
||||||
|
const isPreKeyError = errorMessage.includes('PreKey')
|
||||||
|
|
||||||
// Handle both pre-key and normal retries in single mutex
|
console.debug(`[handleMessage] Attempting retry request for failed decryption`)
|
||||||
retryMutex.mutex(async () => {
|
|
||||||
try {
|
|
||||||
if (!ws.isOpen) {
|
|
||||||
logger.debug({ node }, 'Connection closed, skipping retry')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getBinaryNodeChild(node, 'unavailable')) {
|
// Handle both pre-key and normal retries in single mutex
|
||||||
logger.debug('Message unavailable, skipping retry')
|
retryMutex.mutex(async () => {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle pre-key errors with upload and delay
|
|
||||||
if (isPreKeyError) {
|
|
||||||
logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying')
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.debug('Uploading pre-keys for error recovery')
|
|
||||||
await uploadPreKeys(5)
|
|
||||||
logger.debug('Waiting for server to process new pre-keys')
|
|
||||||
await delay(1000)
|
|
||||||
} catch (uploadErr) {
|
|
||||||
logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const encNode = getBinaryNodeChild(node, 'enc')
|
|
||||||
await sendRetryRequest(node, !encNode)
|
|
||||||
if (retryRequestDelayMs) {
|
|
||||||
await delay(retryRequestDelayMs)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry')
|
|
||||||
// Still attempt retry even if pre-key upload failed
|
|
||||||
try {
|
try {
|
||||||
|
if (!ws.isOpen) {
|
||||||
|
logger.debug({ node }, 'Connection closed, skipping retry')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getBinaryNodeChild(node, 'unavailable')) {
|
||||||
|
logger.debug('Message unavailable, skipping retry')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pre-key errors with upload and delay
|
||||||
|
if (isPreKeyError) {
|
||||||
|
logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying')
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug('Uploading pre-keys for error recovery')
|
||||||
|
await uploadPreKeys(5)
|
||||||
|
logger.debug('Waiting for server to process new pre-keys')
|
||||||
|
await delay(1000)
|
||||||
|
} catch (uploadErr) {
|
||||||
|
logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const encNode = getBinaryNodeChild(node, 'enc')
|
const encNode = getBinaryNodeChild(node, 'enc')
|
||||||
await sendRetryRequest(node, !encNode)
|
await sendRetryRequest(node, !encNode)
|
||||||
} catch (retryErr) {
|
if (retryRequestDelayMs) {
|
||||||
logger.error({ retryErr }, 'Failed to send retry after error handling')
|
await delay(retryRequestDelayMs)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry')
|
||||||
|
// Still attempt retry even if pre-key upload failed
|
||||||
|
try {
|
||||||
|
const encNode = getBinaryNodeChild(node, 'enc')
|
||||||
|
await sendRetryRequest(node, !encNode)
|
||||||
|
} catch (retryErr) {
|
||||||
|
logger.error({ retryErr }, 'Failed to send retry after error handling')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// no type in the receipt => message delivered
|
||||||
|
let type: MessageReceiptType = undefined
|
||||||
|
let participant = msg.key.participant
|
||||||
|
if (category === 'peer') {
|
||||||
|
// special peer message
|
||||||
|
type = 'peer_msg'
|
||||||
|
} else if (msg.key.fromMe) {
|
||||||
|
// message was sent by us from a different device
|
||||||
|
type = 'sender'
|
||||||
|
// need to specially handle this case
|
||||||
|
if (isJidUser(msg.key.remoteJid!)) {
|
||||||
|
participant = author
|
||||||
|
}
|
||||||
|
} else if (!sendActiveReceipts) {
|
||||||
|
type = 'inactive'
|
||||||
}
|
}
|
||||||
})
|
|
||||||
} else {
|
await sendReceipt(msg.key.remoteJid!, participant!, [msg.key.id!], type)
|
||||||
// no type in the receipt => message delivered
|
|
||||||
let type: MessageReceiptType = undefined
|
// send ack for history message
|
||||||
let participant = msg.key.participant
|
const isAnyHistoryMsg = getHistoryMsg(msg.message!)
|
||||||
if (category === 'peer') {
|
if (isAnyHistoryMsg) {
|
||||||
// special peer message
|
const jid = jidNormalizedUser(msg.key.remoteJid!)
|
||||||
type = 'peer_msg'
|
await sendReceipt(jid, undefined, [msg.key.id!], 'hist_sync')
|
||||||
} else if (msg.key.fromMe) {
|
|
||||||
// message was sent by us from a different device
|
|
||||||
type = 'sender'
|
|
||||||
// need to specially handle this case
|
|
||||||
if (isJidUser(msg.key.remoteJid!)) {
|
|
||||||
participant = author
|
|
||||||
}
|
}
|
||||||
} else if (!sendActiveReceipts) {
|
|
||||||
type = 'inactive'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendReceipt(msg.key.remoteJid!, participant!, [msg.key.id!], type)
|
cleanMessage(msg, authState.creds.me!.id)
|
||||||
|
|
||||||
// send ack for history message
|
await sendMessageAck(node)
|
||||||
const isAnyHistoryMsg = getHistoryMsg(msg.message!)
|
|
||||||
if (isAnyHistoryMsg) {
|
|
||||||
const jid = jidNormalizedUser(msg.key.remoteJid!)
|
|
||||||
await sendReceipt(jid, undefined, [msg.key.id!], 'hist_sync')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanMessage(msg, authState.creds.me!.id)
|
await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify')
|
||||||
|
})
|
||||||
await sendMessageAck(node)
|
])
|
||||||
|
} catch (error) {
|
||||||
await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify')
|
logger.error({ error, node }, 'error in handling message')
|
||||||
})
|
|
||||||
])
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ error, node }, 'error in handling message')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchMessageHistory = async (
|
|
||||||
count: number,
|
|
||||||
oldestMsgKey: WAMessageKey,
|
|
||||||
oldestMsgTimestamp: number | Long
|
|
||||||
): Promise<string> => {
|
|
||||||
if (!authState.creds.me?.id) {
|
|
||||||
throw new Boom('Not authenticated')
|
|
||||||
}
|
|
||||||
|
|
||||||
const pdoMessage: proto.Message.IPeerDataOperationRequestMessage = {
|
|
||||||
historySyncOnDemandRequest: {
|
|
||||||
chatJid: oldestMsgKey.remoteJid,
|
|
||||||
oldestMsgFromMe: oldestMsgKey.fromMe,
|
|
||||||
oldestMsgId: oldestMsgKey.id,
|
|
||||||
oldestMsgTimestampMs: oldestMsgTimestamp,
|
|
||||||
onDemandMsgCount: count
|
|
||||||
},
|
|
||||||
peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND
|
|
||||||
}
|
|
||||||
|
|
||||||
return sendPeerDataOperationMessage(pdoMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestPlaceholderResend = async (messageKey: WAMessageKey): Promise<string | undefined> => {
|
|
||||||
if (!authState.creds.me?.id) {
|
|
||||||
throw new Boom('Not authenticated')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (placeholderResendCache.get(messageKey?.id!)) {
|
|
||||||
logger.debug({ messageKey }, 'already requested resend')
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
placeholderResendCache.set(messageKey?.id!, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
await delay(5000)
|
|
||||||
|
|
||||||
if (!placeholderResendCache.get(messageKey?.id!)) {
|
|
||||||
logger.debug({ messageKey }, 'message received while resend requested')
|
|
||||||
return 'RESOLVED'
|
|
||||||
}
|
|
||||||
|
|
||||||
const pdoMessage = {
|
|
||||||
placeholderMessageResendRequest: [
|
|
||||||
{
|
|
||||||
messageKey
|
|
||||||
}
|
|
||||||
],
|
|
||||||
peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (placeholderResendCache.get(messageKey?.id!)) {
|
|
||||||
logger.debug({ messageKey }, 'PDO message without response after 15 seconds. Phone possibly offline')
|
|
||||||
placeholderResendCache.del(messageKey?.id!)
|
|
||||||
}
|
}
|
||||||
}, 15_000)
|
}
|
||||||
|
|
||||||
return sendPeerDataOperationMessage(pdoMessage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCall = async (node: BinaryNode) => {
|
const handleCall = async (node: BinaryNode) => {
|
||||||
@@ -1318,158 +1471,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles newsletter notifications
|
|
||||||
async function handleNewsletterNotification(node: BinaryNode) {
|
|
||||||
const from = node.attrs.from!
|
|
||||||
const child = getAllBinaryNodeChildren(node)[0]!
|
|
||||||
const author = node.attrs.participant!
|
|
||||||
|
|
||||||
logger.info({ from, child }, 'got newsletter notification')
|
|
||||||
|
|
||||||
switch (child.tag) {
|
|
||||||
case 'reaction':
|
|
||||||
const reactionUpdate = {
|
|
||||||
id: from,
|
|
||||||
server_id: child.attrs.message_id!,
|
|
||||||
reaction: {
|
|
||||||
code: getBinaryNodeChildString(child, 'reaction'),
|
|
||||||
count: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ev.emit('newsletter.reaction', reactionUpdate)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'view':
|
|
||||||
const viewUpdate = {
|
|
||||||
id: from,
|
|
||||||
server_id: child.attrs.message_id!,
|
|
||||||
count: parseInt(child.content?.toString() || '0', 10)
|
|
||||||
}
|
|
||||||
ev.emit('newsletter.view', viewUpdate)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'participant':
|
|
||||||
const participantUpdate = {
|
|
||||||
id: from,
|
|
||||||
author,
|
|
||||||
user: child.attrs.jid!,
|
|
||||||
action: child.attrs.action!,
|
|
||||||
new_role: child.attrs.role!
|
|
||||||
}
|
|
||||||
ev.emit('newsletter-participants.update', participantUpdate)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'update':
|
|
||||||
const settingsNode = getBinaryNodeChild(child, 'settings')
|
|
||||||
if (settingsNode) {
|
|
||||||
const update: Record<string, any> = {}
|
|
||||||
const nameNode = getBinaryNodeChild(settingsNode, 'name')
|
|
||||||
if (nameNode?.content) update.name = nameNode.content.toString()
|
|
||||||
|
|
||||||
const descriptionNode = getBinaryNodeChild(settingsNode, 'description')
|
|
||||||
if (descriptionNode?.content) update.description = descriptionNode.content.toString()
|
|
||||||
|
|
||||||
ev.emit('newsletter-settings.update', {
|
|
||||||
id: from,
|
|
||||||
update
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'message':
|
|
||||||
const plaintextNode = getBinaryNodeChild(child, 'plaintext')
|
|
||||||
if (plaintextNode?.content) {
|
|
||||||
try {
|
|
||||||
const contentBuf =
|
|
||||||
typeof plaintextNode.content === 'string'
|
|
||||||
? Buffer.from(plaintextNode.content, 'binary')
|
|
||||||
: Buffer.from(plaintextNode.content as Uint8Array)
|
|
||||||
const messageProto = proto.Message.decode(contentBuf)
|
|
||||||
const fullMessage = proto.WebMessageInfo.create({
|
|
||||||
key: {
|
|
||||||
remoteJid: from,
|
|
||||||
id: child.attrs.message_id || child.attrs.server_id,
|
|
||||||
fromMe: false
|
|
||||||
},
|
|
||||||
message: messageProto,
|
|
||||||
messageTimestamp: +child.attrs.t!
|
|
||||||
})
|
|
||||||
await upsertMessage(fullMessage, 'append')
|
|
||||||
logger.info('Processed plaintext newsletter message')
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ error }, 'Failed to decode plaintext newsletter message')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
logger.warn({ node }, 'Unknown newsletter notification')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles mex newsletter notifications
|
|
||||||
async function handleMexNewsletterNotification(node: BinaryNode) {
|
|
||||||
const mexNode = getBinaryNodeChild(node, 'mex')
|
|
||||||
if (!mexNode?.content) {
|
|
||||||
logger.warn({ node }, 'Invalid mex newsletter notification')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let data: any
|
|
||||||
try {
|
|
||||||
data = JSON.parse(mexNode.content.toString())
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error, node }, 'Failed to parse mex newsletter notification')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const operation = data?.operation
|
|
||||||
const updates = data?.updates
|
|
||||||
|
|
||||||
if (!updates || !operation) {
|
|
||||||
logger.warn({ data }, 'Invalid mex newsletter notification content')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info({ operation, updates }, 'got mex newsletter notification')
|
|
||||||
|
|
||||||
switch (operation) {
|
|
||||||
case 'NotificationNewsletterUpdate':
|
|
||||||
for (const update of updates) {
|
|
||||||
if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
|
|
||||||
ev.emit('newsletter-settings.update', {
|
|
||||||
id: update.jid,
|
|
||||||
update: update.settings
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'NotificationNewsletterAdminPromote':
|
|
||||||
for (const update of updates) {
|
|
||||||
if (update.jid && update.user) {
|
|
||||||
ev.emit('newsletter-participants.update', {
|
|
||||||
id: update.jid,
|
|
||||||
author: node.attrs.from!,
|
|
||||||
user: update.user,
|
|
||||||
new_role: 'ADMIN',
|
|
||||||
action: 'promote'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
logger.info({ operation, data }, 'Unhandled mex newsletter notification')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// recv a message
|
// recv a message
|
||||||
ws.on('CB:message', (node: BinaryNode) => {
|
ws.on('CB:message', (node: BinaryNode) => {
|
||||||
processNode('message', node, 'processing message', handleMessage)
|
processNode('message', node, 'processing message', handleMessage)
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
// 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
|
||||||
|
|
||||||
// Prevent race conditions in Signal session encryption by user
|
// Prevent race conditions in Signal session encryption by user
|
||||||
const encryptionMutex = makeKeyedMutex()
|
const encryptionMutex = makeKeyedMutex()
|
||||||
|
|
||||||
let mediaConn: Promise<MediaConnInfo>
|
let mediaConn: Promise<MediaConnInfo>
|
||||||
@@ -395,9 +395,6 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jidsRequiringFetch.push(jid)
|
jidsRequiringFetch.push(jid)
|
||||||
if (validation.reason) {
|
|
||||||
logger.debug({ jid, reason: validation.reason }, 'session validation failed')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1128,7 +1125,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
await sendNode(stanza)
|
await sendNode(stanza)
|
||||||
|
|
||||||
// Add message to retry cache if enabled
|
// Add message to retry cache if enabled
|
||||||
if (messageRetryManager && !participant) {
|
if (messageRetryManager && !participant) {
|
||||||
messageRetryManager.addRecentMessage(destinationJid, msgId, message)
|
messageRetryManager.addRecentMessage(destinationJid, msgId, message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
NOISE_WA_HEADER,
|
NOISE_WA_HEADER,
|
||||||
UPLOAD_TIMEOUT
|
UPLOAD_TIMEOUT
|
||||||
} from '../Defaults'
|
} from '../Defaults'
|
||||||
import { cleanupQueues } from '../Signal/Group/queue-job'
|
|
||||||
import type { SocketConfig } from '../Types'
|
import type { SocketConfig } from '../Types'
|
||||||
import { DisconnectReason } from '../Types'
|
import { DisconnectReason } from '../Types'
|
||||||
import {
|
import {
|
||||||
@@ -454,8 +453,6 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupQueues()
|
|
||||||
|
|
||||||
closed = true
|
closed = true
|
||||||
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed')
|
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed')
|
||||||
|
|
||||||
|
|||||||
@@ -329,4 +329,3 @@ function isSessionRecordError(error: any): boolean {
|
|||||||
const errorMessage = error?.message || error?.toString() || ''
|
const errorMessage = error?.message || error?.toString() || ''
|
||||||
return DECRYPTION_RETRY_CONFIG.sessionRecordErrors.some(errorPattern => errorMessage.includes(errorPattern))
|
return DECRYPTION_RETRY_CONFIG.sessionRecordErrors.some(errorPattern => errorMessage.includes(errorPattern))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user