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