@prototypevip/baileys 0.1.3 → 0.1.5

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.
@@ -0,0 +1,652 @@
1
+ import { proto } from '../../WAProto/index.js'
2
+ import { encodeNewsletterMessage, generateMessageIDV2 } from './generics.js'
3
+ import { generateWAMessage, normalizeMessageContent } from './messages.js'
4
+ import { prepareModernMessageContent } from './modern-messages.js'
5
+ import { WAMessageStatus } from '../Types/Message.js'
6
+ import { getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET } from '../WABinary/index.js'
7
+ import { isJidNewsletter } from '../WABinary/jid-utils.js'
8
+
9
+ const NEWSLETTER_STATUS_MEDIA_TYPES = new Set(['audio', 'gif', 'image', 'video'])
10
+ const NEWSLETTER_STATUS_WEB_MEDIA_TYPES = new Set(['image', 'video'])
11
+ const NEWSLETTER_STATUS_INTERACTIONS = new Set(['question', 'question_response', 'question_reshare'])
12
+ const NEWSLETTER_STATUS_CONTENT_TYPES = new Set(['text', 'media', 'reaction'])
13
+ const NEWSLETTER_STATUS_CALLBACK_EVENTS = ['CB:ack', 'CB:status', 'CB:iq', 'CB:notification', 'CB:message']
14
+ const NEWSLETTER_STATUS_EDIT_REACTION_REVOKE = '7'
15
+ const NEWSLETTER_STATUS_EDIT_ADMIN_REVOKE = '8'
16
+ const NEWSLETTER_STATUS_ACK_CLASS = 'status'
17
+ const NEWSLETTER_STATUS_ACK_TIMEOUT_MS = 20000
18
+ const NEWSLETTER_STATUS_SERVER_ID_TIMEOUT_MS = 8000
19
+
20
+ export const getNewsletterStatusMediaType = message => {
21
+ const content = normalizeMessageContent(message)
22
+ if (content?.imageMessage) return 'image'
23
+ if (content?.videoMessage) return content.videoMessage.gifPlayback ? 'gif' : 'video'
24
+ if (content?.audioMessage) return 'audio'
25
+ return undefined
26
+ }
27
+
28
+ const assertNewsletterJid = jid => {
29
+ if (!isJidNewsletter(jid)) throw new TypeError('Newsletter status target must be a @newsletter JID')
30
+ }
31
+
32
+ const assertInteraction = interactionType => {
33
+ if (interactionType !== undefined && !NEWSLETTER_STATUS_INTERACTIONS.has(interactionType)) {
34
+ throw new TypeError(`Unsupported newsletter status interaction: ${interactionType}`)
35
+ }
36
+ }
37
+
38
+ const assertMediaType = mediaType => {
39
+ if (mediaType !== undefined && !NEWSLETTER_STATUS_MEDIA_TYPES.has(mediaType)) {
40
+ throw new TypeError(`Unsupported newsletter status media type: ${mediaType}`)
41
+ }
42
+ }
43
+
44
+ const isPresent = value => value !== undefined && value !== null && value !== ''
45
+
46
+ export const NEWSLETTER_SERVER_ID_MIN = 99
47
+ export const NEWSLETTER_SERVER_ID_MAX = 2147476647
48
+
49
+ export const toNewsletterServerId = (value, label) => {
50
+ const parsed = typeof value === 'number' ? value : Number(String(value).trim())
51
+ if (!Number.isInteger(parsed) || parsed < NEWSLETTER_SERVER_ID_MIN || parsed > NEWSLETTER_SERVER_ID_MAX) {
52
+ 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`)
53
+ }
54
+ return String(parsed)
55
+ }
56
+
57
+ const toServerId = (value, label) => toNewsletterServerId(value, label)
58
+
59
+ const buildMetaNode = ({ interactionType, parentServerId, responseServerId, aiContent }) => {
60
+ assertInteraction(interactionType)
61
+
62
+ const attrs = {}
63
+ if (interactionType) {
64
+ attrs.interaction_type = interactionType
65
+ if (interactionType === 'question_reshare') {
66
+ if (!isPresent(parentServerId)) throw new TypeError('question_reshare requires parentServerId')
67
+ if (!isPresent(responseServerId)) throw new TypeError('question_reshare requires responseServerId')
68
+ attrs.parent_server_id = toServerId(parentServerId, 'parentServerId')
69
+ attrs.response_server_id = String(responseServerId)
70
+ }
71
+ else if (interactionType === 'question_response' && isPresent(responseServerId)) {
72
+ attrs.response_server_id = String(responseServerId)
73
+ }
74
+ }
75
+
76
+ if (!interactionType && !aiContent) return undefined
77
+
78
+ return {
79
+ tag: 'meta',
80
+ attrs,
81
+ content: aiContent ? [{ tag: 'ai_content', attrs: {}, content: undefined }] : undefined
82
+ }
83
+ }
84
+
85
+ export const withNewsletterStatusAttribution = content => ({
86
+ ...content,
87
+ contextInfo: {
88
+ statusAttributions: [{ type: proto.StatusAttribution.Type.NEWSLETTER_STATUS }],
89
+ featureEligibilities: { canBeReshared: true },
90
+ ...(content?.contextInfo || {})
91
+ }
92
+ })
93
+
94
+ const snapshotCallbackNode = (event, node) => ({
95
+ event,
96
+ tag: node?.tag,
97
+ attrs: node?.attrs || {},
98
+ children: Array.isArray(node?.content)
99
+ ? node.content.map(child => ({ tag: child?.tag, attrs: child?.attrs || {} }))
100
+ : []
101
+ })
102
+
103
+ const collectStatusCallbacks = sock => {
104
+ const frames = []
105
+ const listeners = []
106
+
107
+ for (const event of NEWSLETTER_STATUS_CALLBACK_EVENTS) {
108
+ const listener = node => {
109
+ if (frames.length >= 40) return
110
+ frames.push(snapshotCallbackNode(event, node))
111
+ }
112
+ sock.ws.on(event, listener)
113
+ listeners.push([event, listener])
114
+ }
115
+
116
+ return {
117
+ frames,
118
+ stop: () => {
119
+ for (const [event, listener] of listeners) {
120
+ sock.ws.off(event, listener)
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ const readIntAttr = (attrs, key) => {
127
+ if (!isPresent(attrs?.[key])) return undefined
128
+ const parsed = Number.parseInt(String(attrs[key]), 10)
129
+ return Number.isFinite(parsed) ? parsed : undefined
130
+ }
131
+
132
+ export const parseNewsletterStatusAck = (node, { jid, messageId } = {}) => {
133
+ if (!node || typeof node !== 'object') throw new TypeError('Newsletter status ack node is required')
134
+ if (node.tag !== 'ack') {
135
+ const error = new Error(`Newsletter status expected <ack>, got <${node.tag}>`)
136
+ error.data = node
137
+ throw error
138
+ }
139
+
140
+ const attrs = node.attrs || {}
141
+ if (attrs.class !== undefined && attrs.class !== NEWSLETTER_STATUS_ACK_CLASS) {
142
+ const error = new Error(`Newsletter status ack has class "${attrs.class}", expected "${NEWSLETTER_STATUS_ACK_CLASS}"`)
143
+ error.data = node
144
+ throw error
145
+ }
146
+ if (isPresent(messageId) && isPresent(attrs.id) && attrs.id !== messageId) {
147
+ const error = new Error(`Newsletter status ack id mismatch: ${attrs.id} != ${messageId}`)
148
+ error.data = node
149
+ throw error
150
+ }
151
+ if (isPresent(jid) && isPresent(attrs.from) && attrs.from !== jid) {
152
+ const error = new Error(`Newsletter status ack from mismatch: ${attrs.from} != ${jid}`)
153
+ error.data = node
154
+ throw error
155
+ }
156
+
157
+ return {
158
+ class: attrs.class,
159
+ from: attrs.from,
160
+ id: attrs.id,
161
+ t: readIntAttr(attrs, 't'),
162
+ serverId: readIntAttr(attrs, 'server_id'),
163
+ edit: attrs.edit,
164
+ error: isPresent(attrs.error) ? String(attrs.error) : undefined,
165
+ applicationError: readIntAttr(attrs, 'application_error'),
166
+ backoff: readIntAttr(attrs, 'backoff'),
167
+ node
168
+ }
169
+ }
170
+
171
+ /**
172
+ * The ack carries from, class, id and t, and nothing else. The server id lands
173
+ * separately, in the <status> stanza the server sends back to the publisher --
174
+ * WASmaxInStatusDeliverFromNewsletterMixin reads from, server_id, t and
175
+ * is_sender off exactly that tag. Reacting to or revoking your own status needs
176
+ * that id, so wait a moment for the echo rather than returning undefined.
177
+ */
178
+ export const waitForNewsletterStatusServerId = (sock, { jid, messageId, timeoutMs = 8000 }) => {
179
+ let settle
180
+ const promise = new Promise(resolve => {
181
+ let done = false
182
+ const finish = value => {
183
+ if (done) return
184
+ done = true
185
+ clearTimeout(timer)
186
+ sock.ws.off('CB:status', onStatus)
187
+ resolve(value)
188
+ }
189
+ const onStatus = node => {
190
+ if (node?.tag !== 'status') return
191
+ const attrs = node.attrs || {}
192
+ if (isPresent(jid) && isPresent(attrs.from) && attrs.from !== jid) return
193
+ if (isPresent(messageId) && isPresent(attrs.id) && attrs.id !== messageId) return
194
+ const serverId = readIntAttr(attrs, 'server_id')
195
+ if (serverId !== undefined) finish({ serverId, node })
196
+ }
197
+ const timer = setTimeout(() => finish(undefined), timeoutMs)
198
+ sock.ws.on('CB:status', onStatus)
199
+ settle = finish
200
+ })
201
+ promise.cancel = () => settle(undefined)
202
+ return promise
203
+ }
204
+
205
+ const assertStatusServerResponse = (response, { jid, messageId, callbacks = [] }) => {
206
+ if (!response) {
207
+ const suffix = callbacks.length
208
+ ? `; callbacks=${JSON.stringify(callbacks)}`
209
+ : '; no ack/status/iq/notification/message callback observed'
210
+ const error = new Error(`Newsletter status server ACK timed out for ${messageId}${suffix}`)
211
+ error.data = { messageId, callbacks }
212
+ throw error
213
+ }
214
+
215
+ const ack = parseNewsletterStatusAck(response, { jid, messageId })
216
+ if (ack.error) {
217
+ const details = [
218
+ ack.applicationError !== undefined ? `application_error=${ack.applicationError}` : undefined,
219
+ ack.backoff !== undefined ? `backoff=${ack.backoff}` : undefined
220
+ ].filter(Boolean).join(', ')
221
+ const hint = ack.error === '403' || ack.error === '401' || ack.error === 'not-authorized' || ack.error === 'forbidden'
222
+ ? '; the channel may be missing the CHANNEL_STATUS_PRODUCER capability, check newsletterCanPostStatus(jid)'
223
+ : ''
224
+ const error = new Error(`Newsletter status rejected by server (${ack.error})${details ? `: ${details}` : ''}${hint}`)
225
+ error.data = ack
226
+ throw error
227
+ }
228
+
229
+ return ack
230
+ }
231
+
232
+ export const buildNewsletterAdminProfileStatusMessage = message => {
233
+ if (!message || typeof message !== 'object') throw new TypeError('Newsletter status message is required')
234
+ return proto.Message.create({
235
+ newsletterAdminProfileStatusMessage: proto.Message.FutureProofMessage.create({
236
+ message
237
+ })
238
+ })
239
+ }
240
+
241
+ const resolveStatusPayload = ({ message, payload }) => {
242
+ if (payload !== undefined) {
243
+ if (!(payload instanceof Uint8Array)) throw new TypeError('Newsletter status payload must be a Uint8Array')
244
+ return payload
245
+ }
246
+ if (!message || typeof message !== 'object') throw new TypeError('Newsletter status message is required')
247
+ return encodeNewsletterMessage(message)
248
+ }
249
+
250
+ export const buildNewsletterStatusNode = ({
251
+ jid,
252
+ message,
253
+ payload,
254
+ messageId,
255
+ mediaType,
256
+ mediaId,
257
+ mediaHandle,
258
+ parentServerId,
259
+ responseServerId,
260
+ interactionType,
261
+ aiContent
262
+ }) => {
263
+ assertNewsletterJid(jid)
264
+ if (!messageId) throw new TypeError('Newsletter status messageId is required')
265
+ assertMediaType(mediaType)
266
+ assertInteraction(interactionType)
267
+
268
+ const handle = isPresent(mediaHandle) ? mediaHandle : mediaId
269
+ if (isPresent(handle) && !mediaType) {
270
+ throw new TypeError('mediaId requires a media newsletter status')
271
+ }
272
+ if (mediaType && !isPresent(handle)) {
273
+ throw new TypeError('Native newsletter status media requires the media handle returned by the newsletter upload')
274
+ }
275
+ if (interactionType === 'question_reshare' && !mediaType) {
276
+ throw new TypeError('question_reshare requires media')
277
+ }
278
+ if (interactionType === 'question_response') {
279
+ if (mediaType) throw new TypeError('question_response is published as a text status')
280
+ if (!isPresent(parentServerId)) throw new TypeError('question_response requires parentServerId')
281
+ }
282
+
283
+ const attrs = {
284
+ to: jid,
285
+ id: messageId,
286
+ type: mediaType ? 'media' : 'text'
287
+ }
288
+ if (mediaType) attrs.media_id = String(handle)
289
+ if (interactionType === 'question_response') {
290
+ attrs.server_id = toServerId(parentServerId, 'parentServerId')
291
+ }
292
+
293
+ const content = [{
294
+ tag: 'plaintext',
295
+ attrs: mediaType ? { mediatype: mediaType } : {},
296
+ content: resolveStatusPayload({ message, payload })
297
+ }]
298
+
299
+ const metaNode = buildMetaNode({ interactionType, parentServerId, responseServerId, aiContent })
300
+ if (metaNode) content.push(metaNode)
301
+
302
+ return { tag: 'status', attrs, content }
303
+ }
304
+
305
+ export const buildNewsletterStatusReactionNode = ({ jid, messageId, parentServerId, reaction }) => {
306
+ assertNewsletterJid(jid)
307
+ if (!messageId) throw new TypeError('Newsletter status reaction messageId is required')
308
+ if (!isPresent(parentServerId)) {
309
+ throw new TypeError('Newsletter status reaction requires parentServerId')
310
+ }
311
+
312
+ const isRevoke = !isPresent(reaction)
313
+ const attrs = {
314
+ to: jid,
315
+ id: messageId,
316
+ server_id: toServerId(parentServerId, 'parentServerId'),
317
+ type: 'reaction'
318
+ }
319
+ if (isRevoke) attrs.edit = NEWSLETTER_STATUS_EDIT_REACTION_REVOKE
320
+
321
+ return {
322
+ tag: 'status',
323
+ attrs,
324
+ content: [{
325
+ tag: 'reaction',
326
+ attrs: isRevoke ? {} : { code: reaction },
327
+ content: undefined
328
+ }]
329
+ }
330
+ }
331
+
332
+ export const buildNewsletterStatusRevokeNode = ({ jid, statusId }) => {
333
+ assertNewsletterJid(jid)
334
+ if (!statusId) throw new TypeError('Newsletter status revoke requires the status id')
335
+
336
+ return {
337
+ tag: 'status',
338
+ attrs: {
339
+ to: jid,
340
+ id: statusId,
341
+ type: 'text',
342
+ edit: NEWSLETTER_STATUS_EDIT_ADMIN_REVOKE
343
+ },
344
+ content: [{ tag: 'plaintext', attrs: {}, content: undefined }]
345
+ }
346
+ }
347
+
348
+ const sendStatusNode = async (sock, node, { jid, messageId, timeoutMs }) => {
349
+ const diagnostics = collectStatusCallbacks(sock)
350
+ const responsePromise = sock.waitForMessage(messageId, timeoutMs)
351
+
352
+ try {
353
+ await sock.sendNode(node)
354
+ const response = await responsePromise
355
+ return assertStatusServerResponse(response, { jid, messageId, callbacks: diagnostics.frames })
356
+ }
357
+ finally {
358
+ diagnostics.stop()
359
+ }
360
+ }
361
+
362
+ export const makeNewsletterStatusSender = (sock, config) => async (jid, content, options = {}) => {
363
+ assertNewsletterJid(jid)
364
+ if (!content || typeof content !== 'object' || Array.isArray(content)) {
365
+ throw new TypeError('Newsletter status content must be an object')
366
+ }
367
+
368
+ const userJid = sock.authState?.creds?.me?.id
369
+ if (!userJid) throw new TypeError('Not authenticated')
370
+
371
+ const {
372
+ mediaId,
373
+ mediaHandle: requestedMediaHandle,
374
+ parentServerId,
375
+ responseServerId,
376
+ interactionType: requestedInteractionType,
377
+ aiContent,
378
+ statusAttribution = true,
379
+ messageId: requestedMessageId,
380
+ ackTimeoutMs,
381
+ resolveServerId,
382
+ serverIdTimeoutMs,
383
+ transport,
384
+ wrapAdminProfile,
385
+ ...messageOptions
386
+ } = options
387
+
388
+ if (transport !== undefined) {
389
+ config.logger?.warn?.({ transport }, 'newsletter status "transport" option is obsolete, the verified status-publish transport is always used')
390
+ }
391
+
392
+ let uploadedMediaHandle
393
+ const upload = async (...args) => {
394
+ const result = await sock.waUploadToServer(...args)
395
+ if (args[1]?.newsletter) {
396
+ uploadedMediaHandle = result?.handle ?? result?.media_id ?? result?.mediaId ?? result?.fbid ?? uploadedMediaHandle
397
+ }
398
+ return result
399
+ }
400
+
401
+ const preparedContent = prepareModernMessageContent(content)
402
+ const fullMsg = await generateWAMessage(jid, statusAttribution ? withNewsletterStatusAttribution(preparedContent) : preparedContent, {
403
+ logger: config.logger,
404
+ userJid,
405
+ upload,
406
+ mediaCache: config.mediaCache,
407
+ options: config.options,
408
+ ...messageOptions,
409
+ messageId: requestedMessageId || generateMessageIDV2(userJid)
410
+ })
411
+
412
+ const normalized = normalizeMessageContent(fullMsg.message)
413
+ const mediaType = getNewsletterStatusMediaType(fullMsg.message)
414
+ if (!mediaType && (normalized?.documentMessage || normalized?.stickerMessage)) {
415
+ throw new TypeError('Native newsletter status supports text, image, video, gif, and audio')
416
+ }
417
+ if (mediaType && !NEWSLETTER_STATUS_WEB_MEDIA_TYPES.has(mediaType)) {
418
+ config.logger?.warn?.({ mediaType }, 'newsletter status media type is not published by WhatsApp Web itself, the server may reject it')
419
+ }
420
+
421
+ const interactionType = requestedInteractionType || (content.question ? 'question' : undefined)
422
+ if (interactionType === 'question' && !mediaType) {
423
+ config.logger?.warn?.('WhatsApp Web only publishes question newsletter statuses on top of media, a text question status may be rejected')
424
+ }
425
+
426
+ const node = buildNewsletterStatusNode({
427
+ jid,
428
+ message: wrapAdminProfile ? buildNewsletterAdminProfileStatusMessage(fullMsg.message) : fullMsg.message,
429
+ messageId: fullMsg.key.id,
430
+ mediaType,
431
+ mediaHandle: requestedMediaHandle ?? mediaId ?? uploadedMediaHandle,
432
+ parentServerId,
433
+ responseServerId,
434
+ interactionType,
435
+ aiContent
436
+ })
437
+
438
+ const echo = resolveServerId === false
439
+ ? null
440
+ : waitForNewsletterStatusServerId(sock, {
441
+ jid,
442
+ messageId: fullMsg.key.id,
443
+ timeoutMs: serverIdTimeoutMs ?? NEWSLETTER_STATUS_SERVER_ID_TIMEOUT_MS
444
+ })
445
+
446
+ let ack
447
+ try {
448
+ ack = await sendStatusNode(sock, node, {
449
+ jid,
450
+ messageId: fullMsg.key.id,
451
+ timeoutMs: ackTimeoutMs ?? NEWSLETTER_STATUS_ACK_TIMEOUT_MS
452
+ })
453
+ }
454
+ catch (error) {
455
+ echo?.cancel()
456
+ throw error
457
+ }
458
+
459
+ const delivered = echo ? await echo : undefined
460
+
461
+ fullMsg.status = WAMessageStatus.SERVER_ACK
462
+ fullMsg.newsletterStatusServerId = ack.serverId ?? delivered?.serverId
463
+ fullMsg.newsletterStatusAck = ack
464
+ fullMsg.newsletterStatusResponse = ack.node
465
+ if (delivered) fullMsg.newsletterStatusDelivered = delivered.node
466
+ return fullMsg
467
+ }
468
+
469
+ export const makeNewsletterStatusReactionSender = sock => async (jid, parentServerId, reaction, options = {}) => {
470
+ assertNewsletterJid(jid)
471
+ const userJid = sock.authState?.creds?.me?.id
472
+ if (!userJid) throw new TypeError('Not authenticated')
473
+
474
+ const messageId = options.messageId || generateMessageIDV2(userJid)
475
+ const node = buildNewsletterStatusReactionNode({ jid, messageId, parentServerId, reaction })
476
+ const ack = await sendStatusNode(sock, node, {
477
+ jid,
478
+ messageId,
479
+ timeoutMs: options.ackTimeoutMs ?? NEWSLETTER_STATUS_ACK_TIMEOUT_MS
480
+ })
481
+
482
+ return {
483
+ key: {
484
+ remoteJid: jid,
485
+ fromMe: true,
486
+ id: messageId
487
+ },
488
+ status: WAMessageStatus.SERVER_ACK,
489
+ newsletterStatusServerId: ack.serverId,
490
+ newsletterStatusAck: ack,
491
+ newsletterStatusResponse: ack.node
492
+ }
493
+ }
494
+
495
+ export const makeNewsletterStatusRevokeSender = sock => async (jid, statusId, options = {}) => {
496
+ assertNewsletterJid(jid)
497
+ const userJid = sock.authState?.creds?.me?.id
498
+ if (!userJid) throw new TypeError('Not authenticated')
499
+
500
+ const node = buildNewsletterStatusRevokeNode({ jid, statusId })
501
+ const ack = await sendStatusNode(sock, node, {
502
+ jid,
503
+ messageId: statusId,
504
+ timeoutMs: options.ackTimeoutMs ?? NEWSLETTER_STATUS_ACK_TIMEOUT_MS
505
+ })
506
+
507
+ return {
508
+ key: {
509
+ remoteJid: jid,
510
+ fromMe: true,
511
+ id: statusId
512
+ },
513
+ status: WAMessageStatus.SERVER_ACK,
514
+ newsletterStatusAck: ack,
515
+ newsletterStatusResponse: ack.node
516
+ }
517
+ }
518
+
519
+ const decodeStatusPayload = plaintext => {
520
+ const bytes = plaintext?.content
521
+ if (!bytes || Array.isArray(bytes)) return undefined
522
+ return proto.Message.decode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes))
523
+ }
524
+
525
+ const parseStatusAdminProfile = meta => {
526
+ const adminProfile = meta && getBinaryNodeChild(meta, 'admin_profile')
527
+ if (!adminProfile) return undefined
528
+
529
+ const name = getBinaryNodeChild(adminProfile, 'name')
530
+ const picture = getBinaryNodeChild(adminProfile, 'picture')
531
+ const content = name?.content
532
+ return {
533
+ id: adminProfile.attrs?.id,
534
+ name: typeof content === 'string'
535
+ ? content
536
+ : content instanceof Uint8Array
537
+ ? Buffer.from(content).toString('utf-8')
538
+ : undefined,
539
+ pictureId: picture?.attrs?.id,
540
+ pictureDirectPath: picture?.attrs?.direct_path
541
+ }
542
+ }
543
+
544
+ const parseStatusNode = status => {
545
+ const plaintext = getBinaryNodeChild(status, 'plaintext')
546
+ const reaction = getBinaryNodeChild(status, 'reaction')
547
+ const meta = getBinaryNodeChild(status, 'meta')
548
+ const reactions = getBinaryNodeChild(status, 'reactions')
549
+ const viewsCount = getBinaryNodeChild(status, 'views_count')
550
+ const responsesCount = getBinaryNodeChild(status, 'responses_count')
551
+ return {
552
+ id: status.attrs?.id,
553
+ serverId: readIntAttr(status.attrs, 'server_id'),
554
+ t: readIntAttr(status.attrs, 't'),
555
+ isSender: status.attrs?.is_sender === 'true',
556
+ type: status.attrs?.type,
557
+ edit: status.attrs?.edit,
558
+ mediaType: plaintext?.attrs?.mediatype,
559
+ interactionType: meta?.attrs?.interaction_type,
560
+ reaction: reaction ? reaction.attrs?.code ?? '' : undefined,
561
+ adminProfile: parseStatusAdminProfile(meta),
562
+ paidPartnership: meta ? !!getBinaryNodeChild(meta, 'paid_partnership') : false,
563
+ aiContent: meta ? !!getBinaryNodeChild(meta, 'ai_content') : false,
564
+ editTimestamp: readIntAttr(meta?.attrs, 'msg_edit_t'),
565
+ originalTimestamp: readIntAttr(meta?.attrs, 'original_msg_t'),
566
+ reactionCounts: reactions
567
+ ? getBinaryNodeChildren(reactions, 'reaction').map(entry => ({
568
+ code: entry.attrs?.code,
569
+ count: readIntAttr(entry.attrs, 'count')
570
+ }))
571
+ : undefined,
572
+ viewsCount: readIntAttr(viewsCount?.attrs, 'count'),
573
+ responsesCount: readIntAttr(responsesCount?.attrs, 'count'),
574
+ message: decodeStatusPayload(plaintext),
575
+ node: status
576
+ }
577
+ }
578
+
579
+ export const parseNewsletterStatusUpdatesResponse = node => {
580
+ const updates = getBinaryNodeChild(node, 'status_updates')
581
+ if (!updates) {
582
+ const children = Array.isArray(node?.content) ? node.content.map(child => child?.tag) : []
583
+ const error = new Error(`Newsletter status updates response has no <status_updates> child, got [${children.join(', ')}]`)
584
+ error.data = node
585
+ throw error
586
+ }
587
+
588
+ return parseNewsletterStatusesResponse(updates)
589
+ }
590
+
591
+ export const makeNewsletterStatusUpdatesFetcher = sock => async (jid, options = {}) => {
592
+ assertNewsletterJid(jid)
593
+
594
+ const { count = 20, since, before, after } = options
595
+ const attrs = { count: String(count) }
596
+ if (isPresent(since)) attrs.since = String(since)
597
+ if (isPresent(before)) attrs.before = toServerId(before, 'before')
598
+ else if (isPresent(after)) attrs.after = toServerId(after, 'after')
599
+
600
+ const result = await sock.query({
601
+ tag: 'iq',
602
+ attrs: {
603
+ to: jid,
604
+ xmlns: 'newsletter',
605
+ type: 'get'
606
+ },
607
+ content: [{ tag: 'status_updates', attrs, content: undefined }]
608
+ })
609
+
610
+ return parseNewsletterStatusUpdatesResponse(result)
611
+ }
612
+
613
+ export const parseNewsletterStatusesResponse = node => {
614
+ const statuses = getBinaryNodeChild(node, 'statuses')
615
+ if (!statuses) {
616
+ const children = Array.isArray(node?.content) ? node.content.map(child => child?.tag) : []
617
+ const error = new Error(`Newsletter statuses response has no <statuses> child, got [${children.join(', ')}]`)
618
+ error.data = node
619
+ throw error
620
+ }
621
+
622
+ return {
623
+ jid: statuses.attrs?.jid,
624
+ t: readIntAttr(statuses.attrs, 't'),
625
+ statuses: getBinaryNodeChildren(statuses, 'status').map(parseStatusNode)
626
+ }
627
+ }
628
+
629
+ export const makeNewsletterStatusFetcher = sock => async (jid, options = {}) => {
630
+ assertNewsletterJid(jid)
631
+
632
+ const { count = 20, before, after, viewRole } = options
633
+ const attrs = { type: 'jid', jid }
634
+ if (isPresent(viewRole)) attrs.view_role = String(viewRole).toLowerCase()
635
+ attrs.count = String(count)
636
+ if (isPresent(before)) attrs.before = toServerId(before, 'before')
637
+ else if (isPresent(after)) attrs.after = toServerId(after, 'after')
638
+
639
+ const result = await sock.query({
640
+ tag: 'iq',
641
+ attrs: {
642
+ to: S_WHATSAPP_NET,
643
+ xmlns: 'newsletter',
644
+ type: 'get'
645
+ },
646
+ content: [{ tag: 'statuses', attrs, content: undefined }]
647
+ })
648
+
649
+ return parseNewsletterStatusesResponse(result)
650
+ }
651
+
652
+ export { NEWSLETTER_STATUS_CONTENT_TYPES, NEWSLETTER_STATUS_MEDIA_TYPES, NEWSLETTER_STATUS_WEB_MEDIA_TYPES }