@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.
- package/LICENSE +21 -0
- package/README.md +3203 -2
- package/WAProto/WAProtoCompile.js +291 -0
- package/WAProto/fetcher.js +69 -0
- package/WAProto/index.js +88 -0
- package/WAProto/parser.js +313 -0
- package/engine-requirements.js +10 -0
- package/lib/Builders/AIRich.js +590 -0
- package/lib/Builders/base.js +130 -0
- package/lib/Builders/builders.js +448 -0
- package/lib/Builders/index.js +3 -0
- package/lib/Defaults/index.js +225 -0
- package/lib/Defaults/phonenumber-mcc.json +223 -0
- package/lib/Signal/index.js +2 -0
- package/lib/Signal/libsignal.js +523 -0
- package/lib/Signal/lid-mapping.js +283 -0
- package/lib/Socket/Client/index.js +2 -0
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.js +84 -0
- package/lib/Socket/business.js +152 -0
- package/lib/Socket/chats.js +855 -0
- package/lib/Socket/communities.js +147 -0
- package/lib/Socket/groups.js +294 -0
- package/lib/Socket/index.js +15 -0
- package/lib/Socket/messages-recv.js +904 -0
- package/lib/Socket/messages-send.js +693 -0
- package/lib/Socket/mex.js +34 -0
- package/lib/Socket/newsletter.js +752 -0
- package/lib/Socket/nexus-handler.js +55 -0
- package/lib/Socket/registration.js +155 -0
- package/lib/Socket/socket.js +798 -0
- package/lib/Socket/spam-report.js +65 -0
- package/lib/Socket/username.js +82 -0
- package/lib/Store/index.js +6 -0
- package/lib/Store/make-cache-manager-store.js +81 -0
- package/lib/Store/make-in-memory-store.js +425 -0
- package/lib/Store/make-ordered-dictionary.js +82 -0
- package/lib/Store/object-repository.js +31 -0
- package/lib/Store/reporting-info-store.js +38 -0
- package/lib/Types/Auth.js +2 -0
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Call.js +2 -0
- package/lib/Types/Chat.js +8 -0
- package/lib/Types/Contact.js +2 -0
- package/lib/Types/Events.js +2 -0
- package/lib/Types/GroupMetadata.js +2 -0
- package/lib/Types/Label.js +25 -0
- package/lib/Types/LabelAssociation.js +7 -0
- package/lib/Types/Message.js +15 -0
- package/lib/Types/Newsletter.js +100 -0
- package/lib/Types/Product.js +2 -0
- package/lib/Types/Signal.js +2 -0
- package/lib/Types/Socket.js +14 -0
- package/lib/Types/State.js +26 -0
- package/lib/Types/USync.js +2 -0
- package/lib/Types/index.js +30 -0
- package/lib/Utils/auth-utils.js +142 -0
- package/lib/Utils/baileys-event-stream.js +33 -0
- package/lib/Utils/bot-capabilities.js +40 -0
- package/lib/Utils/bot-planning-replay.js +44 -0
- package/lib/Utils/browser-utils.js +26 -0
- package/lib/Utils/business.js +75 -0
- package/lib/Utils/chat-utils.js +316 -0
- package/lib/Utils/companion-reg-client-utils.js +20 -0
- package/lib/Utils/consumer-application.js +38 -0
- package/lib/Utils/crypto.js +52 -0
- package/lib/Utils/decode-wa-message.js +197 -0
- package/lib/Utils/event-buffer.js +273 -0
- package/lib/Utils/extras.js +186 -0
- package/lib/Utils/generics.js +195 -0
- package/lib/Utils/history.js +51 -0
- package/lib/Utils/identity-change-handler.js +19 -0
- package/lib/Utils/index.js +39 -0
- package/lib/Utils/key-store.js +3 -0
- package/lib/Utils/link-preview.js +51 -0
- package/lib/Utils/logger.js +2 -0
- package/lib/Utils/lt-hash.js +3 -0
- package/lib/Utils/make-mutex.js +16 -0
- package/lib/Utils/message-composer.js +59 -0
- package/lib/Utils/message-retry-manager.js +48 -0
- package/lib/Utils/messages-media.js +520 -0
- package/lib/Utils/messages.js +742 -0
- package/lib/Utils/meta-ai-msmsg.js +122 -0
- package/lib/Utils/meta-compositing.js +75 -0
- package/lib/Utils/modern-messages.js +235 -0
- package/lib/Utils/native-flow.js +55 -0
- package/lib/Utils/noise-handler.js +161 -0
- package/lib/Utils/pre-key-manager.js +76 -0
- package/lib/Utils/process-message.js +399 -0
- package/lib/Utils/reporting-utils.js +171 -0
- package/lib/Utils/signal.js +167 -0
- package/lib/Utils/spam-report-utils.js +142 -0
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +102 -0
- package/lib/Utils/use-multi-file-auth-state.js +307 -0
- package/lib/Utils/validate-connection.js +181 -0
- package/lib/WABinary/constants.js +1321 -0
- package/lib/WABinary/decode.js +152 -0
- package/lib/WABinary/encode.js +140 -0
- package/lib/WABinary/generic-utils.js +179 -0
- package/lib/WABinary/index.js +6 -0
- package/lib/WABinary/jid-utils.js +235 -0
- package/lib/WABinary/types.js +4 -0
- package/lib/WAM/BinaryInfo.js +10 -0
- package/lib/WAM/constants.js +22853 -0
- package/lib/WAM/encode.js +150 -0
- package/lib/WAM/index.js +3 -0
- package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
- package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
- package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
- package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
- package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
- package/lib/WAUSync/Protocols/index.js +12 -0
- package/lib/WAUSync/USyncQuery.js +100 -0
- package/lib/WAUSync/USyncUser.js +20 -0
- package/lib/WAUSync/index.js +3 -0
- package/lib/index.js +68 -0
- 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
|
+
}
|