@zervonex/baileys 0.0.0-stage → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3203 -2
  3. package/WAProto/WAProtoCompile.js +291 -0
  4. package/WAProto/fetcher.js +69 -0
  5. package/WAProto/index.js +88 -0
  6. package/WAProto/parser.js +313 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Builders/AIRich.js +590 -0
  9. package/lib/Builders/base.js +130 -0
  10. package/lib/Builders/builders.js +448 -0
  11. package/lib/Builders/index.js +3 -0
  12. package/lib/Defaults/index.js +225 -0
  13. package/lib/Defaults/phonenumber-mcc.json +223 -0
  14. package/lib/Signal/index.js +2 -0
  15. package/lib/Signal/libsignal.js +523 -0
  16. package/lib/Signal/lid-mapping.js +283 -0
  17. package/lib/Socket/Client/index.js +2 -0
  18. package/lib/Socket/Client/types.js +10 -0
  19. package/lib/Socket/Client/websocket.js +84 -0
  20. package/lib/Socket/business.js +152 -0
  21. package/lib/Socket/chats.js +855 -0
  22. package/lib/Socket/communities.js +147 -0
  23. package/lib/Socket/groups.js +294 -0
  24. package/lib/Socket/index.js +15 -0
  25. package/lib/Socket/messages-recv.js +904 -0
  26. package/lib/Socket/messages-send.js +693 -0
  27. package/lib/Socket/mex.js +34 -0
  28. package/lib/Socket/newsletter.js +752 -0
  29. package/lib/Socket/nexus-handler.js +55 -0
  30. package/lib/Socket/registration.js +155 -0
  31. package/lib/Socket/socket.js +798 -0
  32. package/lib/Socket/spam-report.js +65 -0
  33. package/lib/Socket/username.js +82 -0
  34. package/lib/Store/index.js +6 -0
  35. package/lib/Store/make-cache-manager-store.js +81 -0
  36. package/lib/Store/make-in-memory-store.js +425 -0
  37. package/lib/Store/make-ordered-dictionary.js +82 -0
  38. package/lib/Store/object-repository.js +31 -0
  39. package/lib/Store/reporting-info-store.js +38 -0
  40. package/lib/Types/Auth.js +2 -0
  41. package/lib/Types/Bussines.js +2 -0
  42. package/lib/Types/Call.js +2 -0
  43. package/lib/Types/Chat.js +8 -0
  44. package/lib/Types/Contact.js +2 -0
  45. package/lib/Types/Events.js +2 -0
  46. package/lib/Types/GroupMetadata.js +2 -0
  47. package/lib/Types/Label.js +25 -0
  48. package/lib/Types/LabelAssociation.js +7 -0
  49. package/lib/Types/Message.js +15 -0
  50. package/lib/Types/Newsletter.js +100 -0
  51. package/lib/Types/Product.js +2 -0
  52. package/lib/Types/Signal.js +2 -0
  53. package/lib/Types/Socket.js +14 -0
  54. package/lib/Types/State.js +26 -0
  55. package/lib/Types/USync.js +2 -0
  56. package/lib/Types/index.js +30 -0
  57. package/lib/Utils/auth-utils.js +142 -0
  58. package/lib/Utils/baileys-event-stream.js +33 -0
  59. package/lib/Utils/bot-capabilities.js +40 -0
  60. package/lib/Utils/bot-planning-replay.js +44 -0
  61. package/lib/Utils/browser-utils.js +26 -0
  62. package/lib/Utils/business.js +75 -0
  63. package/lib/Utils/chat-utils.js +316 -0
  64. package/lib/Utils/companion-reg-client-utils.js +20 -0
  65. package/lib/Utils/consumer-application.js +38 -0
  66. package/lib/Utils/crypto.js +52 -0
  67. package/lib/Utils/decode-wa-message.js +197 -0
  68. package/lib/Utils/event-buffer.js +273 -0
  69. package/lib/Utils/extras.js +186 -0
  70. package/lib/Utils/generics.js +195 -0
  71. package/lib/Utils/history.js +51 -0
  72. package/lib/Utils/identity-change-handler.js +19 -0
  73. package/lib/Utils/index.js +39 -0
  74. package/lib/Utils/key-store.js +3 -0
  75. package/lib/Utils/link-preview.js +51 -0
  76. package/lib/Utils/logger.js +2 -0
  77. package/lib/Utils/lt-hash.js +3 -0
  78. package/lib/Utils/make-mutex.js +16 -0
  79. package/lib/Utils/message-composer.js +59 -0
  80. package/lib/Utils/message-retry-manager.js +48 -0
  81. package/lib/Utils/messages-media.js +520 -0
  82. package/lib/Utils/messages.js +742 -0
  83. package/lib/Utils/meta-ai-msmsg.js +122 -0
  84. package/lib/Utils/meta-compositing.js +75 -0
  85. package/lib/Utils/modern-messages.js +235 -0
  86. package/lib/Utils/native-flow.js +55 -0
  87. package/lib/Utils/noise-handler.js +161 -0
  88. package/lib/Utils/pre-key-manager.js +76 -0
  89. package/lib/Utils/process-message.js +399 -0
  90. package/lib/Utils/reporting-utils.js +171 -0
  91. package/lib/Utils/signal.js +167 -0
  92. package/lib/Utils/spam-report-utils.js +142 -0
  93. package/lib/Utils/sync-action-utils.js +33 -0
  94. package/lib/Utils/tc-token-utils.js +102 -0
  95. package/lib/Utils/use-multi-file-auth-state.js +307 -0
  96. package/lib/Utils/validate-connection.js +181 -0
  97. package/lib/WABinary/constants.js +1321 -0
  98. package/lib/WABinary/decode.js +152 -0
  99. package/lib/WABinary/encode.js +140 -0
  100. package/lib/WABinary/generic-utils.js +179 -0
  101. package/lib/WABinary/index.js +6 -0
  102. package/lib/WABinary/jid-utils.js +235 -0
  103. package/lib/WABinary/types.js +4 -0
  104. package/lib/WAM/BinaryInfo.js +10 -0
  105. package/lib/WAM/constants.js +22853 -0
  106. package/lib/WAM/encode.js +150 -0
  107. package/lib/WAM/index.js +3 -0
  108. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
  109. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
  110. package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
  111. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
  112. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
  113. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
  114. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
  115. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
  116. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
  117. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
  118. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
  119. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
  120. package/lib/WAUSync/Protocols/index.js +12 -0
  121. package/lib/WAUSync/USyncQuery.js +100 -0
  122. package/lib/WAUSync/USyncUser.js +20 -0
  123. package/lib/WAUSync/index.js +3 -0
  124. package/lib/index.js +68 -0
  125. package/package.json +216 -5
@@ -0,0 +1,197 @@
1
+ import { Boom } from '@hapi/boom'
2
+ import { proto } from '../../WAProto/index.js'
3
+ import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidGroup, isJidMetaAI, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser } from '../WABinary/index.js'
4
+ import { unpadRandomMax16 } from './generics.js'
5
+
6
+ export const NO_MESSAGE_FOUND_ERROR_TEXT = 'Message absent from node'
7
+ export const MISSING_KEYS_ERROR_TEXT = 'Key used already or never filled'
8
+ export const ACCOUNT_RESTRICTED_TEXT = 'Your account has been restricted'
9
+
10
+ export const DECRYPTION_RETRY_CONFIG = { maxRetries: 3, baseDelayMs: 100, sessionRecordErrors: ['No session record', 'SessionError: No session record'] }
11
+
12
+ export const NACK_REASONS = {
13
+ ParsingError: 487, UnrecognizedStanza: 488, UnrecognizedStanzaClass: 489, UnrecognizedStanzaType: 490,
14
+ InvalidProtobuf: 491, InvalidHostedCompanionStanza: 493, MissingMessageSecret: 495,
15
+ SignalErrorOldCounter: 496, MessageDeletedOnPeer: 499, UnhandledError: 500,
16
+ UnsupportedAdminRevoke: 550, UnsupportedLIDGroup: 551, DBOperationFailed: 552
17
+ }
18
+
19
+ export const SERVER_ERROR_CODES = { MessageAccountRestriction: '463', SmaxInvalid: '479' }
20
+
21
+ export const getDecryptionJid = async (sender, repository, logger) => {
22
+ if (isJidNewsletter(sender) || isJidStatusBroadcast(sender) || isJidMetaAI(sender)) return sender
23
+ if (isLidUser(sender) || isHostedLidUser(sender)) return sender
24
+ return (await repository.lidMapping.getLIDForPN(sender)) || sender
25
+ }
26
+
27
+ const storeMappingFromEnvelope = async (stanza, sender, repository, decryptionJid, logger, meId, meLid) => {
28
+ const { senderAlt } = extractAddressingContext(stanza)
29
+ if (!senderAlt) return
30
+ if (isLidUser(senderAlt) && isPnUser(sender) && decryptionJid === sender) {
31
+ if (areJidsSameUser(sender, meId) || areJidsSameUser(senderAlt, meLid)) return
32
+ try {
33
+ await repository.lidMapping.storeLIDPNMappings([{ lid: senderAlt, pn: sender }])
34
+ repository.migrateSession(sender, senderAlt).catch(err => logger?.warn?.({ sender, senderAlt, err }, 'Failed to migrate session (PN→LID)'))
35
+ logger.debug({ sender, senderAlt }, 'Stored LID mapping from envelope (PN→LID)')
36
+ } catch (error) { logger.warn({ sender, senderAlt, error }, 'Failed to store LID mapping (PN→LID)') }
37
+ } else if (isPnUser(senderAlt) && isLidUser(sender)) {
38
+ if (areJidsSameUser(sender, meLid) || areJidsSameUser(senderAlt, meId)) return
39
+ try {
40
+ await repository.lidMapping.storeLIDPNMappings([{ lid: sender, pn: senderAlt }])
41
+ repository.migrateSession(senderAlt, sender).catch(err => logger?.warn?.({ sender, senderAlt, err }, 'Failed to migrate session (LID→PN)'))
42
+ logger.debug({ sender, senderAlt }, 'Stored LID mapping from envelope (LID→PN)')
43
+ } catch (error) { logger.warn({ sender, senderAlt, error }, 'Failed to store LID mapping (LID→PN)') }
44
+ }
45
+ }
46
+
47
+ export const extractAddressingContext = stanza => {
48
+ const sender = stanza.attrs.participant || stanza.attrs.from
49
+ const addressingMode = stanza.attrs.addressing_mode || (sender?.endsWith('lid') ? 'lid' : 'pn')
50
+ let senderAlt, recipientAlt
51
+ if (addressingMode === 'lid') {
52
+ senderAlt = stanza.attrs.participant_pn || stanza.attrs.sender_pn || stanza.attrs.peer_recipient_pn
53
+ recipientAlt = stanza.attrs.recipient_pn
54
+ } else {
55
+ senderAlt = stanza.attrs.participant_lid || stanza.attrs.sender_lid || stanza.attrs.peer_recipient_lid
56
+ recipientAlt = stanza.attrs.recipient_lid
57
+ }
58
+ return { addressingMode, senderAlt, recipientAlt }
59
+ }
60
+
61
+ const findChildByTag = (node, tag) => Array.isArray(node?.content) ? node.content.find(c => c?.tag === tag) : undefined
62
+
63
+ export const extractNewsletterMessageMeta = stanza => {
64
+ const meta = findChildByTag(stanza, 'meta')
65
+ if (!meta) return undefined
66
+ const result = {}
67
+ const adminProfile = findChildByTag(meta, 'admin_profile')
68
+ if (adminProfile) {
69
+ const name = findChildByTag(adminProfile, 'name')
70
+ const picture = findChildByTag(adminProfile, 'picture')
71
+ const content = name?.content
72
+ result.adminProfile = {
73
+ id: adminProfile.attrs?.id,
74
+ name: typeof content === 'string' ? content : content instanceof Uint8Array ? Buffer.from(content).toString('utf-8') : undefined,
75
+ pictureId: picture?.attrs?.id,
76
+ pictureDirectPath: picture?.attrs?.direct_path
77
+ }
78
+ }
79
+ if (findChildByTag(meta, 'paid_partnership')) result.paidPartnership = true
80
+ if (findChildByTag(meta, 'ai_content')) result.aiContent = true
81
+ return result
82
+ }
83
+
84
+ export const decodeMessageNode = (stanza, meId, meLid) => {
85
+ let msgType, chatId, author, fromMe = false
86
+ const { id: msgId, from, participant, recipient } = stanza.attrs
87
+ const addressingContext = extractAddressingContext(stanza)
88
+ const isMe = jid => areJidsSameUser(jid, meId)
89
+ const isMeLid = jid => areJidsSameUser(jid, meLid)
90
+
91
+ if (isPnUser(from) || isLidUser(from) || isHostedLidUser(from) || isHostedPnUser(from)) {
92
+ if (isMe(from) || isMeLid(from)) fromMe = true
93
+ chatId = (recipient && !isJidMetaAI(recipient)) ? (fromMe ? recipient : (() => { throw new Boom('recipient present, but msg not from me', { data: stanza }) })()) : from
94
+ msgType = 'chat'; author = from
95
+ } else if (isJidGroup(from)) {
96
+ if (!participant) throw new Boom('No participant in group message')
97
+ if (isMe(participant) || isMeLid(participant)) fromMe = true
98
+ msgType = 'group'; author = participant; chatId = from
99
+ } else if (isJidBroadcast(from)) {
100
+ if (!participant) throw new Boom('No participant in group message')
101
+ const isParticipantMe = isMe(participant)
102
+ msgType = isJidStatusBroadcast(from) ? (isParticipantMe ? 'direct_peer_status' : 'other_status') : (isParticipantMe ? 'peer_broadcast' : 'other_broadcast')
103
+ fromMe = isParticipantMe; chatId = from; author = participant
104
+ } else if (isJidNewsletter(from)) {
105
+ msgType = 'newsletter'; chatId = from; author = from
106
+ if (isMe(from) || isMeLid(from)) fromMe = true
107
+ } else throw new Boom('Unknown message type', { data: stanza })
108
+
109
+ const key = {
110
+ remoteJid: chatId,
111
+ remoteJidAlt: !isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
112
+ fromMe, id: msgId, participant,
113
+ participantAlt: isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
114
+ addressingMode: addressingContext.addressingMode,
115
+ ...(msgType === 'newsletter' && stanza.attrs.server_id ? { server_id: stanza.attrs.server_id } : {})
116
+ }
117
+ const fullMessage = { key, messageTimestamp: +stanza.attrs.t, pushName: stanza.attrs?.notify, broadcast: isJidBroadcast(from) }
118
+ if (key.fromMe) fullMessage.status = proto.WebMessageInfo.Status.SERVER_ACK
119
+ return { fullMessage, author, sender: msgType === 'chat' ? author : chatId }
120
+ }
121
+
122
+ export const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
123
+ const { fullMessage, author, sender } = decodeMessageNode(stanza, meId, meLid)
124
+ return {
125
+ fullMessage,
126
+ category: stanza.attrs.category,
127
+ author,
128
+ async decrypt() {
129
+ let decryptables = 0
130
+ if (Array.isArray(stanza.content)) {
131
+ for (const { tag, attrs, content } of stanza.content) {
132
+ if (tag === 'verified_name' && content instanceof Uint8Array) {
133
+ const cert = proto.VerifiedNameCertificate.decode(content)
134
+ fullMessage.verifiedBizName = proto.VerifiedNameCertificate.Details.decode(cert.details).verifiedName
135
+ }
136
+ if (tag === 'unavailable' && attrs.type === 'view_once') fullMessage.key.isViewOnce = true
137
+ if (tag !== 'enc' && tag !== 'plaintext') continue
138
+ if (!(content instanceof Uint8Array)) continue
139
+ decryptables++
140
+ const decryptionJid = await getDecryptionJid(author, repository, logger)
141
+ const { senderAlt } = extractAddressingContext(stanza)
142
+ const decryptionAltJid = senderAlt ? await getDecryptionJid(senderAlt, repository, logger) : null
143
+ if (tag !== 'plaintext') storeMappingFromEnvelope(stanza, author, repository, decryptionJid, logger, meId, meLid).catch(err => logger?.warn?.({ err }, 'storeMappingFromEnvelope failed'))
144
+ try {
145
+ const e2eType = tag === 'plaintext' ? 'plaintext' : attrs.type
146
+ switch (e2eType) {
147
+ case 'skmsg':
148
+ try {
149
+ fullMessage._msgBuffer = await repository.decryptGroupMessage({ group: sender, authorJid: author, msg: content })
150
+ } catch (decryptErr) {
151
+ const errMsg = decryptErr?.message || decryptErr?.toString() || ''
152
+ if (errMsg.includes('memory access out of bounds')) {
153
+ console.error('[Signal] Stale sender key — group:', sender, 'author:', author, 'err:', errMsg)
154
+ await repository.deleteSenderKey(sender, author).catch(e => console.error('[Signal] Failed to delete sender key:', e))
155
+ }
156
+ throw decryptErr
157
+ }
158
+ break
159
+ case 'pkmsg':
160
+ case 'msg': {
161
+ const buf = await repository.decryptMessage({ jid: decryptionJid, type: e2eType, ciphertext: content })
162
+ if (buf === null) return // DuplicatedMessage — libsignal handled silently
163
+ fullMessage._msgBuffer = buf
164
+ break
165
+ }
166
+ case 'plaintext':
167
+ fullMessage._msgBuffer = content
168
+ break
169
+ case 'msmsg':
170
+ if (!stanza.attrs._msmsgDecrypted) throw new Error('msmsg type but no pre-decrypted message available')
171
+ { let msg = stanza.attrs._msmsgDecrypted; msg = msg.deviceSentMessage?.message || msg; fullMessage.message ? Object.assign(fullMessage.message, msg) : (fullMessage.message = msg) }
172
+ continue
173
+ default:
174
+ throw new Error(`Unknown e2e type: ${e2eType}`)
175
+ }
176
+ let msg = proto.Message.decode(e2eType !== 'plaintext' ? unpadRandomMax16(fullMessage._msgBuffer) : fullMessage._msgBuffer)
177
+ msg = msg.deviceSentMessage?.message || msg
178
+ if (msg.senderKeyDistributionMessage) {
179
+ await repository.processSenderKeyDistributionMessage({ authorJid: author, item: msg.senderKeyDistributionMessage })
180
+ .catch(err => logger.error({ key: fullMessage.key, err }, 'failed to process sender key distribution message'))
181
+ }
182
+ fullMessage.message ? Object.assign(fullMessage.message, msg) : (fullMessage.message = msg)
183
+ const viewOnceInner = msg?.viewOnceMessage?.message || msg?.viewOnceMessageV2?.message || msg?.viewOnceMessageV2Extension?.message
184
+ if (viewOnceInner?.imageMessage?.viewOnce || viewOnceInner?.videoMessage?.viewOnce || viewOnceInner?.audioMessage?.viewOnce) fullMessage.key.isViewOnce = true
185
+ } catch (err) {
186
+ const errStr = err?.message || (typeof err === 'string' ? err : '') || ''
187
+ const isExpected = errStr.includes('InvalidPreKeyId') || errStr.includes('SessionNotFound') || errStr.includes('InvalidMessage') || errStr.includes('no sender key state') || errStr.includes('memory access out of bounds') || errStr.includes('old counter') || errStr.includes('DuplicatedMessage') || errStr.includes('BadMac')
188
+ ; (isExpected ? logger?.debug?.bind(logger) : logger?.error?.bind(logger))?.({ key: fullMessage.key, err, errStr, messageType: tag === 'plaintext' ? 'plaintext' : attrs.type, sender, author }, 'failed to decrypt message')
189
+ fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
190
+ fullMessage.messageStubParameters = [errStr]
191
+ }
192
+ }
193
+ }
194
+ if (!decryptables) { fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT; fullMessage.messageStubParameters = [NO_MESSAGE_FOUND_ERROR_TEXT] }
195
+ }
196
+ }
197
+ }
@@ -0,0 +1,273 @@
1
+ import EventEmitter from 'events'
2
+ import { WAMessageStatus } from '../Types/index.js'
3
+ import { trimUndefined } from './generics.js'
4
+ import { updateMessageWithReaction, updateMessageWithReceipt } from './messages.js'
5
+ import { isRealMessage, shouldIncrementChatUnread } from './process-message.js'
6
+
7
+ const BUFFERABLE_EVENT = ['messaging-history.set', 'chats.upsert', 'chats.update', 'chats.delete', 'contacts.upsert', 'contacts.update', 'messages.upsert', 'messages.update', 'messages.delete', 'messages.reaction', 'message-receipt.update', 'groups.update']
8
+ const BUFFERABLE_EVENT_SET = new Set(BUFFERABLE_EVENT)
9
+
10
+ const BUFFER_TIMEOUT_MS = 30000
11
+ const MAX_HISTORY_CACHE_SIZE = 10000
12
+
13
+ const makeBufferData = () => ({
14
+ historySets: { chats: {}, messages: {}, contacts: {}, isLatest: false, empty: true },
15
+ chatUpserts: {}, chatUpdates: {}, chatDeletes: new Set(),
16
+ contactUpserts: {}, contactUpdates: {},
17
+ messageUpserts: {}, messageUpdates: {}, messageReactions: {}, messageDeletes: {}, messageReceipts: {},
18
+ groupUpdates: {}
19
+ })
20
+
21
+ const stringifyMessageKey = key => `${key.remoteJid},${key.id},${key.fromMe ? '1' : '0'}`
22
+
23
+ export const makeEventBuffer = logger => {
24
+ const ev = new EventEmitter()
25
+ const historyCache = new Set()
26
+ let data = makeBufferData()
27
+ let isBuffering = false
28
+ let bufferTimeout = null
29
+ let bufferCount = 0
30
+
31
+ ev.on('event', map => { for (const event in map) ev.emit(event, map[event]) })
32
+
33
+ function buffer() {
34
+ bufferCount++
35
+ if (isBuffering) return
36
+ logger.debug('Event buffer activated')
37
+ isBuffering = true
38
+ if (bufferTimeout) clearTimeout(bufferTimeout)
39
+ bufferTimeout = setTimeout(() => { if (isBuffering) { logger.warn('Buffer timeout reached, auto-flushing'); flush() } }, BUFFER_TIMEOUT_MS)
40
+ }
41
+
42
+ function flush() {
43
+ if (!isBuffering) return false
44
+ logger.debug({ bufferCount }, 'Flushing event buffer')
45
+ isBuffering = false
46
+ bufferCount = 0
47
+ if (bufferTimeout) { clearTimeout(bufferTimeout); bufferTimeout = null }
48
+ if (historyCache.size > MAX_HISTORY_CACHE_SIZE) { logger.debug({ cacheSize: historyCache.size }, 'Clearing history cache'); historyCache.clear() }
49
+ const newData = makeBufferData()
50
+ for (const update of Object.values(data.chatUpdates)) {
51
+ if (update.conditional) { newData.chatUpdates[update.id] = update; delete data.chatUpdates[update.id] }
52
+ }
53
+ const consolidated = consolidateEvents(data)
54
+ if (Object.keys(consolidated).length) ev.emit('event', consolidated)
55
+ data = newData
56
+ logger.trace({ conditionalChatUpdatesLeft: Object.keys(newData.chatUpdates).length }, 'released buffered events')
57
+ return true
58
+ }
59
+
60
+ return {
61
+ process(handler) {
62
+ ev.on('event', handler)
63
+ return () => ev.off('event', handler)
64
+ },
65
+ emit(event, evData) {
66
+ if (isBuffering && BUFFERABLE_EVENT_SET.has(event)) { append(data, historyCache, event, evData, logger); return true }
67
+ return ev.emit('event', { [event]: evData })
68
+ },
69
+ isBuffering: () => isBuffering,
70
+ buffer,
71
+ flush,
72
+ createBufferedFunction(work) {
73
+ return async (...args) => {
74
+ buffer()
75
+ try {
76
+ const result = await work(...args)
77
+ if (bufferCount === 1) setTimeout(() => { if (isBuffering && bufferCount === 1) flush() }, 100)
78
+ return result
79
+ } finally {
80
+ bufferCount = Math.max(0, bufferCount - 1)
81
+ if (bufferCount === 0) setTimeout(flush, 100)
82
+ }
83
+ }
84
+ },
85
+ on: (...args) => ev.on(...args),
86
+ off: (...args) => ev.off(...args),
87
+ listener: eventName => ev.listenerCount(eventName),
88
+ removeAllListeners: (...args) => ev.removeAllListeners(...args)
89
+ }
90
+ }
91
+
92
+ function append(data, historyCache, event, eventData, logger) {
93
+ function absorbingChatUpdate(existing) {
94
+ const chatId = existing.id || ''
95
+ const update = data.chatUpdates[chatId]
96
+ if (!update) return
97
+ const conditionMatches = update.conditional ? update.conditional(data) : true
98
+ if (conditionMatches) { delete update.conditional; logger.debug({ chatId }, 'absorbed chat update in existing chat'); Object.assign(existing, concatChats(update, existing)); delete data.chatUpdates[chatId] }
99
+ else if (conditionMatches === false) { logger.debug({ chatId }, 'chat update condition fail, removing'); delete data.chatUpdates[chatId] }
100
+ }
101
+
102
+ function decrementChatReadCounterIfMsgDidUnread(message) {
103
+ const chatId = message.key.remoteJid
104
+ const chat = data.chatUpdates[chatId] || data.chatUpserts[chatId]
105
+ if (isRealMessage(message) && shouldIncrementChatUnread(message) && typeof chat?.unreadCount === 'number' && chat.unreadCount > 0) {
106
+ logger.debug({ chatId: chat.id }, 'decrementing chat counter')
107
+ chat.unreadCount -= 1
108
+ if (chat.unreadCount === 0) delete chat.unreadCount
109
+ }
110
+ }
111
+
112
+ switch (event) {
113
+ case 'messaging-history.set':
114
+ for (const chat of eventData.chats) {
115
+ const id = chat.id || ''
116
+ const existing = data.historySets.chats[id]
117
+ if (existing) existing.endOfHistoryTransferType = chat.endOfHistoryTransferType
118
+ else if (!historyCache.has(id)) { data.historySets.chats[id] = chat; historyCache.add(id); absorbingChatUpdate(chat) }
119
+ }
120
+ for (const contact of eventData.contacts) {
121
+ const existing = data.historySets.contacts[contact.id]
122
+ if (existing) Object.assign(existing, trimUndefined(contact))
123
+ else {
124
+ const hid = `c:${contact.id}`
125
+ if (!historyCache.has(hid) || contact.notify || contact.name || contact.verifiedName) { data.historySets.contacts[contact.id] = contact; historyCache.add(hid) }
126
+ }
127
+ }
128
+ for (const message of eventData.messages) {
129
+ const key = stringifyMessageKey(message.key)
130
+ if (!data.historySets.messages[key] && !historyCache.has(key)) { data.historySets.messages[key] = message; historyCache.add(key) }
131
+ }
132
+ data.historySets.empty = false
133
+ data.historySets.syncType = eventData.syncType
134
+ data.historySets.progress = eventData.progress
135
+ data.historySets.peerDataRequestSessionId = eventData.peerDataRequestSessionId
136
+ data.historySets.isLatest = eventData.isLatest || data.historySets.isLatest
137
+ break
138
+ case 'chats.upsert':
139
+ for (const chat of eventData) {
140
+ const id = chat.id || ''
141
+ let upsert = data.chatUpserts[id]
142
+ if (id && !upsert) { upsert = data.historySets.chats[id]; if (upsert) logger.debug({ chatId: id }, 'absorbed chat upsert in chat set') }
143
+ upsert = upsert ? concatChats(upsert, chat) : chat
144
+ data.chatUpserts[id] = upsert
145
+ absorbingChatUpdate(upsert)
146
+ data.chatDeletes.delete(id)
147
+ }
148
+ break
149
+ case 'chats.update':
150
+ for (const update of eventData) {
151
+ const chatId = update.id
152
+ const conditionMatches = update.conditional ? update.conditional(data) : true
153
+ if (conditionMatches) {
154
+ delete update.conditional
155
+ const upsert = data.historySets.chats[chatId] || data.chatUpserts[chatId]
156
+ if (upsert) concatChats(upsert, update)
157
+ else data.chatUpdates[chatId] = concatChats(data.chatUpdates[chatId] || {}, update)
158
+ } else if (conditionMatches === undefined) data.chatUpdates[chatId] = update
159
+ data.chatDeletes.delete(chatId)
160
+ }
161
+ break
162
+ case 'chats.delete':
163
+ for (const chatId of eventData) {
164
+ data.chatDeletes.add(chatId)
165
+ delete data.chatUpdates[chatId]
166
+ delete data.chatUpserts[chatId]
167
+ delete data.historySets.chats[chatId]
168
+ }
169
+ break
170
+ case 'contacts.upsert':
171
+ for (const contact of eventData) {
172
+ let upsert = data.contactUpserts[contact.id]
173
+ if (!upsert) { upsert = data.historySets.contacts[contact.id]; if (upsert) logger.debug({ contactId: contact.id }, 'absorbed contact upsert in contact set') }
174
+ upsert = upsert ? Object.assign(upsert, trimUndefined(contact)) : contact
175
+ data.contactUpserts[contact.id] = upsert
176
+ if (data.contactUpdates[contact.id]) { Object.assign(data.contactUpdates[contact.id], trimUndefined(contact)); delete data.contactUpdates[contact.id] }
177
+ }
178
+ break
179
+ case 'contacts.update':
180
+ for (const update of eventData) {
181
+ const upsert = data.historySets.contacts[update.id] || data.contactUpserts[update.id]
182
+ if (upsert) Object.assign(upsert, update)
183
+ else data.contactUpdates[update.id] = Object.assign(data.contactUpdates[update.id] || {}, update)
184
+ }
185
+ break
186
+ case 'messages.upsert': {
187
+ const { messages, type } = eventData
188
+ for (const message of messages) {
189
+ const key = stringifyMessageKey(message.key)
190
+ let existing = data.messageUpserts[key]?.message
191
+ if (!existing) { existing = data.historySets.messages[key]; if (existing) logger.debug({ messageId: key }, 'absorbed message upsert in message set') }
192
+ if (existing) message.messageTimestamp = existing.messageTimestamp
193
+ if (data.messageUpdates[key]) { logger.debug('absorbed prior message update in message upsert'); Object.assign(message, data.messageUpdates[key].update); delete data.messageUpdates[key] }
194
+ if (data.historySets.messages[key]) data.historySets.messages[key] = message
195
+ else data.messageUpserts[key] = { message, type: type === 'notify' || data.messageUpserts[key]?.type === 'notify' ? 'notify' : type }
196
+ }
197
+ break
198
+ }
199
+ case 'messages.update':
200
+ for (const { key, update } of eventData) {
201
+ const keyStr = stringifyMessageKey(key)
202
+ const existing = data.historySets.messages[keyStr] || data.messageUpserts[keyStr]?.message
203
+ if (existing) {
204
+ Object.assign(existing, update)
205
+ if (update.status === WAMessageStatus.READ && !key.fromMe) decrementChatReadCounterIfMsgDidUnread(existing)
206
+ } else {
207
+ const msgUpdate = data.messageUpdates[keyStr] || { key, update: {} }
208
+ Object.assign(msgUpdate.update, update)
209
+ data.messageUpdates[keyStr] = msgUpdate
210
+ }
211
+ }
212
+ break
213
+ case 'messages.delete':
214
+ if ('keys' in eventData) {
215
+ for (const key of eventData.keys) {
216
+ const keyStr = stringifyMessageKey(key)
217
+ if (!data.messageDeletes[keyStr]) data.messageDeletes[keyStr] = key
218
+ delete data.messageUpserts[keyStr]
219
+ delete data.messageUpdates[keyStr]
220
+ }
221
+ }
222
+ break
223
+ case 'messages.reaction':
224
+ for (const { key, reaction } of eventData) {
225
+ const keyStr = stringifyMessageKey(key)
226
+ const existing = data.messageUpserts[keyStr]
227
+ if (existing) updateMessageWithReaction(existing.message, reaction)
228
+ else { data.messageReactions[keyStr] = data.messageReactions[keyStr] || { key, reactions: [] }; updateMessageWithReaction(data.messageReactions[keyStr], reaction) }
229
+ }
230
+ break
231
+ case 'message-receipt.update':
232
+ for (const { key, receipt } of eventData) {
233
+ const keyStr = stringifyMessageKey(key)
234
+ const existing = data.messageUpserts[keyStr]
235
+ if (existing) updateMessageWithReceipt(existing.message, receipt)
236
+ else { data.messageReceipts[keyStr] = data.messageReceipts[keyStr] || { key, userReceipt: [] }; updateMessageWithReceipt(data.messageReceipts[keyStr], receipt) }
237
+ }
238
+ break
239
+ case 'groups.update':
240
+ for (const update of eventData) {
241
+ if (!data.groupUpdates[update.id]) data.groupUpdates[update.id] = Object.assign(data.groupUpdates[update.id] || {}, update)
242
+ }
243
+ break
244
+ default:
245
+ throw new Error(`"${event}" cannot be buffered`)
246
+ }
247
+ }
248
+
249
+ function consolidateEvents(data) {
250
+ const map = {}
251
+ const add = (key, list) => { if (list.length) map[key] = list }
252
+ if (!data.historySets.empty) map['messaging-history.set'] = { chats: Object.values(data.historySets.chats), messages: Object.values(data.historySets.messages), contacts: Object.values(data.historySets.contacts), syncType: data.historySets.syncType, progress: data.historySets.progress, isLatest: data.historySets.isLatest, peerDataRequestSessionId: data.historySets.peerDataRequestSessionId }
253
+ add('chats.upsert', Object.values(data.chatUpserts))
254
+ add('chats.update', Object.values(data.chatUpdates))
255
+ add('chats.delete', Array.from(data.chatDeletes))
256
+ const messageUpsertList = Object.values(data.messageUpserts)
257
+ if (messageUpsertList.length) map['messages.upsert'] = { messages: messageUpsertList.map(m => m.message), type: messageUpsertList[0].type }
258
+ add('messages.update', Object.values(data.messageUpdates))
259
+ const messageDeleteList = Object.values(data.messageDeletes)
260
+ if (messageDeleteList.length) map['messages.delete'] = { keys: messageDeleteList }
261
+ add('messages.reaction', Object.values(data.messageReactions).flatMap(({ key, reactions }) => reactions.map(reaction => ({ key, reaction }))))
262
+ add('message-receipt.update', Object.values(data.messageReceipts).flatMap(({ key, userReceipt }) => userReceipt.map(receipt => ({ key, receipt }))))
263
+ add('contacts.upsert', Object.values(data.contactUpserts))
264
+ add('contacts.update', Object.values(data.contactUpdates))
265
+ add('groups.update', Object.values(data.groupUpdates))
266
+ return map
267
+ }
268
+
269
+ function concatChats(a, b) {
270
+ if (b.unreadCount === null && a.unreadCount < 0) { a.unreadCount = undefined; b.unreadCount = undefined }
271
+ if (typeof a.unreadCount === 'number' && typeof b.unreadCount === 'number') { b = { ...b }; if (b.unreadCount >= 0) b.unreadCount = Math.max(b.unreadCount, 0) + Math.max(a.unreadCount, 0) }
272
+ return Object.assign(a, b)
273
+ }