Files
InfiniteAPI/src/Utils/history.ts
T
Claude 4b02652369 style: auto-fix lint errors and formatting issues
Applied yarn lint --fix to auto-correct:
- Import spacing and sorting across multiple files
- Trailing commas
- Arrow function formatting
- Object literal formatting
- Indentation consistency
- Removed unused imports (BaileysEventType, jest from tests)

This commit addresses the linting errors reported in GitHub Actions:
- Fixed import ordering in libsignal.ts
- Fixed trailing commas in Defaults/index.ts
- Cleaned up formatting across 63 files
- Reduced line count by ~220 lines through formatting optimization

Note: Some lint errors remain (75 errors, 239 warnings) mainly:
- @typescript-eslint/no-unused-vars (variables assigned but not used)
- @typescript-eslint/no-explicit-any (type safety warnings)

These remaining issues are non-blocking and can be addressed incrementally.

https://claude.ai/code/session_015R3U3kiprQiNTTNNt31Sg6
2026-02-13 21:59:35 +00:00

426 lines
13 KiB
TypeScript

import { promisify } from 'util'
import { inflate } from 'zlib'
import { proto } from '../../WAProto/index.js'
import type { Chat, Contact, LIDMapping, WAMessage } from '../Types'
import { WAMessageStubType } from '../Types'
import {
isAnyLidUser,
isAnyPnUser,
isJidBroadcast,
isJidGroup,
isJidNewsletter,
jidDecode,
jidNormalizedUser
} from '../WABinary/index.js'
import { toNumber } from './generics'
import type { ILogger } from './logger.js'
import { normalizeMessageContent } from './messages'
import { downloadContentFromMessage } from './messages-media'
const inflatePromise = promisify(inflate)
/**
* Downloads and decompresses history sync data from WhatsApp servers.
*
* @param msg - The history sync notification message containing download info
* @param options - Request options for the download
* @returns Decoded HistorySync protocol buffer
*/
export const downloadHistory = async (msg: proto.Message.IHistorySyncNotification, options: RequestInit) => {
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options })
const bufferArray: Buffer[] = []
for await (const chunk of stream) {
bufferArray.push(chunk)
}
let buffer: Buffer = Buffer.concat(bufferArray)
// decompress buffer
buffer = await inflatePromise(buffer)
const syncData = proto.HistorySync.decode(buffer)
return syncData
}
/**
* Checks if a JID represents a person (can have LID-PN mapping).
* Excludes groups, broadcasts, newsletters, and bots.
*
* @param jid - The JID to check
* @returns true if the JID can have LID-PN mapping
*/
export function isPersonJid(jid: string | undefined): boolean {
if (!jid) {
return false
}
// Groups, broadcasts, and newsletters don't have LID-PN mappings
if (isJidGroup(jid) || isJidBroadcast(jid) || isJidNewsletter(jid)) {
return false
}
// Only person JIDs (LID or PN formats) can have mappings
return isAnyLidUser(jid) || isAnyPnUser(jid)
}
/**
* Extracts LID-PN mapping from a conversation object.
*
* WhatsApp uses two identifier systems:
* - LID (Logical ID): Format `{number}@lid` or `{number}@hosted.lid`
* - PN (Phone Number): Format `{number}@s.whatsapp.net` or `{number}@hosted`
*
* Conversations may have their ID in either format, with the alternate
* format stored in `lidJid` or `pnJid` properties respectively.
*
* Skips non-person JIDs:
* - `@g.us` (groups)
* - `@broadcast` (broadcast lists)
* - `@newsletter` (channels)
*
* @param chatId - The conversation ID (may be LID or PN format)
* @param lidJid - The LID JID if chat ID is PN format
* @param pnJid - The PN JID if chat ID is LID format
* @returns LID-PN mapping if extractable, undefined otherwise
*
* @example
* // Chat ID is LID, pnJid contains phone number
* extractLidPnFromConversation('123456789@lid', undefined, '5511999999999@s.whatsapp.net')
* // Returns: { lid: '123456789@lid', pn: '5511999999999@s.whatsapp.net' }
*
* @example
* // Chat ID is PN, lidJid contains LID
* extractLidPnFromConversation('5511999999999@s.whatsapp.net', '123456789@lid', undefined)
* // Returns: { lid: '123456789@lid', pn: '5511999999999@s.whatsapp.net' }
*
* @example
* // Newsletter - returns undefined (no mapping)
* extractLidPnFromConversation('123456789@newsletter', undefined, undefined)
* // Returns: undefined
*/
export function extractLidPnFromConversation(
chatId: string,
lidJid: string | undefined | null,
pnJid: string | undefined | null
): LIDMapping | undefined {
// Skip non-person JIDs (groups, broadcasts, newsletters)
if (!isPersonJid(chatId)) {
return undefined
}
// Check if chat ID is in LID format
const chatIsLid = isAnyLidUser(chatId)
// Check if chat ID is in PN format
const chatIsPn = isAnyPnUser(chatId)
if (chatIsLid && pnJid) {
// Chat ID is LID, pnJid contains the phone number
return {
lid: jidNormalizedUser(chatId),
pn: jidNormalizedUser(pnJid)
}
}
if (chatIsPn && lidJid) {
// Chat ID is PN, lidJid contains the LID
return {
lid: jidNormalizedUser(lidJid),
pn: jidNormalizedUser(chatId)
}
}
return undefined
}
/**
* Extracts LID-PN mapping from a message's alternative JID fields.
*
* Messages may contain alternate JID formats in:
* - `key.remoteJidAlt` - Alternative remote JID format
* - `key.participantAlt` - Alternative participant JID format (for groups)
*
* IMPORTANT: Uses || (OR) to ensure BOTH JIDs are person JIDs before extracting.
* This prevents "poisoned" mappings where one side is a group/newsletter/broadcast.
*
* @param remoteJid - The primary remote JID
* @param remoteJidAlt - The alternative remote JID (may be LID or PN)
* @param participant - The primary participant JID (for group messages)
* @param participantAlt - The alternative participant JID
* @returns LID-PN mapping if extractable, undefined otherwise
*/
export function extractLidPnFromMessage(
remoteJid: string | undefined | null,
remoteJidAlt: string | undefined | null,
participant: string | undefined | null,
participantAlt: string | undefined | null
): LIDMapping | undefined {
// For group messages, use participant fields
const primaryJid = participant || remoteJid
const altJid = participantAlt || remoteJidAlt
if (!primaryJid || !altJid) {
return undefined
}
// FIXED: Use || (OR) instead of && (AND)
// Both JIDs MUST be person JIDs to create a valid mapping.
// Using && would allow mixed scenarios (e.g., person + group) to proceed,
// resulting in invalid "poisoned" mappings.
if (!isPersonJid(primaryJid) || !isPersonJid(altJid)) {
return undefined
}
const primaryDecoded = jidDecode(primaryJid)
const altDecoded = jidDecode(altJid)
if (!primaryDecoded || !altDecoded) {
return undefined
}
// Determine which is LID and which is PN
const primaryIsLid = primaryDecoded.server === 'lid' || primaryDecoded.server === 'hosted.lid'
const altIsLid = altDecoded.server === 'lid' || altDecoded.server === 'hosted.lid'
if (primaryIsLid && !altIsLid) {
// Primary is LID, alt is PN
return {
lid: jidNormalizedUser(primaryJid),
pn: jidNormalizedUser(altJid)
}
}
if (!primaryIsLid && altIsLid) {
// Primary is PN, alt is LID
return {
lid: jidNormalizedUser(altJid),
pn: jidNormalizedUser(primaryJid)
}
}
return undefined
}
/**
* Extracts phone number (PN) from message userReceipt fields.
*
* This is a fallback mechanism for LID chats that don't have a pnJid property.
* It looks at outgoing messages (fromMe: true) and extracts the recipient's
* phone number from the userReceipt.userJid field.
*
* @param messages - Array of history sync messages from a conversation
* @returns The extracted phone number JID, or undefined if not found
*
* @see https://github.com/WhiskeySockets/Baileys/pull/2282
*/
const extractPnFromMessages = (messages: proto.IHistorySyncMsg[]): string | undefined => {
for (const msgItem of messages) {
const message = msgItem.message
// Only extract from outgoing messages (fromMe: true) in 1:1 chats
// because userReceipt.userJid is the recipient's JID
if (!message?.key?.fromMe || !message.userReceipt?.length) {
continue
}
const userJid = message.userReceipt[0]?.userJid
if (userJid && isAnyPnUser(userJid)) {
return userJid
}
}
return undefined
}
/**
* Processes a history sync message and extracts chats, contacts, messages,
* and LID-PN mappings.
*
* LID-PN mappings are extracted from three sources:
* 1. Top-level `phoneNumberToLidMappings` array in the history sync payload
* 2. Individual conversation objects that contain both LID and PN identifiers
* (via `lidJid` and `pnJid` properties)
* 3. Message objects with alternate JID fields (`remoteJidAlt`, `participantAlt`)
*
* This multi-source extraction ensures maximum mapping coverage, as WhatsApp may
* provide mappings in different locations depending on the sync type and context.
*
* Skipped JID types (no LID-PN mapping):
* - `@g.us` (groups)
* - `@broadcast` (broadcast lists)
* - `@newsletter` (channels)
*
* @param item - The history sync protocol buffer to process
* @param logger - Optional logger instance for trace-level debugging
* @returns Processed data including chats, contacts, messages, and LID-PN mappings
*
* @see https://github.com/WhiskeySockets/Baileys/issues/2263
*/
export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger) => {
logger?.trace({ syncType: item.syncType, progress: item.progress }, 'processing history sync')
const messages: WAMessage[] = []
const contacts: Contact[] = []
const chats: Chat[] = []
// Use Map for O(1) deduplication of LID-PN mappings
const lidPnMap = new Map<string, LIDMapping>()
/**
* Adds a LID-PN mapping to the map with deduplication.
* Uses LID as key since each LID should map to exactly one PN.
*/
const addLidPnMapping = (mapping: LIDMapping): void => {
// Normalize and validate
if (!mapping.lid || !mapping.pn) {
return
}
lidPnMap.set(mapping.lid, mapping)
}
// Source 1: Extract from top-level phoneNumberToLidMappings array
for (const m of item.phoneNumberToLidMappings || []) {
if (m.lidJid && m.pnJid) {
addLidPnMapping({
lid: jidNormalizedUser(m.lidJid),
pn: jidNormalizedUser(m.pnJid)
})
}
}
switch (item.syncType) {
case proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP:
case proto.HistorySync.HistorySyncType.RECENT:
case proto.HistorySync.HistorySyncType.FULL:
case proto.HistorySync.HistorySyncType.ON_DEMAND:
for (const chat of (item.conversations ?? []) as Chat[]) {
const chatId = chat.id!
// Source 2: Extract LID-PN mapping from conversation object
// This handles cases where the mapping isn't in phoneNumberToLidMappings
const conversationMapping = extractLidPnFromConversation(chatId, chat.lidJid, chat.pnJid)
if (conversationMapping) {
addLidPnMapping(conversationMapping)
} else if (isAnyLidUser(chatId) && !chat.pnJid) {
// Source 2b: Fallback - extract PN from userReceipt in messages when pnJid is missing
// This handles edge cases where the conversation is LID but pnJid wasn't provided
const pnFromReceipt = extractPnFromMessages(chat.messages || [])
if (pnFromReceipt) {
addLidPnMapping({
lid: jidNormalizedUser(chatId),
pn: jidNormalizedUser(pnFromReceipt)
})
}
}
contacts.push({
id: chatId,
name: chat.displayName || chat.name || chat.username || undefined,
lid: chat.lidJid || chat.accountLid || undefined,
phoneNumber: chat.pnJid || undefined
})
const msgs = chat.messages || []
delete chat.messages
for (const item of msgs) {
if (!item.message) continue
const message = item.message as WAMessage
messages.push(message)
// Source 3: Extract LID-PN mapping from message's alternative JID fields
// Messages may have remoteJidAlt or participantAlt with alternate format
const messageMapping = extractLidPnFromMessage(
message.key.remoteJid,
message.key.remoteJidAlt,
message.key.participant,
message.key.participantAlt
)
if (messageMapping) {
addLidPnMapping(messageMapping)
}
if (!chat.messages?.length) {
// keep only the most recent message in the chat array
chat.messages = [{ message }]
}
if (!message.key.fromMe && !chat.lastMessageRecvTimestamp) {
chat.lastMessageRecvTimestamp = toNumber(message.messageTimestamp)
}
if (
(message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_BSP ||
message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_FB) &&
message.messageStubParameters?.[0]
) {
contacts.push({
id: message.key.participant || message.key.remoteJid || '',
verifiedName: message.messageStubParameters?.[0]
})
}
}
chats.push({ ...chat })
}
break
case proto.HistorySync.HistorySyncType.PUSH_NAME:
for (const c of item.pushnames ?? []) {
contacts.push({ id: c.id!, notify: c.pushname! })
}
break
}
// Convert Map back to array for return
const lidPnMappings = Array.from(lidPnMap.values())
return {
chats,
contacts,
messages,
lidPnMappings,
syncType: item.syncType,
progress: item.progress
}
}
/**
* Downloads and processes a history sync notification in one step.
*
* Handles two cases:
* - Inline payload: Decodes directly from `initialHistBootstrapInlinePayload`
* - Remote download: Fetches from WhatsApp servers via `downloadHistory`
*
* @param msg - The history sync notification message
* @param options - Request options for the download
* @param logger - Optional logger instance for trace-level debugging
* @returns Processed history data including chats, contacts, messages, and LID-PN mappings
*/
export const downloadAndProcessHistorySyncNotification = async (
msg: proto.Message.IHistorySyncNotification,
options: RequestInit,
logger?: ILogger
) => {
let historyMsg: proto.HistorySync
if (msg.initialHistBootstrapInlinePayload) {
historyMsg = proto.HistorySync.decode(await inflatePromise(msg.initialHistBootstrapInlinePayload))
} else {
historyMsg = await downloadHistory(msg, options)
}
return processHistoryMessage(historyMsg, logger)
}
/**
* Extracts the history sync notification from a protocol message.
*
* @param message - The protocol message to check
* @returns The history sync notification if present, undefined otherwise
*/
export const getHistoryMsg = (message: proto.IMessage) => {
const normalizedContent = !!message ? normalizeMessageContent(message) : undefined
const anyHistoryMsg = normalizedContent?.protocolMessage?.historySyncNotification
return anyHistoryMsg
}