From 6c52b01ea70a5860596fbf91fed1d1f5a0509c63 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Thu, 19 Mar 2026 16:44:14 -0300 Subject: [PATCH] fix: add pastParticipants to HistorySync with LID normalization and deduplication (#303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs fixed compared to prior implementation: 1. history.ts — normalize userJid LID→PN pastParticipants[].userJid may arrive as a LID identifier (e.g. "46802258641027@lid"). The consumer expects a phone number. After building the lidPnMap from conversations and phoneNumberToLidMappings, each userJid is resolved to its PN equivalent. If the mapping is not available the original value is preserved (no data loss). 2. event-buffer.ts — deduplicate by groupJid + userJid across chunks Multiple HistorySync chunks can contain the same group. The previous approach concatenated blindly, producing duplicate entries. Now a keyed map { [groupJid]: IPastParticipant[] } is used so each group appears once and each participant within a group appears at most once. Types updated (Events.ts): - messaging-history.set event includes pastParticipants?: IPastParticipants[] - BufferedEventData.historySets.pastParticipants typed as keyed map --- src/Types/Events.ts | 4 ++++ src/Utils/event-buffer.ts | 28 ++++++++++++++++++++++++++++ src/Utils/history.ts | 15 +++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/src/Types/Events.ts b/src/Types/Events.ts index d97320c0..96a14096 100644 --- a/src/Types/Events.ts +++ b/src/Types/Events.ts @@ -27,6 +27,8 @@ export type BaileysEventMap = { chats: Chat[] contacts: Contact[] messages: WAMessage[] + /** Past participants for group chats (people who left/were removed). userJid is always a phone number (PN), never a LID. */ + pastParticipants?: proto.IPastParticipants[] | null isLatest?: boolean progress?: number | null syncType?: proto.HistorySync.HistorySyncType | null @@ -185,6 +187,8 @@ export type BufferedEventData = { chats: { [jid: string]: Chat } contacts: { [jid: string]: Contact } messages: { [uqId: string]: WAMessage } + /** Keyed by groupJid for O(1) deduplication across chunks */ + pastParticipants: { [groupJid: string]: proto.IPastParticipant[] } empty: boolean isLatest: boolean progress?: number | null diff --git a/src/Utils/event-buffer.ts b/src/Utils/event-buffer.ts index 1b19a3e8..027d8798 100644 --- a/src/Utils/event-buffer.ts +++ b/src/Utils/event-buffer.ts @@ -1,4 +1,5 @@ import EventEmitter from 'events' +import { proto } from '../../WAProto/index.js' import type { BaileysEvent, BaileysEventEmitter, @@ -890,6 +891,7 @@ const makeBufferData = (): BufferedEventData => { chats: {}, messages: {}, contacts: {}, + pastParticipants: {}, isLatest: false, empty: true }, @@ -967,6 +969,28 @@ function append( } } + // Merge pastParticipants with deduplication by groupJid and by userJid within each group. + // Multiple HistorySync chunks can carry the same group -- we merge rather than concatenate + // to avoid duplicate entries in the final event delivered to the consumer. + for (const group of (eventData.pastParticipants ?? []) as proto.IPastParticipants[]) { + const groupJid = group.groupJid + if (!groupJid) continue + + if (!data.historySets.pastParticipants[groupJid]) { + data.historySets.pastParticipants[groupJid] = [] + } + + const existing = data.historySets.pastParticipants[groupJid] + const seenJids = new Set(existing.map(p => p.userJid).filter(Boolean)) + + for (const participant of (group.pastParticipants ?? []) as proto.IPastParticipant[]) { + if (participant.userJid && !seenJids.has(participant.userJid)) { + existing.push(participant) + seenJids.add(participant.userJid) + } + } + } + data.historySets.empty = false data.historySets.syncType = eventData.syncType data.historySets.progress = eventData.progress @@ -1292,6 +1316,10 @@ function consolidateEvents(data: BufferedEventData) { chats: Object.values(data.historySets.chats), messages: Object.values(data.historySets.messages), contacts: Object.values(data.historySets.contacts), + // Convert dedup map back to array. Each entry has groupJid + participants (all unique userJids). + pastParticipants: Object.entries(data.historySets.pastParticipants).map( + ([groupJid, participants]) => ({ groupJid, pastParticipants: participants }) + ), syncType: data.historySets.syncType, progress: data.historySets.progress, isLatest: data.historySets.isLatest, diff --git a/src/Utils/history.ts b/src/Utils/history.ts index 6442abf2..58ff7bb4 100644 --- a/src/Utils/history.ts +++ b/src/Utils/history.ts @@ -374,10 +374,25 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger // Convert Map back to array for return const lidPnMappings = Array.from(lidPnMap.values()) + // Normalize pastParticipants: resolve LID userJids → PN so the consumer always + // receives a phone number, never an opaque LID identifier. + // Uses the lidPnMap built above (populated from phoneNumberToLidMappings + conversations). + // If a LID cannot be resolved, the original value is kept rather than dropping the participant. + const pastParticipants: proto.IPastParticipants[] = (item.pastParticipants ?? []).map(group => ({ + groupJid: group.groupJid, + pastParticipants: (group.pastParticipants ?? []).map(participant => { + const userJid = participant.userJid + if (!userJid || !isAnyLidUser(userJid)) return participant + const mapping = lidPnMap.get(jidNormalizedUser(userJid)) + return mapping?.pn ? { ...participant, userJid: mapping.pn } : participant + }) + })) + return { chats, contacts, messages, + pastParticipants, lidPnMappings, syncType: item.syncType, progress: item.progress