@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.
- package/HELPERS.md +107 -0
- package/lib/Socket/index.js +30 -15
- package/lib/Utils/index.js +4 -0
- package/lib/Utils/messages-media.js +6 -3
- package/lib/Utils/messages.js +1 -0
- package/lib/Utils/modern-messages.js +10 -0
- package/lib/Utils/newsletter-status.js +652 -0
- package/lib/Utils/prototype-helpers.js +610 -0
- package/lib/Utils/status-helpers.js +143 -0
- package/package.json +11 -4
|
@@ -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 }
|