@prototypevip/baileys 0.1.3 → 0.1.4

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.
@@ -2,15 +2,22 @@ import { DEFAULT_CONNECTION_CONFIG } from "../Defaults/index.js";
2
2
  import { makeCommunitiesSocket } from "./communities.js";
3
3
  import { stampSocket } from "../Utils/socket-guard.js";
4
4
  import { verifyBrandIntegrity } from "../Utils/integrity-guard.js";
5
+ import {
6
+ makeNewsletterStatusSender,
7
+ makeNewsletterStatusReactionSender,
8
+ makeNewsletterStatusRevokeSender,
9
+ makeNewsletterStatusFetcher,
10
+ makeNewsletterStatusUpdatesFetcher,
11
+ } from "../Utils/newsletter-status.js";
12
+ import { sendPersonalStatus, sendGroupStatus } from "../Utils/status-helpers.js";
5
13
 
6
14
  /**
7
15
  * Create a WhatsApp socket.
8
16
  * Clean version — without AntiBan.
17
+ * v0.1.4 — Channel Status + Group Status + Personal Status APIs
9
18
  */
10
19
  const makeWASocket = (config = {}) => {
11
- // بصمة "prototype" بتتفحص هنا تلقائي — قبل حتى ما أي سوكت يتبني، مش
12
- // محتاجه استدعاء يدوي من البوت (نفس فلسفة stampSocket تحت). لو حد فَكّ
13
- // اسم الباكدج أو غيّر بايتات البصمة يدوي، المكتبه بترفض تشتغل خالص.
20
+ // بصمة "prototype" بتتفحص هنا تلقائي — قبل حتى ما أي سوكت يتبني
14
21
  if (!verifyBrandIntegrity()) {
15
22
  try {
16
23
  console.error("[INTEGRITY-GUARD] brand fingerprint mismatch — refusing to start.");
@@ -32,14 +39,7 @@ const makeWASocket = (config = {}) => {
32
39
 
33
40
  const newConfig = { ...DEFAULT_CONNECTION_CONFIG, ...restConfig };
34
41
 
35
- // ملحوظه مهمه: `Utils/generics.js` (ودوال تانيه زيها) بتستورد
36
- // DEFAULT_CONNECTION_CONFIG مباشره كـ `config` وبتقرا منها وقت التشغيل
37
- // (مش الـ newConfig المدموج فوق)، فأي override زي generateMessageID/
38
- // generateMessageIDV2 بيتحط هنا كان بيتضاع تماما - الكود الداخلي في
39
- // المكتبه (زي توليد id الأزرار) كان بيفضل يستخدم القيم الافتراضيه
40
- // الأصليه (GIN4B0T/G1N4B0T) حتى لو المستخدم غيّرها في makeWASocket().
41
- // بنعمل mutate للـ object المشترك نفسه عشان أي import ثابت ليه في باقي
42
- // المكتبه ياخد نفس القيم المدموجه.
42
+ // mutate shared DEFAULT so internal imports see overrides
43
43
  Object.assign(DEFAULT_CONNECTION_CONFIG, newConfig);
44
44
 
45
45
  let sock;
@@ -52,11 +52,26 @@ const makeWASocket = (config = {}) => {
52
52
 
53
53
  sock = makeCommunitiesSocket(newConfig);
54
54
 
55
- // ختم السوكت تلقائي هنا — قبل كده كان لازم البوت ينادي stampSocket()
56
- // يدوي بعد ما السوكت يترجع (وده كان سهل يتنسى في ملف 2300+ سطر زي
57
- // patch.js)، دلوقتي أي سوكت طالع من makeWASocket() بيتختم لوحده.
55
+ // ختم السوكت تلقائي
58
56
  stampSocket(sock);
59
57
 
58
+ // ── Channels & Stories APIs (0.1.4) ──────────────────────────
59
+ // Newsletter / Channel status (Elaina-style transport)
60
+ sock.sendNewsletterStatus = makeNewsletterStatusSender(sock, newConfig);
61
+ sock.sendChannelStatus = sock.sendNewsletterStatus; // alias
62
+ sock.sendNewsletterStatusReaction = makeNewsletterStatusReactionSender(sock);
63
+ sock.revokeNewsletterStatus = makeNewsletterStatusRevokeSender(sock);
64
+ sock.getNewsletterStatuses = makeNewsletterStatusFetcher(sock);
65
+ sock.getNewsletterStatusUpdates = makeNewsletterStatusUpdatesFetcher(sock);
66
+
67
+ // Personal story (status@broadcast)
68
+ sock.sendPersonalStatus = (content, statusJidList, options) =>
69
+ sendPersonalStatus(sock, content, statusJidList, options);
70
+
71
+ // Group story (groupStatusMessageV2 / groupStatus flag)
72
+ sock.sendGroupStatus = (groupJid, content, options) =>
73
+ sendGroupStatus(sock, groupJid, content, options);
74
+
60
75
  return sock;
61
76
  };
62
77
 
@@ -67,4 +82,4 @@ const wrapSocketWithFingerprint = (makeSock, opts) =>
67
82
  const PRESETS = {};
68
83
 
69
84
  export default makeWASocket;
70
- export { makeWASocket, wrapSocket, wrapSocketWithFingerprint, PRESETS };
85
+ export { makeWASocket, wrapSocket, wrapSocketWithFingerprint, PRESETS };
@@ -31,3 +31,6 @@ export * from "./ai-rich-formatter.js";
31
31
  export * from "./constants.js";
32
32
  export * from "./socket-guard.js";
33
33
  export * from "./integrity-guard.js";
34
+ export * from "./newsletter-status.js";
35
+ export * from "./status-helpers.js";
36
+ export * from "./modern-messages.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Minimal modern-messages shim for @prototypevip/baileys
3
+ * Full Elaina modern message builders are optional; status APIs only need identity transform.
4
+ */
5
+ export const prepareModernMessageContent = (content) => {
6
+ if (!content || typeof content !== "object") return content;
7
+ return content;
8
+ };
9
+
10
+ export default { prepareModernMessageContent };
@@ -0,0 +1,651 @@
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
+ ...messageOptions
385
+ } = options
386
+
387
+ if (transport !== undefined) {
388
+ config.logger?.warn?.({ transport }, 'newsletter status "transport" option is obsolete, the verified status-publish transport is always used')
389
+ }
390
+
391
+ let uploadedMediaHandle
392
+ const upload = async (...args) => {
393
+ const result = await sock.waUploadToServer(...args)
394
+ if (args[1]?.newsletter) {
395
+ uploadedMediaHandle = result?.handle ?? result?.media_id ?? result?.mediaId ?? result?.fbid ?? uploadedMediaHandle
396
+ }
397
+ return result
398
+ }
399
+
400
+ const preparedContent = prepareModernMessageContent(content)
401
+ const fullMsg = await generateWAMessage(jid, statusAttribution ? withNewsletterStatusAttribution(preparedContent) : preparedContent, {
402
+ logger: config.logger,
403
+ userJid,
404
+ upload,
405
+ mediaCache: config.mediaCache,
406
+ options: config.options,
407
+ ...messageOptions,
408
+ messageId: requestedMessageId || generateMessageIDV2(userJid)
409
+ })
410
+
411
+ const normalized = normalizeMessageContent(fullMsg.message)
412
+ const mediaType = getNewsletterStatusMediaType(fullMsg.message)
413
+ if (!mediaType && (normalized?.documentMessage || normalized?.stickerMessage)) {
414
+ throw new TypeError('Native newsletter status supports text, image, video, gif, and audio')
415
+ }
416
+ if (mediaType && !NEWSLETTER_STATUS_WEB_MEDIA_TYPES.has(mediaType)) {
417
+ config.logger?.warn?.({ mediaType }, 'newsletter status media type is not published by WhatsApp Web itself, the server may reject it')
418
+ }
419
+
420
+ const interactionType = requestedInteractionType || (content.question ? 'question' : undefined)
421
+ if (interactionType === 'question' && !mediaType) {
422
+ config.logger?.warn?.('WhatsApp Web only publishes question newsletter statuses on top of media, a text question status may be rejected')
423
+ }
424
+
425
+ const node = buildNewsletterStatusNode({
426
+ jid,
427
+ message: fullMsg.message,
428
+ messageId: fullMsg.key.id,
429
+ mediaType,
430
+ mediaHandle: requestedMediaHandle ?? mediaId ?? uploadedMediaHandle,
431
+ parentServerId,
432
+ responseServerId,
433
+ interactionType,
434
+ aiContent
435
+ })
436
+
437
+ const echo = resolveServerId === false
438
+ ? null
439
+ : waitForNewsletterStatusServerId(sock, {
440
+ jid,
441
+ messageId: fullMsg.key.id,
442
+ timeoutMs: serverIdTimeoutMs ?? NEWSLETTER_STATUS_SERVER_ID_TIMEOUT_MS
443
+ })
444
+
445
+ let ack
446
+ try {
447
+ ack = await sendStatusNode(sock, node, {
448
+ jid,
449
+ messageId: fullMsg.key.id,
450
+ timeoutMs: ackTimeoutMs ?? NEWSLETTER_STATUS_ACK_TIMEOUT_MS
451
+ })
452
+ }
453
+ catch (error) {
454
+ echo?.cancel()
455
+ throw error
456
+ }
457
+
458
+ const delivered = echo ? await echo : undefined
459
+
460
+ fullMsg.status = WAMessageStatus.SERVER_ACK
461
+ fullMsg.newsletterStatusServerId = ack.serverId ?? delivered?.serverId
462
+ fullMsg.newsletterStatusAck = ack
463
+ fullMsg.newsletterStatusResponse = ack.node
464
+ if (delivered) fullMsg.newsletterStatusDelivered = delivered.node
465
+ return fullMsg
466
+ }
467
+
468
+ export const makeNewsletterStatusReactionSender = sock => async (jid, parentServerId, reaction, options = {}) => {
469
+ assertNewsletterJid(jid)
470
+ const userJid = sock.authState?.creds?.me?.id
471
+ if (!userJid) throw new TypeError('Not authenticated')
472
+
473
+ const messageId = options.messageId || generateMessageIDV2(userJid)
474
+ const node = buildNewsletterStatusReactionNode({ jid, messageId, parentServerId, reaction })
475
+ const ack = await sendStatusNode(sock, node, {
476
+ jid,
477
+ messageId,
478
+ timeoutMs: options.ackTimeoutMs ?? NEWSLETTER_STATUS_ACK_TIMEOUT_MS
479
+ })
480
+
481
+ return {
482
+ key: {
483
+ remoteJid: jid,
484
+ fromMe: true,
485
+ id: messageId
486
+ },
487
+ status: WAMessageStatus.SERVER_ACK,
488
+ newsletterStatusServerId: ack.serverId,
489
+ newsletterStatusAck: ack,
490
+ newsletterStatusResponse: ack.node
491
+ }
492
+ }
493
+
494
+ export const makeNewsletterStatusRevokeSender = sock => async (jid, statusId, options = {}) => {
495
+ assertNewsletterJid(jid)
496
+ const userJid = sock.authState?.creds?.me?.id
497
+ if (!userJid) throw new TypeError('Not authenticated')
498
+
499
+ const node = buildNewsletterStatusRevokeNode({ jid, statusId })
500
+ const ack = await sendStatusNode(sock, node, {
501
+ jid,
502
+ messageId: statusId,
503
+ timeoutMs: options.ackTimeoutMs ?? NEWSLETTER_STATUS_ACK_TIMEOUT_MS
504
+ })
505
+
506
+ return {
507
+ key: {
508
+ remoteJid: jid,
509
+ fromMe: true,
510
+ id: statusId
511
+ },
512
+ status: WAMessageStatus.SERVER_ACK,
513
+ newsletterStatusAck: ack,
514
+ newsletterStatusResponse: ack.node
515
+ }
516
+ }
517
+
518
+ const decodeStatusPayload = plaintext => {
519
+ const bytes = plaintext?.content
520
+ if (!bytes || Array.isArray(bytes)) return undefined
521
+ return proto.Message.decode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes))
522
+ }
523
+
524
+ const parseStatusAdminProfile = meta => {
525
+ const adminProfile = meta && getBinaryNodeChild(meta, 'admin_profile')
526
+ if (!adminProfile) return undefined
527
+
528
+ const name = getBinaryNodeChild(adminProfile, 'name')
529
+ const picture = getBinaryNodeChild(adminProfile, 'picture')
530
+ const content = name?.content
531
+ return {
532
+ id: adminProfile.attrs?.id,
533
+ name: typeof content === 'string'
534
+ ? content
535
+ : content instanceof Uint8Array
536
+ ? Buffer.from(content).toString('utf-8')
537
+ : undefined,
538
+ pictureId: picture?.attrs?.id,
539
+ pictureDirectPath: picture?.attrs?.direct_path
540
+ }
541
+ }
542
+
543
+ const parseStatusNode = status => {
544
+ const plaintext = getBinaryNodeChild(status, 'plaintext')
545
+ const reaction = getBinaryNodeChild(status, 'reaction')
546
+ const meta = getBinaryNodeChild(status, 'meta')
547
+ const reactions = getBinaryNodeChild(status, 'reactions')
548
+ const viewsCount = getBinaryNodeChild(status, 'views_count')
549
+ const responsesCount = getBinaryNodeChild(status, 'responses_count')
550
+ return {
551
+ id: status.attrs?.id,
552
+ serverId: readIntAttr(status.attrs, 'server_id'),
553
+ t: readIntAttr(status.attrs, 't'),
554
+ isSender: status.attrs?.is_sender === 'true',
555
+ type: status.attrs?.type,
556
+ edit: status.attrs?.edit,
557
+ mediaType: plaintext?.attrs?.mediatype,
558
+ interactionType: meta?.attrs?.interaction_type,
559
+ reaction: reaction ? reaction.attrs?.code ?? '' : undefined,
560
+ adminProfile: parseStatusAdminProfile(meta),
561
+ paidPartnership: meta ? !!getBinaryNodeChild(meta, 'paid_partnership') : false,
562
+ aiContent: meta ? !!getBinaryNodeChild(meta, 'ai_content') : false,
563
+ editTimestamp: readIntAttr(meta?.attrs, 'msg_edit_t'),
564
+ originalTimestamp: readIntAttr(meta?.attrs, 'original_msg_t'),
565
+ reactionCounts: reactions
566
+ ? getBinaryNodeChildren(reactions, 'reaction').map(entry => ({
567
+ code: entry.attrs?.code,
568
+ count: readIntAttr(entry.attrs, 'count')
569
+ }))
570
+ : undefined,
571
+ viewsCount: readIntAttr(viewsCount?.attrs, 'count'),
572
+ responsesCount: readIntAttr(responsesCount?.attrs, 'count'),
573
+ message: decodeStatusPayload(plaintext),
574
+ node: status
575
+ }
576
+ }
577
+
578
+ export const parseNewsletterStatusUpdatesResponse = node => {
579
+ const updates = getBinaryNodeChild(node, 'status_updates')
580
+ if (!updates) {
581
+ const children = Array.isArray(node?.content) ? node.content.map(child => child?.tag) : []
582
+ const error = new Error(`Newsletter status updates response has no <status_updates> child, got [${children.join(', ')}]`)
583
+ error.data = node
584
+ throw error
585
+ }
586
+
587
+ return parseNewsletterStatusesResponse(updates)
588
+ }
589
+
590
+ export const makeNewsletterStatusUpdatesFetcher = sock => async (jid, options = {}) => {
591
+ assertNewsletterJid(jid)
592
+
593
+ const { count = 20, since, before, after } = options
594
+ const attrs = { count: String(count) }
595
+ if (isPresent(since)) attrs.since = String(since)
596
+ if (isPresent(before)) attrs.before = toServerId(before, 'before')
597
+ else if (isPresent(after)) attrs.after = toServerId(after, 'after')
598
+
599
+ const result = await sock.query({
600
+ tag: 'iq',
601
+ attrs: {
602
+ to: jid,
603
+ xmlns: 'newsletter',
604
+ type: 'get'
605
+ },
606
+ content: [{ tag: 'status_updates', attrs, content: undefined }]
607
+ })
608
+
609
+ return parseNewsletterStatusUpdatesResponse(result)
610
+ }
611
+
612
+ export const parseNewsletterStatusesResponse = node => {
613
+ const statuses = getBinaryNodeChild(node, 'statuses')
614
+ if (!statuses) {
615
+ const children = Array.isArray(node?.content) ? node.content.map(child => child?.tag) : []
616
+ const error = new Error(`Newsletter statuses response has no <statuses> child, got [${children.join(', ')}]`)
617
+ error.data = node
618
+ throw error
619
+ }
620
+
621
+ return {
622
+ jid: statuses.attrs?.jid,
623
+ t: readIntAttr(statuses.attrs, 't'),
624
+ statuses: getBinaryNodeChildren(statuses, 'status').map(parseStatusNode)
625
+ }
626
+ }
627
+
628
+ export const makeNewsletterStatusFetcher = sock => async (jid, options = {}) => {
629
+ assertNewsletterJid(jid)
630
+
631
+ const { count = 20, before, after, viewRole } = options
632
+ const attrs = { type: 'jid', jid }
633
+ if (isPresent(viewRole)) attrs.view_role = String(viewRole).toLowerCase()
634
+ attrs.count = String(count)
635
+ if (isPresent(before)) attrs.before = toServerId(before, 'before')
636
+ else if (isPresent(after)) attrs.after = toServerId(after, 'after')
637
+
638
+ const result = await sock.query({
639
+ tag: 'iq',
640
+ attrs: {
641
+ to: S_WHATSAPP_NET,
642
+ xmlns: 'newsletter',
643
+ type: 'get'
644
+ },
645
+ content: [{ tag: 'statuses', attrs, content: undefined }]
646
+ })
647
+
648
+ return parseNewsletterStatusesResponse(result)
649
+ }
650
+
651
+ export { NEWSLETTER_STATUS_CONTENT_TYPES, NEWSLETTER_STATUS_MEDIA_TYPES, NEWSLETTER_STATUS_WEB_MEDIA_TYPES }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Status helpers — personal stories + group status
3
+ * Sources:
4
+ * - baileys.wiki Broadcasts (status@broadcast + statusJidList)
5
+ * - WhiskeySockets/Baileys Discussion #2318 (groupStatusMessageV2)
6
+ * - c4bal/wolfsocket groupStatus patterns
7
+ */
8
+ import { generateWAMessage, generateWAMessageFromContent, prepareWAMessageMedia } from "./messages.js";
9
+ import { generateMessageIDV2, unixTimestampSeconds } from "./generics.js";
10
+ import { jidNormalizedUser, isJidGroup } from "../WABinary/jid-utils.js";
11
+
12
+ /**
13
+ * Personal WhatsApp Status (story) → status@broadcast
14
+ * @param {object} sock
15
+ * @param {object} content - same shape as sendMessage content (text/image/video/audio)
16
+ * @param {string[]} [statusJidList] - audience JIDs
17
+ * @param {object} [options]
18
+ */
19
+ export async function sendPersonalStatus(sock, content, statusJidList = [], options = {}) {
20
+ const list = Array.isArray(statusJidList) ? statusJidList.filter(Boolean) : [];
21
+ return sock.sendMessage(
22
+ "status@broadcast",
23
+ content,
24
+ {
25
+ ...options,
26
+ broadcast: true,
27
+ statusJidList: list.length ? list : options.statusJidList,
28
+ backgroundColor: options.backgroundColor,
29
+ font: options.font,
30
+ }
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Group Status (story visible inside one group) via groupStatusMessageV2
36
+ * @param {object} sock
37
+ * @param {string} groupJid - must end with @g.us
38
+ * @param {object} content - { text } | { image, caption } | { video, caption } | { audio }
39
+ * @param {object} [options]
40
+ */
41
+ export async function sendGroupStatus(sock, groupJid, content, options = {}) {
42
+ if (!isJidGroup(groupJid)) {
43
+ throw new TypeError("sendGroupStatus requires a group JID (@g.us)");
44
+ }
45
+
46
+ const senderJid = jidNormalizedUser(sock.user?.id);
47
+ const colorArgb = (options.backgroundColorArgb ?? options.colorArgb ?? 0xff23313a) >>> 0;
48
+ const font = options.font ?? 1;
49
+
50
+ // Prefer high-level flag if upstream sendMessage already supports groupStatus
51
+ if (typeof sock.sendMessage === "function" && content && (content.image || content.video || content.audio || content.text)) {
52
+ try {
53
+ // Path A: c4bal-style flag
54
+ if (content.image || content.video || content.audio) {
55
+ return await sock.sendMessage(
56
+ groupJid,
57
+ { ...content, groupStatus: true },
58
+ options
59
+ );
60
+ }
61
+ } catch (_) {
62
+ /* fall through to V2 payload */
63
+ }
64
+ }
65
+
66
+ const contextInfo = {
67
+ forwardingScore: 0,
68
+ featureEligibilities: {
69
+ canBeReshared: true,
70
+ canReceiveMultiReact: true,
71
+ },
72
+ pairedMediaType: 0,
73
+ statusSourceType: 4,
74
+ statusAttributions: [{ type: 10 }],
75
+ isGroupStatus: true,
76
+ };
77
+
78
+ // Media path: prepare media then send with isGroupStatus context
79
+ if (content.image || content.video || content.audio || content.sticker) {
80
+ const mediaContent = { ...content };
81
+ delete mediaContent.groupStatus;
82
+ const prepared = await generateWAMessage(groupJid, mediaContent, {
83
+ userJid: senderJid,
84
+ upload: sock.waUploadToServer,
85
+ logger: sock.logger,
86
+ ...options,
87
+ });
88
+ const key = Object.keys(prepared.message || {}).find((k) => k.endsWith("Message"));
89
+ if (key && prepared.message[key]) {
90
+ prepared.message[key].contextInfo = {
91
+ ...(prepared.message[key].contextInfo || {}),
92
+ ...contextInfo,
93
+ };
94
+ }
95
+ await sock.relayMessage(groupJid, prepared.message, {
96
+ messageId: prepared.key.id,
97
+ });
98
+ return prepared;
99
+ }
100
+
101
+ // Text path: groupStatusMessageV2 wrapper (#2318)
102
+ const text = content.text || content.caption || " ";
103
+ const innerContent = {
104
+ extendedTextMessage: {
105
+ text,
106
+ font,
107
+ backgroundArgb: colorArgb,
108
+ contextInfo: {
109
+ forwardingScore: 0,
110
+ featureEligibilities: {
111
+ canBeReshared: true,
112
+ canReceiveMultiReact: true,
113
+ },
114
+ pairedMediaType: 0,
115
+ isGroupStatus: true,
116
+ statusAttributions: [
117
+ {
118
+ type: 6,
119
+ groupStatus: { authorJid: senderJid },
120
+ },
121
+ ],
122
+ },
123
+ },
124
+ };
125
+
126
+ const messageContent = {
127
+ groupStatusMessageV2: {
128
+ message: innerContent,
129
+ },
130
+ };
131
+
132
+ const generated = generateWAMessageFromContent(groupJid, messageContent, {
133
+ userJid: senderJid,
134
+ messageId: options.messageId || generateMessageIDV2(senderJid),
135
+ });
136
+
137
+ await sock.relayMessage(groupJid, generated.message, {
138
+ messageId: generated.key.id,
139
+ });
140
+ return generated;
141
+ }
142
+
143
+ export default { sendPersonalStatus, sendGroupStatus };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@prototypevip/baileys",
3
- "version": "0.1.3",
4
- "description": "Baileys fork (@prototypevip/baileys) — clean, without anti-ban, group encryption disabled (like 0.0.8)",
3
+ "version": "0.1.4",
4
+ "description": "Baileys fork (@prototypevip/baileys) — clean + Channel/Group/Personal Status APIs (0.1.4)",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
7
7
  "scripts": {