@zervonex/baileys 0.0.0-stage → 3.0.2

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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3206 -3
  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 +643 -0
  9. package/lib/Builders/base.js +130 -0
  10. package/lib/Builders/builders.js +449 -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 +981 -0
  26. package/lib/Socket/messages-send.js +708 -0
  27. package/lib/Socket/mex.js +34 -0
  28. package/lib/Socket/newsletter.js +781 -0
  29. package/lib/Socket/registration.js +155 -0
  30. package/lib/Socket/socket.js +797 -0
  31. package/lib/Socket/spam-report.js +65 -0
  32. package/lib/Socket/username.js +82 -0
  33. package/lib/Socket/zervonex-handler.js +55 -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 +317 -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 +38 -0
  74. package/lib/Utils/link-preview.js +51 -0
  75. package/lib/Utils/logger.js +2 -0
  76. package/lib/Utils/lt-hash.js +3 -0
  77. package/lib/Utils/make-mutex.js +16 -0
  78. package/lib/Utils/message-composer.js +59 -0
  79. package/lib/Utils/message-retry-manager.js +48 -0
  80. package/lib/Utils/messages-media.js +520 -0
  81. package/lib/Utils/messages.js +752 -0
  82. package/lib/Utils/meta-ai-msmsg.js +122 -0
  83. package/lib/Utils/meta-compositing.js +75 -0
  84. package/lib/Utils/modern-messages.js +235 -0
  85. package/lib/Utils/native-flow.js +55 -0
  86. package/lib/Utils/noise-handler.js +161 -0
  87. package/lib/Utils/pre-key-manager.js +76 -0
  88. package/lib/Utils/process-message.js +399 -0
  89. package/lib/Utils/reporting-utils.js +171 -0
  90. package/lib/Utils/signal.js +167 -0
  91. package/lib/Utils/spam-report-utils.js +135 -0
  92. package/lib/Utils/sync-action-utils.js +33 -0
  93. package/lib/Utils/tc-token-utils.js +154 -0
  94. package/lib/Utils/use-multi-file-auth-state.js +307 -0
  95. package/lib/Utils/validate-connection.js +181 -0
  96. package/lib/WABinary/constants.js +1321 -0
  97. package/lib/WABinary/decode.js +152 -0
  98. package/lib/WABinary/encode.js +140 -0
  99. package/lib/WABinary/generic-utils.js +179 -0
  100. package/lib/WABinary/index.js +6 -0
  101. package/lib/WABinary/jid-utils.js +235 -0
  102. package/lib/WABinary/types.js +4 -0
  103. package/lib/WAM/BinaryInfo.js +10 -0
  104. package/lib/WAM/constants.js +22853 -0
  105. package/lib/WAM/encode.js +150 -0
  106. package/lib/WAM/index.js +3 -0
  107. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
  108. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
  109. package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
  110. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
  111. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
  112. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
  113. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
  114. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
  115. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
  116. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
  117. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
  118. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
  119. package/lib/WAUSync/Protocols/index.js +12 -0
  120. package/lib/WAUSync/USyncQuery.js +100 -0
  121. package/lib/WAUSync/USyncUser.js +20 -0
  122. package/lib/WAUSync/index.js +3 -0
  123. package/lib/index.js +67 -0
  124. package/package.json +220 -5
@@ -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
+ }
@@ -0,0 +1,186 @@
1
+ import { trimUndefined } from './generics.js'
2
+ import { createCipheriv, createDecipheriv, randomBytes, randomUUID } from 'crypto'
3
+ import { proto } from '../../WAProto/index.js'
4
+
5
+ export const SCHEDULED_MSG_META_TYPE = 'scheduled_message'
6
+ export const SCHEDULED_MSG_MAX_PER_CHAT = 30
7
+ export const SCHEDULED_MSG_MAX_MEDIA = 1
8
+ export const SCHEDULED_MSG_RESOURCE_LIMIT_NACK_CODE = 419
9
+ export const SCHEDULED_MSG_REVEAL_KEY_BYTES = 32
10
+ export const SCHEDULED_MSG_REVEAL_KEY_IV_BYTES = 12
11
+ export const SCHEDULED_MSG_REVEAL_KEY_RETENTION_DAYS = 30
12
+ export const SCHEDULED_MSG_TAG_BYTES = 16
13
+ export const SCHEDULED_MSG_WINDOW = Object.freeze({ chat: { minSeconds: 600, maxSeconds: 1209600 }, newsletter: { minSeconds: 600, maxSeconds: 2592000 } })
14
+ export const generateRevealKey = () => randomBytes(SCHEDULED_MSG_REVEAL_KEY_BYTES)
15
+ export const generateRevealKeyId = () => randomUUID()
16
+ export const encryptWithRevealKey = (plaintext, revealKey) => {
17
+ const key = Buffer.from(revealKey)
18
+ if (key.length !== SCHEDULED_MSG_REVEAL_KEY_BYTES) throw new TypeError(`reveal key must be ${SCHEDULED_MSG_REVEAL_KEY_BYTES} bytes`)
19
+ const encIv = randomBytes(SCHEDULED_MSG_REVEAL_KEY_IV_BYTES)
20
+ const cipher = createCipheriv('aes-256-gcm', key, encIv)
21
+ const body = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()])
22
+ return { encIv, encPayload: Buffer.concat([body, cipher.getAuthTag()]) }
23
+ }
24
+ export const decryptWithRevealKey = (encPayload, encIv, revealKey) => {
25
+ const key = Buffer.from(revealKey)
26
+ const payload = Buffer.from(encPayload)
27
+ if (payload.length < SCHEDULED_MSG_TAG_BYTES) throw new TypeError('encrypted payload is too short to carry an auth tag')
28
+ const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(encIv))
29
+ decipher.setAuthTag(payload.subarray(payload.length - SCHEDULED_MSG_TAG_BYTES))
30
+ return Buffer.concat([decipher.update(payload.subarray(0, payload.length - SCHEDULED_MSG_TAG_BYTES)), decipher.final()])
31
+ }
32
+ export const buildConditionalRevealMessage = ({ encPayload, encIv, revealKeyId }) => ({ conditionalRevealMessage: { conditionalRevealMessageType: proto.Message.ConditionalRevealMessage.ConditionalRevealMessageType.SCHEDULED_MESSAGE, encPayload: Buffer.from(encPayload), encIv: Buffer.from(encIv), revealKeyId } })
33
+ export const encodeScheduledMessage = (message, revealKey = generateRevealKey()) => {
34
+ const plaintext = proto.Message.encode(proto.Message.fromObject(message)).finish()
35
+ const { encIv, encPayload } = encryptWithRevealKey(plaintext, revealKey)
36
+ const revealKeyId = generateRevealKeyId()
37
+ return { revealKey, revealKeyId, encIv, encPayload, message: buildConditionalRevealMessage({ encPayload, encIv, revealKeyId }) }
38
+ }
39
+ export const decodeScheduledMessage = (conditionalRevealMessage, revealKey) => {
40
+ const inner = conditionalRevealMessage?.conditionalRevealMessage ?? conditionalRevealMessage
41
+ if (!inner?.encPayload || !inner?.encIv) throw new TypeError('conditionalRevealMessage is missing encPayload or encIv')
42
+ return proto.Message.decode(decryptWithRevealKey(inner.encPayload, inner.encIv, revealKey))
43
+ }
44
+ export const buildScheduledMsgMetaNode = ({ kind = 'schedule', scheduledTimestampS, revealKeyId, revealKey }) => {
45
+ if (!revealKeyId) throw new TypeError('revealKeyId is required')
46
+ if (kind === 'schedule' && !revealKey) throw new TypeError('scheduling requires the reveal key')
47
+ const attrs = { type: SCHEDULED_MSG_META_TYPE }
48
+ if (kind === 'schedule') {
49
+ if (!Number.isFinite(Number(scheduledTimestampS))) throw new TypeError('scheduledTimestampS must be a unix timestamp in seconds')
50
+ attrs.st = String(Math.floor(Number(scheduledTimestampS)))
51
+ }
52
+ return { tag: 'meta', attrs, content: [{ tag: 'key', attrs: { rkid: revealKeyId }, content: kind === 'schedule' ? Buffer.from(revealKey) : undefined }] }
53
+ }
54
+ export const buildUnscheduleProtocolMessage = (key) => ({ protocolMessage: { key, type: proto.Message.ProtocolMessage.Type.MESSAGE_UNSCHEDULE } })
55
+ export const isScheduledTimeValid = (scheduledTimestampS, nowSeconds = Math.floor(Date.now() / 1000), window = SCHEDULED_MSG_WINDOW.chat) => {
56
+ const rounded = Math.floor(nowSeconds / 60) * 60
57
+ return scheduledTimestampS - rounded >= window.minSeconds && scheduledTimestampS - nowSeconds <= window.maxSeconds
58
+ }
59
+ export const SplitPaymentStatus = Object.freeze({ PENDING: 0, PAID: 1 })
60
+ export const ReminderFrequency = Object.freeze({ REMINDER_FREQUENCY_UNKNOWN: 0, WEEKLY: 1, BI_WEEKLY: 2, MONTHLY: 3, QUARTERLY: 4 })
61
+ export const ReminderStatus = Object.freeze({ REMINDER_STATUS_UNKNOWN: 0, ACTIVE: 1, CANCELLED_BY_CREATOR: 2, STOPPED_BY_RECEIVER: 3, EXPIRED: 4, PAID: 5 })
62
+ export const MONEY_OFFSET = 1000
63
+ const drop = (object) => { for (const key of Object.keys(object)) { if (object[key] === undefined || object[key] === null) delete object[key] } return object }
64
+ export const money = (amount, currencyCode, offset = MONEY_OFFSET) => {
65
+ const value = Number(amount)
66
+ if (!Number.isFinite(value) || value < 0) throw new TypeError('money needs a non-negative amount')
67
+ if (typeof currencyCode !== 'string' || currencyCode.length !== 3) throw new TypeError('money needs a three letter currency code, like IDR or USD')
68
+ if (!Number.isInteger(offset) || offset <= 0) throw new TypeError('money offset must be a positive integer')
69
+ return { value: Math.round(value * offset), offset, currencyCode: currencyCode.toUpperCase() }
70
+ }
71
+ export const readMoney = (value) => { if (!value?.offset) return undefined; return { amount: Number(value.value) / Number(value.offset), currencyCode: value.currencyCode } }
72
+ export const buildSplitPayment = ({ splitId, total, currency, description, requesterJid, participants = [], createdAt } = {}) => {
73
+ if (!splitId) throw new TypeError('buildSplitPayment needs a splitId')
74
+ if (!participants.length) throw new TypeError('buildSplitPayment needs at least one participant')
75
+ return drop({ splitId: String(splitId), totalAmount: total === undefined ? undefined : money(total, currency), description, requesterJid, participants: participants.map((p, i) => { if (!p?.jid) throw new TypeError(`participant ${i} needs a jid`); return drop({ jid: p.jid, amount: p.amount === undefined ? undefined : money(p.amount, p.currency ?? currency), status: p.status ?? SplitPaymentStatus.PENDING }) }), createdAtMs: createdAt === undefined ? Date.now() : Number(createdAt) })
76
+ }
77
+ export const buildSplitPaymentUpdate = ({ splitId, participantJid } = {}) => { if (!splitId || !participantJid) throw new TypeError('buildSplitPaymentUpdate needs both splitId and participantJid'); return { splitId: String(splitId), participantJid } }
78
+ export const buildPaymentReminder = ({ reminderId, instanceId, description, frequency = ReminderFrequency.MONTHLY, status = ReminderStatus.ACTIVE, amount, currency, payeeVpa, payeeJid, payerJid } = {}) => {
79
+ if (!reminderId) throw new TypeError('buildPaymentReminder needs a reminderId')
80
+ return drop({ reminderId: String(reminderId), instanceId: instanceId === undefined ? undefined : String(instanceId), description, frequency, status, amount: amount === undefined ? undefined : money(amount, currency), payeeVpa, payeeJid, payerJid })
81
+ }
82
+ export const readSplitPayment = (msg) => {
83
+ const split = (msg?.message ?? msg)?.splitPaymentMessage
84
+ if (!split) return null
85
+ return drop({ splitId: split.splitId, total: readMoney(split.totalAmount), description: split.description, requesterJid: split.requesterJid, createdAtMs: split.createdAtMs === undefined ? undefined : Number(split.createdAtMs), participants: (split.participants ?? []).map(p => drop({ jid: p.jid, amount: readMoney(p.amount), status: p.status })) })
86
+ }
87
+ export const readPaymentReminder = (msg) => {
88
+ const reminder = (msg?.message ?? msg)?.paymentReminderMessage
89
+ if (!reminder) return null
90
+ return drop({ reminderId: reminder.reminderId, instanceId: reminder.instanceId, description: reminder.description, frequency: reminder.frequency, status: reminder.status, amount: readMoney(reminder.amount), payeeVpa: reminder.payeeVpa, payeeJid: reminder.payeeJid, payerJid: reminder.payerJid })
91
+ }
92
+ export const StatusLinkType = Object.freeze({ RASTERIZED_LINK_PREVIEW: 1, RASTERIZED_LINK_TRUNCATED: 2, RASTERIZED_LINK_FULL_URL: 3 })
93
+ export const STICKER_DEFAULT_AREA = Object.freeze({ x: 0.25, y: 0.4, width: 0.5, height: 0.2 })
94
+ const fraction = (value, name) => { const n = Number(value); if (!Number.isFinite(n) || n < 0 || n > 1) throw new TypeError(`${name} must be between 0 and 1`); return n }
95
+ export const stickerArea = ({ x, y, width, height } = STICKER_DEFAULT_AREA) => {
96
+ const left = fraction(x, 'x'), top = fraction(y, 'y'), right = fraction(left + Number(width), 'x + width'), bottom = fraction(top + Number(height), 'y + height')
97
+ if (right <= left || bottom <= top) throw new TypeError('a sticker area needs a positive width and height')
98
+ return [{ x: left, y: top }, { x: right, y: top }, { x: right, y: bottom }, { x: left, y: bottom }]
99
+ }
100
+ const annotation = (area, fields) => ({ polygonVertices: Array.isArray(area) ? area : stickerArea(area), ...fields })
101
+ export const locationSticker = ({ latitude, longitude, name, area, skipConfirmation } = {}) => {
102
+ const degreesLatitude = Number(latitude), degreesLongitude = Number(longitude)
103
+ if (!Number.isFinite(degreesLatitude) || !Number.isFinite(degreesLongitude)) throw new TypeError('locationSticker needs a numeric latitude and longitude')
104
+ return annotation(area, { location: { degreesLatitude, degreesLongitude, ...(name ? { name } : {}) }, ...(skipConfirmation === undefined ? {} : { shouldSkipConfirmation: !!skipConfirmation }) })
105
+ }
106
+ export const channelSticker = ({ jid, name, serverMessageId = 0, accessibilityText, area } = {}) => {
107
+ if (typeof jid !== 'string' || !jid.endsWith('@newsletter')) throw new TypeError('channelSticker needs the channel jid ending in @newsletter')
108
+ return annotation(area, { newsletter: { newsletterJid: jid, serverMessageId: Number(serverMessageId), ...(name ? { newsletterName: name } : {}), ...(accessibilityText ? { accessibilityText } : {}) } })
109
+ }
110
+ export const linkSticker = ({ url, title, area, linkType = StatusLinkType.RASTERIZED_LINK_PREVIEW } = {}) => {
111
+ if (typeof url !== 'string' || !url) throw new TypeError('linkSticker needs a url')
112
+ return annotation(area, { tapAction: { tapUrl: url, ...(title ? { title } : {}) }, statusLinkType: linkType })
113
+ }
114
+ export const musicSticker = ({ songId, title, author, mediaId, artworkDirectPath, artworkSha256, artworkEncSha256, artworkMediaKey, artistAttribution, countryBlocklist, isExplicit, startTimeMs = 0, derivedStartTimeMs, durationMs, area } = {}) => {
115
+ if (!songId && !mediaId) throw new TypeError('musicSticker needs at least a songId or a musicContentMediaId')
116
+ const bytes = v => v === undefined ? undefined : Buffer.isBuffer(v) ? v : Buffer.from(v, 'base64')
117
+ const em = { ...(songId ? { songId: String(songId) } : {}), ...(mediaId ? { musicContentMediaId: String(mediaId) } : {}), ...(title ? { title } : {}), ...(author ? { author } : {}), ...(artistAttribution ? { artistAttribution } : {}), ...(artworkDirectPath ? { artworkDirectPath } : {}), ...(isExplicit === undefined ? {} : { isExplicit: !!isExplicit }), musicSongStartTimeInMs: Number(startTimeMs), ...(derivedStartTimeMs === undefined ? {} : { derivedContentStartTimeInMs: Number(derivedStartTimeMs) }), ...(durationMs === undefined ? {} : { overlapDurationInMs: Number(durationMs) }) }
118
+ for (const [k, v] of [['artworkSha256', artworkSha256], ['artworkEncSha256', artworkEncSha256], ['artworkMediaKey', artworkMediaKey], ['countryBlocklist', countryBlocklist]]) { const b = bytes(v); if (b) em[k] = b }
119
+ return annotation(area, { embeddedContent: { embeddedMusic: em } })
120
+ }
121
+ export const messageSticker = ({ stanzaId, message, area } = {}) => {
122
+ if (!message) throw new TypeError('messageSticker needs the message it embeds')
123
+ return annotation(area, { embeddedContent: { embeddedMessage: { ...(stanzaId ? { stanzaId } : {}), message } } })
124
+ }
125
+ const STICKER_ACTIONS = ['location', 'newsletter', 'embeddedAction', 'tapAction']
126
+ export const normalizeStickers = (stickers) => {
127
+ const list = Array.isArray(stickers) ? stickers : [stickers]
128
+ return list.filter(Boolean).map((sticker, i) => {
129
+ const v = sticker.polygonVertices
130
+ if (!Array.isArray(v) || v.length !== 4) throw new TypeError(`sticker ${i} needs four polygonVertices`)
131
+ const actions = STICKER_ACTIONS.filter(n => sticker[n] !== undefined && sticker[n] !== null)
132
+ if (actions.length > 1) throw new TypeError(`sticker ${i} sets ${actions.join(' and ')}, but only one action is allowed per sticker`)
133
+ return proto.InteractiveAnnotation.fromObject(sticker)
134
+ })
135
+ }
136
+ export const readStickers = (message) => {
137
+ const media = message?.imageMessage ?? message?.videoMessage ?? message
138
+ const annotations = media?.interactiveAnnotations
139
+ if (!Array.isArray(annotations) || !annotations.length) return []
140
+ return annotations.map(item => {
141
+ const [topLeft, , bottomRight] = item.polygonVertices ?? []
142
+ const music = item.embeddedContent?.embeddedMusic
143
+ return { kind: item.location ? 'location' : item.newsletter ? 'channel' : item.tapAction ? 'link' : music ? 'music' : item.embeddedContent?.embeddedMessage ? 'message' : 'unknown', area: topLeft && bottomRight ? { x: topLeft.x, y: topLeft.y, width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y } : undefined, location: item.location ?? undefined, channel: item.newsletter ?? undefined, link: item.tapAction ? { url: item.tapAction.tapUrl, title: item.tapAction.title, linkType: item.statusLinkType } : undefined, music: music ? { songId: music.songId, title: music.title, author: music.author } : undefined, message: item.embeddedContent?.embeddedMessage ?? undefined, annotation: item }
144
+ })
145
+ }
146
+ export const MusicMessageStyle = Object.freeze({ UNKNOWN: 0, VINYL: 1 })
147
+ export const MUSIC_ALLOWED_HOSTS = Object.freeze(['.whatsapp.net', '.whatsapp.com', '.fbcdn.net', '.facebook.com', '.instagram.com', '.cdninstagram.com'])
148
+ export const isMusicHostAllowed = (value) => { let host; try { host = new URL(String(value)).hostname.toLowerCase() } catch { return false } return MUSIC_ALLOWED_HOSTS.some(s => host.endsWith(s) || host === s.slice(1)) }
149
+ const assertMusicHost = (value, label) => { if (value === undefined) return undefined; if (!isMusicHostAllowed(value)) throw new TypeError(`${label} must be hosted on ${MUSIC_ALLOWED_HOSTS.join(', ')}`); return value }
150
+ const embeddedMusicOf = (options) => musicSticker(options).embeddedContent.embeddedMusic
151
+ export const buildMusicMessage = ({ songUri, artworkUri, style = MusicMessageStyle.VINYL, contextInfo, ...music } = {}) => trimUndefined({ embeddedMusic: embeddedMusicOf(music), songUri: assertMusicHost(songUri, 'songUri'), artworkUri: assertMusicHost(artworkUri, 'artworkUri'), style, contextInfo })
152
+ export const readMusicMessage = (msg) => {
153
+ const music = (msg?.message ?? msg)?.musicMessage
154
+ if (!music) return null
155
+ const em = music.embeddedMusic ?? {}
156
+ return { songId: em.songId, mediaId: em.musicContentMediaId, title: em.title, author: em.author, artistAttribution: em.artistAttribution, isExplicit: em.isExplicit, startTimeMs: em.musicSongStartTimeInMs, durationMs: em.overlapDurationInMs, songUri: music.songUri, artworkUri: music.artworkUri, style: music.style, embeddedMusic: em }
157
+ }
158
+ export function buildAckStanza(node, errorCode, meId) {
159
+ const { tag, attrs } = node
160
+ const stanza = { tag: 'ack', attrs: { id: attrs.id, to: attrs.from, class: tag } }
161
+ if (errorCode) stanza.attrs.error = errorCode.toString()
162
+ if (attrs.participant) stanza.attrs.participant = attrs.participant
163
+ if (attrs.recipient) stanza.attrs.recipient = attrs.recipient
164
+ if (attrs.type) stanza.attrs.type = attrs.type
165
+ if (tag === 'message' && meId) stanza.attrs.from = meId
166
+ return stanza
167
+ }
168
+ export const pickSenderKeyRecipients = (devices, senderKeyMap, { force = false, skip } = {}) => {
169
+ const recipients = []
170
+ for (const device of devices) {
171
+ const deviceJid = device?.jid
172
+ if (!deviceJid || (!force && senderKeyMap[deviceJid]) || (skip && skip(device))) continue
173
+ recipients.push(deviceJid)
174
+ }
175
+ return recipients
176
+ }
177
+ export const deliveredSenderKeyJids = nodes => { const delivered = new Set(); for (const node of nodes || []) { const jid = node?.attrs?.jid; if (jid) delivered.add(jid) } return delivered }
178
+ export const senderKeyResetSummary = (stored, jid) => { const devices = Object.keys(stored?.[jid] || {}); return { jid, cleared: devices.length, devices } }
179
+ export const commitSenderKeyDelivery = (senderKeyMap, recipients, nodes) => {
180
+ const delivered = deliveredSenderKeyJids(nodes), marked = [], skipped = []
181
+ for (const jid of recipients) { if (delivered.has(jid)) { senderKeyMap[jid] = true; marked.push(jid) } else { delete senderKeyMap[jid]; skipped.push(jid) } }
182
+ return { marked, skipped }
183
+ }
184
+ export const GROUP_ADDRESSING_MODES = Object.freeze(['pn', 'lid'])
185
+ export const isUsableGroupMetadata = metadata => { if (!metadata || typeof metadata !== 'object') return false; if (!Array.isArray(metadata.participants)) return false; return GROUP_ADDRESSING_MODES.includes(metadata.addressingMode) }
186
+ export const describeUnusableGroupMetadata = metadata => { if (!metadata || typeof metadata !== 'object') return 'nothing cached'; if (!Array.isArray(metadata.participants)) return 'no participants'; if (metadata.addressingMode === undefined || metadata.addressingMode === null) return 'no addressingMode'; return `addressingMode ${JSON.stringify(metadata.addressingMode)} is not pn or lid` }