@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.
- package/LICENSE +21 -0
- package/README.md +3206 -3
- 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 +643 -0
- package/lib/Builders/base.js +130 -0
- package/lib/Builders/builders.js +449 -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 +981 -0
- package/lib/Socket/messages-send.js +708 -0
- package/lib/Socket/mex.js +34 -0
- package/lib/Socket/newsletter.js +781 -0
- package/lib/Socket/registration.js +155 -0
- package/lib/Socket/socket.js +797 -0
- package/lib/Socket/spam-report.js +65 -0
- package/lib/Socket/username.js +82 -0
- package/lib/Socket/zervonex-handler.js +55 -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 +317 -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 +38 -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 +752 -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 +135 -0
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +154 -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 +67 -0
- 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` }
|