sauruspro-bail 0.0.0-stage → 5.0.0

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 (134) hide show
  1. package/README.md +129 -2
  2. package/WAProto/GenerateStatics.sh +3 -0
  3. package/WAProto/WAProto.proto +6754 -0
  4. package/WAProto/fix-imports.js +85 -0
  5. package/WAProto/index.d.ts +79257 -0
  6. package/WAProto/index.js +242946 -0
  7. package/docs/CHANGELOG.md +22 -0
  8. package/docs/NEWSLETTER-STATUS.md +120 -0
  9. package/docs/SPR.md +51 -0
  10. package/engine-requirements.js +10 -0
  11. package/lib/Defaults/index.js +131 -0
  12. package/lib/Signal/Group/ciphertext-message.js +12 -0
  13. package/lib/Signal/Group/group-session-builder.js +30 -0
  14. package/lib/Signal/Group/group_cipher.js +82 -0
  15. package/lib/Signal/Group/index.js +12 -0
  16. package/lib/Signal/Group/keyhelper.js +18 -0
  17. package/lib/Signal/Group/sender-chain-key.js +26 -0
  18. package/lib/Signal/Group/sender-key-distribution-message.js +63 -0
  19. package/lib/Signal/Group/sender-key-message.js +66 -0
  20. package/lib/Signal/Group/sender-key-name.js +48 -0
  21. package/lib/Signal/Group/sender-key-record.js +41 -0
  22. package/lib/Signal/Group/sender-key-state.js +84 -0
  23. package/lib/Signal/Group/sender-message-key.js +26 -0
  24. package/lib/Signal/libsignal.js +431 -0
  25. package/lib/Signal/lid-mapping.js +277 -0
  26. package/lib/Socket/Client/index.js +3 -0
  27. package/lib/Socket/Client/types.js +11 -0
  28. package/lib/Socket/Client/websocket.js +54 -0
  29. package/lib/Socket/MessageBuilder.js +3729 -0
  30. package/lib/Socket/business.js +379 -0
  31. package/lib/Socket/chats.js +1198 -0
  32. package/lib/Socket/communities.js +431 -0
  33. package/lib/Socket/graphql.js +716 -0
  34. package/lib/Socket/groups.js +374 -0
  35. package/lib/Socket/index.js +92 -0
  36. package/lib/Socket/interop.js +463 -0
  37. package/lib/Socket/luxu.js +387 -0
  38. package/lib/Socket/message-builder.js +556 -0
  39. package/lib/Socket/messages-recv.js +1916 -0
  40. package/lib/Socket/messages-send.js +1516 -0
  41. package/lib/Socket/mex.js +54 -0
  42. package/lib/Socket/newsletter.js +641 -0
  43. package/lib/Socket/privacy.js +318 -0
  44. package/lib/Socket/socket.js +1115 -0
  45. package/lib/Socket/username.js +236 -0
  46. package/lib/Store/index.js +10 -0
  47. package/lib/Store/keyed-db.js +108 -0
  48. package/lib/Store/make-cache-manager-store.js +85 -0
  49. package/lib/Store/make-in-memory-store.js +244 -0
  50. package/lib/Store/make-ordered-dictionary.js +75 -0
  51. package/lib/Store/object-repository.js +32 -0
  52. package/lib/Types/Auth.js +2 -0
  53. package/lib/Types/Bussines.js +2 -0
  54. package/lib/Types/Call.js +2 -0
  55. package/lib/Types/Chat.js +8 -0
  56. package/lib/Types/Contact.js +2 -0
  57. package/lib/Types/Events.js +2 -0
  58. package/lib/Types/GroupMetadata.js +2 -0
  59. package/lib/Types/Label.js +25 -0
  60. package/lib/Types/LabelAssociation.js +7 -0
  61. package/lib/Types/Message.js +11 -0
  62. package/lib/Types/Mex.js +111 -0
  63. package/lib/Types/Product.js +2 -0
  64. package/lib/Types/Signal.js +2 -0
  65. package/lib/Types/Socket.js +3 -0
  66. package/lib/Types/State.js +56 -0
  67. package/lib/Types/USync.js +2 -0
  68. package/lib/Types/index.js +26 -0
  69. package/lib/Utils/auth-utils.js +302 -0
  70. package/lib/Utils/browser-utils.js +48 -0
  71. package/lib/Utils/business.js +231 -0
  72. package/lib/Utils/chat-utils.js +872 -0
  73. package/lib/Utils/companion-reg-client-utils.js +35 -0
  74. package/lib/Utils/crypto.js +118 -0
  75. package/lib/Utils/decode-wa-message.js +350 -0
  76. package/lib/Utils/dinolith-spr.d.ts +26 -0
  77. package/lib/Utils/dinolith-spr.js +521 -0
  78. package/lib/Utils/event-buffer.js +622 -0
  79. package/lib/Utils/generics.js +403 -0
  80. package/lib/Utils/history.js +134 -0
  81. package/lib/Utils/identity-change-handler.js +50 -0
  82. package/lib/Utils/index.js +26 -0
  83. package/lib/Utils/link-preview.js +85 -0
  84. package/lib/Utils/logger.js +3 -0
  85. package/lib/Utils/lt-hash.js +8 -0
  86. package/lib/Utils/make-mutex.js +33 -0
  87. package/lib/Utils/message-composer.js +273 -0
  88. package/lib/Utils/message-retry-manager.js +265 -0
  89. package/lib/Utils/messages-media.js +870 -0
  90. package/lib/Utils/messages.js +1394 -0
  91. package/lib/Utils/newsletter-status.d.ts +163 -0
  92. package/lib/Utils/newsletter-status.js +738 -0
  93. package/lib/Utils/noise-handler.js +201 -0
  94. package/lib/Utils/offline-node-processor.js +40 -0
  95. package/lib/Utils/pre-key-manager.js +106 -0
  96. package/lib/Utils/process-message.js +630 -0
  97. package/lib/Utils/reporting-utils.js +258 -0
  98. package/lib/Utils/signal.js +201 -0
  99. package/lib/Utils/stanza-ack.js +38 -0
  100. package/lib/Utils/sticker.js +133 -0
  101. package/lib/Utils/sync-action-utils.js +49 -0
  102. package/lib/Utils/tc-token-utils.js +163 -0
  103. package/lib/Utils/use-multi-file-auth-state.js +121 -0
  104. package/lib/Utils/use-sqlite-auth-state.js +162 -0
  105. package/lib/Utils/validate-connection.js +203 -0
  106. package/lib/WABinary/constants.js +1301 -0
  107. package/lib/WABinary/decode.js +262 -0
  108. package/lib/WABinary/encode.js +220 -0
  109. package/lib/WABinary/generic-utils.js +204 -0
  110. package/lib/WABinary/index.js +6 -0
  111. package/lib/WABinary/jid-utils.js +98 -0
  112. package/lib/WABinary/types.js +2 -0
  113. package/lib/WAM/BinaryInfo.js +10 -0
  114. package/lib/WAM/constants.js +22853 -0
  115. package/lib/WAM/encode.js +150 -0
  116. package/lib/WAM/index.js +4 -0
  117. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +53 -0
  118. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +43 -0
  119. package/lib/WAUSync/Protocols/USyncContactProtocol.js +54 -0
  120. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +57 -0
  121. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +30 -0
  122. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +58 -0
  123. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +30 -0
  124. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +32 -0
  125. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +28 -0
  126. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +39 -0
  127. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +38 -0
  128. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +27 -0
  129. package/lib/WAUSync/Protocols/index.js +12 -0
  130. package/lib/WAUSync/USyncQuery.js +151 -0
  131. package/lib/WAUSync/USyncUser.js +56 -0
  132. package/lib/WAUSync/index.js +3 -0
  133. package/lib/index.js +67 -0
  134. package/package.json +147 -5
@@ -0,0 +1,738 @@
1
+ /**
2
+ * Newsletter Status (Channel Status) for sauruspro-bail
3
+ * Owner: Saurus / Dinolith
4
+ *
5
+ * Send, react, revoke and fetch WhatsApp Channel (newsletter) statuses.
6
+ * ESM port, uses only local sauruspro-bail modules.
7
+ */
8
+ import { proto } from '../../WAProto/index.js';
9
+ import { encodeNewsletterMessage, generateMessageIDV2 } from './generics.js';
10
+ import { generateWAMessage, normalizeMessageContent } from './messages.js';
11
+ import { WAMessageStatus } from '../Types/Message.js';
12
+ import { getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET, isJidNewsletter } from '../WABinary/index.js';
13
+
14
+ /**
15
+ * Minimal content preparer (replaces the big modern-messages layer).
16
+ * Supports: content.newsletterStatus = { newsletterJid, messageId, actionUrl?, duration?, hasMultipleReshares? }
17
+ * content.statusAttributions = [...]
18
+ */
19
+ export const makeNewsletterStatusAttribution = input => {
20
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('newsletterStatus must be an object')
21
+ if (!input.newsletterJid) throw new TypeError('newsletterStatus.newsletterJid is required')
22
+ if (input.messageId === undefined || input.messageId === null) throw new TypeError('newsletterStatus.messageId is required')
23
+ return proto.StatusAttribution.create({
24
+ type: proto.StatusAttribution.Type.NEWSLETTER_STATUS,
25
+ actionUrl: input.actionUrl,
26
+ statusReshare: {
27
+ source: proto.StatusAttribution.StatusReshare.Source.CHANNEL_RESHARE,
28
+ metadata: {
29
+ duration: input.duration,
30
+ channelJid: input.newsletterJid,
31
+ channelMessageId: Number(input.messageId),
32
+ hasMultipleReshares: input.hasMultipleReshares ?? false
33
+ }
34
+ }
35
+ })
36
+ }
37
+
38
+ export const prepareNewsletterStatusContent = content => {
39
+ if (!content || typeof content !== 'object' || Array.isArray(content)) return content
40
+ const output = { ...content }
41
+ const attributions = Array.isArray(output.contextInfo?.statusAttributions) ? [...output.contextInfo.statusAttributions] : []
42
+ if (output.newsletterStatus) {
43
+ attributions.push(makeNewsletterStatusAttribution(output.newsletterStatus))
44
+ delete output.newsletterStatus
45
+ }
46
+ if (Array.isArray(output.statusAttributions)) {
47
+ attributions.push(...output.statusAttributions)
48
+ delete output.statusAttributions
49
+ }
50
+ if (attributions.length) {
51
+ output.contextInfo = { ...(output.contextInfo || {}), statusAttributions: attributions }
52
+ }
53
+ return output
54
+ }
55
+
56
+
57
+ const NEWSLETTER_STATUS_MEDIA_TYPES = new Set(['audio', 'gif', 'image', 'video'])
58
+ const NEWSLETTER_STATUS_WEB_MEDIA_TYPES = new Set(['image', 'video'])
59
+ const NEWSLETTER_STATUS_INTERACTIONS = new Set(['question', 'question_response', 'question_reshare'])
60
+ const NEWSLETTER_STATUS_CONTENT_TYPES = new Set(['text', 'media', 'reaction'])
61
+ const NEWSLETTER_STATUS_CALLBACK_EVENTS = ['CB:ack', 'CB:status', 'CB:iq', 'CB:notification', 'CB:message']
62
+ const NEWSLETTER_STATUS_EDIT_REACTION_REVOKE = '7'
63
+ const NEWSLETTER_STATUS_EDIT_ADMIN_REVOKE = '8'
64
+ const NEWSLETTER_STATUS_ACK_CLASS = 'status'
65
+ const NEWSLETTER_STATUS_ACK_TIMEOUT_MS = 20000
66
+ const NEWSLETTER_STATUS_SERVER_ID_TIMEOUT_MS = 8000
67
+
68
+ export const getNewsletterStatusMediaType = message => {
69
+ const content = normalizeMessageContent(message)
70
+ if (content?.imageMessage) return 'image'
71
+ if (content?.videoMessage) return content.videoMessage.gifPlayback ? 'gif' : 'video'
72
+ if (content?.audioMessage) return 'audio'
73
+ return undefined
74
+ }
75
+
76
+ const assertNewsletterJid = jid => {
77
+ if (!isJidNewsletter(jid)) throw new TypeError('Newsletter status target must be a @newsletter JID')
78
+ }
79
+
80
+ const assertInteraction = interactionType => {
81
+ if (interactionType !== undefined && !NEWSLETTER_STATUS_INTERACTIONS.has(interactionType)) {
82
+ throw new TypeError(`Unsupported newsletter status interaction: ${interactionType}`)
83
+ }
84
+ }
85
+
86
+ const assertMediaType = mediaType => {
87
+ if (mediaType !== undefined && !NEWSLETTER_STATUS_MEDIA_TYPES.has(mediaType)) {
88
+ throw new TypeError(`Unsupported newsletter status media type: ${mediaType}`)
89
+ }
90
+ }
91
+
92
+ const isPresent = value => value !== undefined && value !== null && value !== ''
93
+
94
+ export const NEWSLETTER_SERVER_ID_MIN = 99
95
+ export const NEWSLETTER_SERVER_ID_MAX = 2147476647
96
+
97
+ export const toNewsletterServerId = (value, label) => {
98
+ const parsed = typeof value === 'number' ? value : Number(String(value).trim())
99
+ if (!Number.isInteger(parsed) || parsed < NEWSLETTER_SERVER_ID_MIN || parsed > NEWSLETTER_SERVER_ID_MAX) {
100
+ throw new TypeError(`${JSON.stringify(value)} is not a newsletter server id${label ? ` for ${label}` : ''}; the server accepts ${NEWSLETTER_SERVER_ID_MIN}..${NEWSLETTER_SERVER_ID_MAX}, and you get one from newsletterFetchMessages or a sent status`)
101
+ }
102
+ return String(parsed)
103
+ }
104
+
105
+ const toServerId = (value, label) => toNewsletterServerId(value, label)
106
+
107
+ const buildMetaNode = ({ interactionType, parentServerId, responseServerId, aiContent }) => {
108
+ assertInteraction(interactionType)
109
+
110
+ const attrs = {}
111
+ if (interactionType) {
112
+ attrs.interaction_type = interactionType
113
+ if (interactionType === 'question_reshare') {
114
+ if (!isPresent(parentServerId)) throw new TypeError('question_reshare requires parentServerId')
115
+ if (!isPresent(responseServerId)) throw new TypeError('question_reshare requires responseServerId')
116
+ attrs.parent_server_id = toServerId(parentServerId, 'parentServerId')
117
+ attrs.response_server_id = String(responseServerId)
118
+ }
119
+ else if (interactionType === 'question_response' && isPresent(responseServerId)) {
120
+ attrs.response_server_id = String(responseServerId)
121
+ }
122
+ }
123
+
124
+ if (!interactionType && !aiContent) return undefined
125
+
126
+ return {
127
+ tag: 'meta',
128
+ attrs,
129
+ content: aiContent ? [{ tag: 'ai_content', attrs: {}, content: undefined }] : undefined
130
+ }
131
+ }
132
+
133
+ export const withNewsletterStatusAttribution = content => ({
134
+ ...content,
135
+ contextInfo: {
136
+ statusAttributions: [{ type: proto.StatusAttribution.Type.NEWSLETTER_STATUS }],
137
+ featureEligibilities: { canBeReshared: true },
138
+ ...(content?.contextInfo || {})
139
+ }
140
+ })
141
+
142
+ const snapshotCallbackNode = (event, node) => ({
143
+ event,
144
+ tag: node?.tag,
145
+ attrs: node?.attrs || {},
146
+ children: Array.isArray(node?.content)
147
+ ? node.content.map(child => ({ tag: child?.tag, attrs: child?.attrs || {} }))
148
+ : []
149
+ })
150
+
151
+ const collectStatusCallbacks = sock => {
152
+ const frames = []
153
+ const listeners = []
154
+ if (!sock?.ws?.on) {
155
+ return { frames, stop: () => {} }
156
+ }
157
+
158
+ for (const event of NEWSLETTER_STATUS_CALLBACK_EVENTS) {
159
+ const listener = node => {
160
+ if (frames.length >= 40) return
161
+ frames.push(snapshotCallbackNode(event, node))
162
+ }
163
+ sock.ws.on(event, listener)
164
+ listeners.push([event, listener])
165
+ }
166
+
167
+ return {
168
+ frames,
169
+ stop: () => {
170
+ for (const [event, listener] of listeners) {
171
+ sock.ws?.off?.(event, listener)
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ const readIntAttr = (attrs, key) => {
178
+ if (!isPresent(attrs?.[key])) return undefined
179
+ const parsed = Number.parseInt(String(attrs[key]), 10)
180
+ return Number.isFinite(parsed) ? parsed : undefined
181
+ }
182
+
183
+ export const parseNewsletterStatusAck = (node, { jid, messageId } = {}) => {
184
+ if (!node || typeof node !== 'object') throw new TypeError('Newsletter status ack node is required')
185
+ if (node.tag !== 'ack') {
186
+ const error = new Error(`Newsletter status expected <ack>, got <${node.tag}>`)
187
+ error.data = node
188
+ throw error
189
+ }
190
+
191
+ const attrs = node.attrs || {}
192
+ if (attrs.class !== undefined && attrs.class !== NEWSLETTER_STATUS_ACK_CLASS) {
193
+ const error = new Error(`Newsletter status ack has class "${attrs.class}", expected "${NEWSLETTER_STATUS_ACK_CLASS}"`)
194
+ error.data = node
195
+ throw error
196
+ }
197
+ if (isPresent(messageId) && isPresent(attrs.id) && attrs.id !== messageId) {
198
+ const error = new Error(`Newsletter status ack id mismatch: ${attrs.id} != ${messageId}`)
199
+ error.data = node
200
+ throw error
201
+ }
202
+ if (isPresent(jid) && isPresent(attrs.from) && attrs.from !== jid) {
203
+ const error = new Error(`Newsletter status ack from mismatch: ${attrs.from} != ${jid}`)
204
+ error.data = node
205
+ throw error
206
+ }
207
+
208
+ return {
209
+ class: attrs.class,
210
+ from: attrs.from,
211
+ id: attrs.id,
212
+ t: readIntAttr(attrs, 't'),
213
+ serverId: readIntAttr(attrs, 'server_id'),
214
+ edit: attrs.edit,
215
+ error: isPresent(attrs.error) ? String(attrs.error) : undefined,
216
+ applicationError: readIntAttr(attrs, 'application_error'),
217
+ backoff: readIntAttr(attrs, 'backoff'),
218
+ node
219
+ }
220
+ }
221
+
222
+ /**
223
+ * The ack carries from, class, id and t, and nothing else. The server id lands
224
+ * separately, in the <status> stanza the server sends back to the publisher --
225
+ * WASmaxInStatusDeliverFromNewsletterMixin reads from, server_id, t and
226
+ * is_sender off exactly that tag. Reacting to or revoking your own status needs
227
+ * that id, so wait a moment for the echo rather than returning undefined.
228
+ */
229
+ export const waitForNewsletterStatusServerId = (sock, { jid, messageId, timeoutMs = 8000 }) => {
230
+ let settle
231
+ const promise = new Promise(resolve => {
232
+ let done = false
233
+ const finish = value => {
234
+ if (done) return
235
+ done = true
236
+ clearTimeout(timer)
237
+ sock.ws.off('CB:status', onStatus)
238
+ resolve(value)
239
+ }
240
+ const onStatus = node => {
241
+ if (node?.tag !== 'status') return
242
+ const attrs = node.attrs || {}
243
+ if (isPresent(jid) && isPresent(attrs.from) && attrs.from !== jid) return
244
+ if (isPresent(messageId) && isPresent(attrs.id) && attrs.id !== messageId) return
245
+ const serverId = readIntAttr(attrs, 'server_id')
246
+ if (serverId !== undefined) finish({ serverId, node })
247
+ }
248
+ const timer = setTimeout(() => finish(undefined), timeoutMs)
249
+ sock.ws.on('CB:status', onStatus)
250
+ settle = finish
251
+ })
252
+ promise.cancel = () => settle(undefined)
253
+ return promise
254
+ }
255
+
256
+ const assertStatusServerResponse = (response, { jid, messageId, callbacks = [] }) => {
257
+ if (!response) {
258
+ const suffix = callbacks.length
259
+ ? `; callbacks=${JSON.stringify(callbacks)}`
260
+ : '; no ack/status/iq/notification/message callback observed'
261
+ const error = new Error(`Newsletter status server ACK timed out for ${messageId}${suffix}`)
262
+ error.data = { messageId, callbacks }
263
+ throw error
264
+ }
265
+
266
+ const ack = parseNewsletterStatusAck(response, { jid, messageId })
267
+ if (ack.error) {
268
+ const details = [
269
+ ack.applicationError !== undefined ? `application_error=${ack.applicationError}` : undefined,
270
+ ack.backoff !== undefined ? `backoff=${ack.backoff}` : undefined
271
+ ].filter(Boolean).join(', ')
272
+ const hint = ack.error === '403' || ack.error === '401' || ack.error === 'not-authorized' || ack.error === 'forbidden'
273
+ ? '; the channel may be missing the CHANNEL_STATUS_PRODUCER capability, check newsletterCanPostStatus(jid)'
274
+ : ''
275
+ const error = new Error(`Newsletter status rejected by server (${ack.error})${details ? `: ${details}` : ''}${hint}`)
276
+ error.data = ack
277
+ throw error
278
+ }
279
+
280
+ return ack
281
+ }
282
+
283
+ export const buildNewsletterAdminProfileStatusMessage = message => {
284
+ if (!message || typeof message !== 'object') throw new TypeError('Newsletter status message is required')
285
+ return proto.Message.create({
286
+ newsletterAdminProfileStatusMessage: proto.Message.FutureProofMessage.create({
287
+ message
288
+ })
289
+ })
290
+ }
291
+
292
+ const resolveStatusPayload = ({ message, payload }) => {
293
+ if (payload !== undefined) {
294
+ if (!(payload instanceof Uint8Array)) throw new TypeError('Newsletter status payload must be a Uint8Array')
295
+ return payload
296
+ }
297
+ if (!message || typeof message !== 'object') throw new TypeError('Newsletter status message is required')
298
+ return encodeNewsletterMessage(message)
299
+ }
300
+
301
+ export const buildNewsletterStatusNode = ({
302
+ jid,
303
+ message,
304
+ payload,
305
+ messageId,
306
+ mediaType,
307
+ mediaId,
308
+ mediaHandle,
309
+ parentServerId,
310
+ responseServerId,
311
+ interactionType,
312
+ aiContent
313
+ }) => {
314
+ assertNewsletterJid(jid)
315
+ if (!messageId) throw new TypeError('Newsletter status messageId is required')
316
+ assertMediaType(mediaType)
317
+ assertInteraction(interactionType)
318
+
319
+ const handle = isPresent(mediaHandle) ? mediaHandle : mediaId
320
+ if (isPresent(handle) && !mediaType) {
321
+ throw new TypeError('mediaId requires a media newsletter status')
322
+ }
323
+ if (mediaType && !isPresent(handle)) {
324
+ throw new TypeError('Native newsletter status media requires the media handle returned by the newsletter upload')
325
+ }
326
+ if (interactionType === 'question_reshare' && !mediaType) {
327
+ throw new TypeError('question_reshare requires media')
328
+ }
329
+ if (interactionType === 'question_response') {
330
+ if (mediaType) throw new TypeError('question_response is published as a text status')
331
+ if (!isPresent(parentServerId)) throw new TypeError('question_response requires parentServerId')
332
+ }
333
+
334
+ const attrs = {
335
+ to: jid,
336
+ id: messageId,
337
+ type: mediaType ? 'media' : 'text'
338
+ }
339
+ if (mediaType) attrs.media_id = String(handle)
340
+ if (interactionType === 'question_response') {
341
+ attrs.server_id = toServerId(parentServerId, 'parentServerId')
342
+ }
343
+
344
+ const content = [{
345
+ tag: 'plaintext',
346
+ attrs: mediaType ? { mediatype: mediaType } : {},
347
+ content: resolveStatusPayload({ message, payload })
348
+ }]
349
+
350
+ const metaNode = buildMetaNode({ interactionType, parentServerId, responseServerId, aiContent })
351
+ if (metaNode) content.push(metaNode)
352
+
353
+ return { tag: 'status', attrs, content }
354
+ }
355
+
356
+ export const buildNewsletterStatusReactionNode = ({ jid, messageId, parentServerId, reaction }) => {
357
+ assertNewsletterJid(jid)
358
+ if (!messageId) throw new TypeError('Newsletter status reaction messageId is required')
359
+ if (!isPresent(parentServerId)) {
360
+ throw new TypeError('Newsletter status reaction requires parentServerId')
361
+ }
362
+
363
+ const isRevoke = !isPresent(reaction)
364
+ const attrs = {
365
+ to: jid,
366
+ id: messageId,
367
+ server_id: toServerId(parentServerId, 'parentServerId'),
368
+ type: 'reaction'
369
+ }
370
+ if (isRevoke) attrs.edit = NEWSLETTER_STATUS_EDIT_REACTION_REVOKE
371
+
372
+ return {
373
+ tag: 'status',
374
+ attrs,
375
+ content: [{
376
+ tag: 'reaction',
377
+ attrs: isRevoke ? {} : { code: reaction },
378
+ content: undefined
379
+ }]
380
+ }
381
+ }
382
+
383
+ export const buildNewsletterStatusRevokeNode = ({ jid, statusId }) => {
384
+ assertNewsletterJid(jid)
385
+ if (!statusId) throw new TypeError('Newsletter status revoke requires the status id')
386
+
387
+ return {
388
+ tag: 'status',
389
+ attrs: {
390
+ to: jid,
391
+ id: statusId,
392
+ type: 'text',
393
+ edit: NEWSLETTER_STATUS_EDIT_ADMIN_REVOKE
394
+ },
395
+ content: [{ tag: 'plaintext', attrs: {}, content: undefined }]
396
+ }
397
+ }
398
+
399
+ const sendStatusNode = async (sock, node, { jid, messageId, timeoutMs }) => {
400
+ const diagnostics = collectStatusCallbacks(sock)
401
+ const responsePromise = sock.waitForMessage(messageId, timeoutMs)
402
+
403
+ try {
404
+ await sock.sendNode(node)
405
+ const response = await responsePromise
406
+ return assertStatusServerResponse(response, { jid, messageId, callbacks: diagnostics.frames })
407
+ }
408
+ finally {
409
+ diagnostics.stop()
410
+ }
411
+ }
412
+
413
+ export const makeNewsletterStatusSender = (sock, config) => async (jid, content, options = {}) => {
414
+ assertNewsletterJid(jid)
415
+ if (!content || typeof content !== 'object' || Array.isArray(content)) {
416
+ throw new TypeError('Newsletter status content must be an object')
417
+ }
418
+
419
+ const userJid = sock.authState?.creds?.me?.id
420
+ if (!userJid) throw new TypeError('Not authenticated')
421
+
422
+ const {
423
+ mediaId,
424
+ mediaHandle: requestedMediaHandle,
425
+ parentServerId,
426
+ responseServerId,
427
+ interactionType: requestedInteractionType,
428
+ aiContent,
429
+ statusAttribution = true,
430
+ messageId: requestedMessageId,
431
+ ackTimeoutMs,
432
+ resolveServerId,
433
+ serverIdTimeoutMs,
434
+ transport,
435
+ ...messageOptions
436
+ } = options
437
+
438
+ if (transport !== undefined) {
439
+ config?.logger?.warn?.({ transport }, 'newsletter status "transport" option is obsolete, the verified status-publish transport is always used')
440
+ }
441
+
442
+ let uploadedMediaHandle
443
+ const upload = async (...args) => {
444
+ const result = await sock.waUploadToServer(...args)
445
+ if (args[1]?.newsletter) {
446
+ uploadedMediaHandle = result?.handle ?? result?.media_id ?? result?.mediaId ?? result?.fbid ?? uploadedMediaHandle
447
+ }
448
+ return result
449
+ }
450
+
451
+ const preparedContent = prepareNewsletterStatusContent(content)
452
+ const fullMsg = await generateWAMessage(jid, statusAttribution ? withNewsletterStatusAttribution(preparedContent) : preparedContent, {
453
+ logger: config?.logger,
454
+ userJid,
455
+ upload,
456
+ mediaCache: config?.mediaCache,
457
+ options: config?.options,
458
+ ...messageOptions,
459
+ messageId: requestedMessageId || generateMessageIDV2(userJid)
460
+ })
461
+
462
+ const normalized = normalizeMessageContent(fullMsg.message)
463
+ const mediaType = getNewsletterStatusMediaType(fullMsg.message)
464
+ if (!mediaType && (normalized?.documentMessage || normalized?.stickerMessage)) {
465
+ throw new TypeError('Native newsletter status supports text, image, video, gif, and audio')
466
+ }
467
+ if (mediaType && !NEWSLETTER_STATUS_WEB_MEDIA_TYPES.has(mediaType)) {
468
+ config?.logger?.warn?.({ mediaType }, 'newsletter status media type is not published by WhatsApp Web itself, the server may reject it')
469
+ }
470
+
471
+ const interactionType = requestedInteractionType || (content.question ? 'question' : undefined)
472
+ if (interactionType === 'question' && !mediaType) {
473
+ config?.logger?.warn?.('WhatsApp Web only publishes question newsletter statuses on top of media, a text question status may be rejected')
474
+ }
475
+
476
+ const node = buildNewsletterStatusNode({
477
+ jid,
478
+ message: fullMsg.message,
479
+ messageId: fullMsg.key.id,
480
+ mediaType,
481
+ mediaHandle: requestedMediaHandle ?? mediaId ?? uploadedMediaHandle,
482
+ parentServerId,
483
+ responseServerId,
484
+ interactionType,
485
+ aiContent
486
+ })
487
+
488
+ const echo = resolveServerId === false
489
+ ? null
490
+ : waitForNewsletterStatusServerId(sock, {
491
+ jid,
492
+ messageId: fullMsg.key.id,
493
+ timeoutMs: serverIdTimeoutMs ?? NEWSLETTER_STATUS_SERVER_ID_TIMEOUT_MS
494
+ })
495
+
496
+ let ack
497
+ try {
498
+ ack = await sendStatusNode(sock, node, {
499
+ jid,
500
+ messageId: fullMsg.key.id,
501
+ timeoutMs: ackTimeoutMs ?? NEWSLETTER_STATUS_ACK_TIMEOUT_MS
502
+ })
503
+ }
504
+ catch (error) {
505
+ echo?.cancel()
506
+ throw error
507
+ }
508
+
509
+ const delivered = echo ? await echo : undefined
510
+
511
+ fullMsg.status = WAMessageStatus.SERVER_ACK
512
+ fullMsg.newsletterStatusServerId = ack.serverId ?? delivered?.serverId
513
+ fullMsg.newsletterStatusAck = ack
514
+ fullMsg.newsletterStatusResponse = ack.node
515
+ if (delivered) fullMsg.newsletterStatusDelivered = delivered.node
516
+ return fullMsg
517
+ }
518
+
519
+ export const makeNewsletterStatusReactionSender = sock => async (jid, parentServerId, reaction, options = {}) => {
520
+ assertNewsletterJid(jid)
521
+ const userJid = sock.authState?.creds?.me?.id
522
+ if (!userJid) throw new TypeError('Not authenticated')
523
+
524
+ const messageId = options.messageId || generateMessageIDV2(userJid)
525
+ const node = buildNewsletterStatusReactionNode({ jid, messageId, parentServerId, reaction })
526
+ const ack = await sendStatusNode(sock, node, {
527
+ jid,
528
+ messageId,
529
+ timeoutMs: options.ackTimeoutMs ?? NEWSLETTER_STATUS_ACK_TIMEOUT_MS
530
+ })
531
+
532
+ return {
533
+ key: {
534
+ remoteJid: jid,
535
+ fromMe: true,
536
+ id: messageId
537
+ },
538
+ status: WAMessageStatus.SERVER_ACK,
539
+ newsletterStatusServerId: ack.serverId,
540
+ newsletterStatusAck: ack,
541
+ newsletterStatusResponse: ack.node
542
+ }
543
+ }
544
+
545
+ export const makeNewsletterStatusRevokeSender = sock => async (jid, statusId, options = {}) => {
546
+ assertNewsletterJid(jid)
547
+ const userJid = sock.authState?.creds?.me?.id
548
+ if (!userJid) throw new TypeError('Not authenticated')
549
+
550
+ const node = buildNewsletterStatusRevokeNode({ jid, statusId })
551
+ const ack = await sendStatusNode(sock, node, {
552
+ jid,
553
+ messageId: statusId,
554
+ timeoutMs: options.ackTimeoutMs ?? NEWSLETTER_STATUS_ACK_TIMEOUT_MS
555
+ })
556
+
557
+ return {
558
+ key: {
559
+ remoteJid: jid,
560
+ fromMe: true,
561
+ id: statusId
562
+ },
563
+ status: WAMessageStatus.SERVER_ACK,
564
+ newsletterStatusAck: ack,
565
+ newsletterStatusResponse: ack.node
566
+ }
567
+ }
568
+
569
+ const decodeStatusPayload = plaintext => {
570
+ const bytes = plaintext?.content
571
+ if (!bytes || Array.isArray(bytes)) return undefined
572
+ return proto.Message.decode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes))
573
+ }
574
+
575
+ const parseStatusAdminProfile = meta => {
576
+ const adminProfile = meta && getBinaryNodeChild(meta, 'admin_profile')
577
+ if (!adminProfile) return undefined
578
+
579
+ const name = getBinaryNodeChild(adminProfile, 'name')
580
+ const picture = getBinaryNodeChild(adminProfile, 'picture')
581
+ const content = name?.content
582
+ return {
583
+ id: adminProfile.attrs?.id,
584
+ name: typeof content === 'string'
585
+ ? content
586
+ : content instanceof Uint8Array
587
+ ? Buffer.from(content).toString('utf-8')
588
+ : undefined,
589
+ pictureId: picture?.attrs?.id,
590
+ pictureDirectPath: picture?.attrs?.direct_path
591
+ }
592
+ }
593
+
594
+ const parseStatusNode = status => {
595
+ const plaintext = getBinaryNodeChild(status, 'plaintext')
596
+ const reaction = getBinaryNodeChild(status, 'reaction')
597
+ const meta = getBinaryNodeChild(status, 'meta')
598
+ const reactions = getBinaryNodeChild(status, 'reactions')
599
+ const viewsCount = getBinaryNodeChild(status, 'views_count')
600
+ const responsesCount = getBinaryNodeChild(status, 'responses_count')
601
+ return {
602
+ id: status.attrs?.id,
603
+ serverId: readIntAttr(status.attrs, 'server_id'),
604
+ t: readIntAttr(status.attrs, 't'),
605
+ isSender: status.attrs?.is_sender === 'true',
606
+ type: status.attrs?.type,
607
+ edit: status.attrs?.edit,
608
+ mediaType: plaintext?.attrs?.mediatype,
609
+ interactionType: meta?.attrs?.interaction_type,
610
+ reaction: reaction ? reaction.attrs?.code ?? '' : undefined,
611
+ adminProfile: parseStatusAdminProfile(meta),
612
+ paidPartnership: meta ? !!getBinaryNodeChild(meta, 'paid_partnership') : false,
613
+ aiContent: meta ? !!getBinaryNodeChild(meta, 'ai_content') : false,
614
+ editTimestamp: readIntAttr(meta?.attrs, 'msg_edit_t'),
615
+ originalTimestamp: readIntAttr(meta?.attrs, 'original_msg_t'),
616
+ reactionCounts: reactions
617
+ ? getBinaryNodeChildren(reactions, 'reaction').map(entry => ({
618
+ code: entry.attrs?.code,
619
+ count: readIntAttr(entry.attrs, 'count')
620
+ }))
621
+ : undefined,
622
+ viewsCount: readIntAttr(viewsCount?.attrs, 'count'),
623
+ responsesCount: readIntAttr(responsesCount?.attrs, 'count'),
624
+ message: decodeStatusPayload(plaintext),
625
+ node: status
626
+ }
627
+ }
628
+
629
+ export const parseNewsletterStatusUpdatesResponse = node => {
630
+ const updates = getBinaryNodeChild(node, 'status_updates')
631
+ if (!updates) {
632
+ const children = Array.isArray(node?.content) ? node.content.map(child => child?.tag) : []
633
+ const error = new Error(`Newsletter status updates response has no <status_updates> child, got [${children.join(', ')}]`)
634
+ error.data = node
635
+ throw error
636
+ }
637
+
638
+ return parseNewsletterStatusesResponse(updates)
639
+ }
640
+
641
+ export const makeNewsletterStatusUpdatesFetcher = sock => async (jid, options = {}) => {
642
+ assertNewsletterJid(jid)
643
+
644
+ const { count = 20, since, before, after } = options
645
+ const attrs = { count: String(count) }
646
+ if (isPresent(since)) attrs.since = String(since)
647
+ if (isPresent(before)) attrs.before = toServerId(before, 'before')
648
+ else if (isPresent(after)) attrs.after = toServerId(after, 'after')
649
+
650
+ const result = await sock.query({
651
+ tag: 'iq',
652
+ attrs: {
653
+ to: jid,
654
+ xmlns: 'newsletter',
655
+ type: 'get'
656
+ },
657
+ content: [{ tag: 'status_updates', attrs, content: undefined }]
658
+ })
659
+
660
+ return parseNewsletterStatusUpdatesResponse(result)
661
+ }
662
+
663
+ export const parseNewsletterStatusesResponse = node => {
664
+ const statuses = getBinaryNodeChild(node, 'statuses')
665
+ if (!statuses) {
666
+ const children = Array.isArray(node?.content) ? node.content.map(child => child?.tag) : []
667
+ const error = new Error(`Newsletter statuses response has no <statuses> child, got [${children.join(', ')}]`)
668
+ error.data = node
669
+ throw error
670
+ }
671
+
672
+ return {
673
+ jid: statuses.attrs?.jid,
674
+ t: readIntAttr(statuses.attrs, 't'),
675
+ statuses: getBinaryNodeChildren(statuses, 'status').map(parseStatusNode)
676
+ }
677
+ }
678
+
679
+ export const makeNewsletterStatusFetcher = sock => async (jid, options = {}) => {
680
+ assertNewsletterJid(jid)
681
+
682
+ const { count = 20, before, after, viewRole } = options
683
+ const attrs = { type: 'jid', jid }
684
+ if (isPresent(viewRole)) attrs.view_role = String(viewRole).toLowerCase()
685
+ attrs.count = String(count)
686
+ if (isPresent(before)) attrs.before = toServerId(before, 'before')
687
+ else if (isPresent(after)) attrs.after = toServerId(after, 'after')
688
+
689
+ const result = await sock.query({
690
+ tag: 'iq',
691
+ attrs: {
692
+ to: S_WHATSAPP_NET,
693
+ xmlns: 'newsletter',
694
+ type: 'get'
695
+ },
696
+ content: [{ tag: 'statuses', attrs, content: undefined }]
697
+ })
698
+
699
+ return parseNewsletterStatusesResponse(result)
700
+ }
701
+
702
+ export const sendNewsletterStatus = (sock, inputOrJid, content, options = {}) => {
703
+ let jid;
704
+ let actualContent;
705
+ let actualOptions;
706
+
707
+ if (typeof inputOrJid === "object" && inputOrJid !== null && ("jid" in inputOrJid || "content" in inputOrJid)) {
708
+ const { jid: targetJid, content: targetContent, ...rest } = inputOrJid;
709
+ jid = targetJid;
710
+ actualContent = targetContent;
711
+ actualOptions = { ...rest, ...content };
712
+ } else {
713
+ jid = inputOrJid;
714
+ actualContent = content;
715
+ actualOptions = options;
716
+ }
717
+
718
+ const sender = makeNewsletterStatusSender(sock, sock?.config);
719
+ return sender(jid, actualContent, actualOptions);
720
+ };
721
+
722
+ export const sendNewsletterStatusReaction = (sock, jid, parentServerId, reaction, options) => {
723
+ return makeNewsletterStatusReactionSender(sock)(jid, parentServerId, reaction, options);
724
+ };
725
+
726
+ export const revokeNewsletterStatus = (sock, jid, serverId, options) => {
727
+ return makeNewsletterStatusRevokeSender(sock)(jid, serverId, options);
728
+ };
729
+
730
+ export const getNewsletterStatuses = (sock, jid, options) => {
731
+ return makeNewsletterStatusFetcher(sock)(jid, options);
732
+ };
733
+
734
+ export const getNewsletterStatusUpdates = (sock, jid, options) => {
735
+ return makeNewsletterStatusUpdatesFetcher(sock)(jid, options);
736
+ };
737
+
738
+ export { NEWSLETTER_STATUS_CONTENT_TYPES, NEWSLETTER_STATUS_MEDIA_TYPES, NEWSLETTER_STATUS_WEB_MEDIA_TYPES }