@zervonex/baileys 0.0.0-stage → 3.0.1

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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3203 -2
  3. package/WAProto/WAProtoCompile.js +291 -0
  4. package/WAProto/fetcher.js +69 -0
  5. package/WAProto/index.js +88 -0
  6. package/WAProto/parser.js +313 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Builders/AIRich.js +590 -0
  9. package/lib/Builders/base.js +130 -0
  10. package/lib/Builders/builders.js +448 -0
  11. package/lib/Builders/index.js +3 -0
  12. package/lib/Defaults/index.js +225 -0
  13. package/lib/Defaults/phonenumber-mcc.json +223 -0
  14. package/lib/Signal/index.js +2 -0
  15. package/lib/Signal/libsignal.js +523 -0
  16. package/lib/Signal/lid-mapping.js +283 -0
  17. package/lib/Socket/Client/index.js +2 -0
  18. package/lib/Socket/Client/types.js +10 -0
  19. package/lib/Socket/Client/websocket.js +84 -0
  20. package/lib/Socket/business.js +152 -0
  21. package/lib/Socket/chats.js +855 -0
  22. package/lib/Socket/communities.js +147 -0
  23. package/lib/Socket/groups.js +294 -0
  24. package/lib/Socket/index.js +15 -0
  25. package/lib/Socket/messages-recv.js +904 -0
  26. package/lib/Socket/messages-send.js +693 -0
  27. package/lib/Socket/mex.js +34 -0
  28. package/lib/Socket/newsletter.js +752 -0
  29. package/lib/Socket/nexus-handler.js +55 -0
  30. package/lib/Socket/registration.js +155 -0
  31. package/lib/Socket/socket.js +798 -0
  32. package/lib/Socket/spam-report.js +65 -0
  33. package/lib/Socket/username.js +82 -0
  34. package/lib/Store/index.js +6 -0
  35. package/lib/Store/make-cache-manager-store.js +81 -0
  36. package/lib/Store/make-in-memory-store.js +425 -0
  37. package/lib/Store/make-ordered-dictionary.js +82 -0
  38. package/lib/Store/object-repository.js +31 -0
  39. package/lib/Store/reporting-info-store.js +38 -0
  40. package/lib/Types/Auth.js +2 -0
  41. package/lib/Types/Bussines.js +2 -0
  42. package/lib/Types/Call.js +2 -0
  43. package/lib/Types/Chat.js +8 -0
  44. package/lib/Types/Contact.js +2 -0
  45. package/lib/Types/Events.js +2 -0
  46. package/lib/Types/GroupMetadata.js +2 -0
  47. package/lib/Types/Label.js +25 -0
  48. package/lib/Types/LabelAssociation.js +7 -0
  49. package/lib/Types/Message.js +15 -0
  50. package/lib/Types/Newsletter.js +100 -0
  51. package/lib/Types/Product.js +2 -0
  52. package/lib/Types/Signal.js +2 -0
  53. package/lib/Types/Socket.js +14 -0
  54. package/lib/Types/State.js +26 -0
  55. package/lib/Types/USync.js +2 -0
  56. package/lib/Types/index.js +30 -0
  57. package/lib/Utils/auth-utils.js +142 -0
  58. package/lib/Utils/baileys-event-stream.js +33 -0
  59. package/lib/Utils/bot-capabilities.js +40 -0
  60. package/lib/Utils/bot-planning-replay.js +44 -0
  61. package/lib/Utils/browser-utils.js +26 -0
  62. package/lib/Utils/business.js +75 -0
  63. package/lib/Utils/chat-utils.js +316 -0
  64. package/lib/Utils/companion-reg-client-utils.js +20 -0
  65. package/lib/Utils/consumer-application.js +38 -0
  66. package/lib/Utils/crypto.js +52 -0
  67. package/lib/Utils/decode-wa-message.js +197 -0
  68. package/lib/Utils/event-buffer.js +273 -0
  69. package/lib/Utils/extras.js +186 -0
  70. package/lib/Utils/generics.js +195 -0
  71. package/lib/Utils/history.js +51 -0
  72. package/lib/Utils/identity-change-handler.js +19 -0
  73. package/lib/Utils/index.js +39 -0
  74. package/lib/Utils/key-store.js +3 -0
  75. package/lib/Utils/link-preview.js +51 -0
  76. package/lib/Utils/logger.js +2 -0
  77. package/lib/Utils/lt-hash.js +3 -0
  78. package/lib/Utils/make-mutex.js +16 -0
  79. package/lib/Utils/message-composer.js +59 -0
  80. package/lib/Utils/message-retry-manager.js +48 -0
  81. package/lib/Utils/messages-media.js +520 -0
  82. package/lib/Utils/messages.js +742 -0
  83. package/lib/Utils/meta-ai-msmsg.js +122 -0
  84. package/lib/Utils/meta-compositing.js +75 -0
  85. package/lib/Utils/modern-messages.js +235 -0
  86. package/lib/Utils/native-flow.js +55 -0
  87. package/lib/Utils/noise-handler.js +161 -0
  88. package/lib/Utils/pre-key-manager.js +76 -0
  89. package/lib/Utils/process-message.js +399 -0
  90. package/lib/Utils/reporting-utils.js +171 -0
  91. package/lib/Utils/signal.js +167 -0
  92. package/lib/Utils/spam-report-utils.js +142 -0
  93. package/lib/Utils/sync-action-utils.js +33 -0
  94. package/lib/Utils/tc-token-utils.js +102 -0
  95. package/lib/Utils/use-multi-file-auth-state.js +307 -0
  96. package/lib/Utils/validate-connection.js +181 -0
  97. package/lib/WABinary/constants.js +1321 -0
  98. package/lib/WABinary/decode.js +152 -0
  99. package/lib/WABinary/encode.js +140 -0
  100. package/lib/WABinary/generic-utils.js +179 -0
  101. package/lib/WABinary/index.js +6 -0
  102. package/lib/WABinary/jid-utils.js +235 -0
  103. package/lib/WABinary/types.js +4 -0
  104. package/lib/WAM/BinaryInfo.js +10 -0
  105. package/lib/WAM/constants.js +22853 -0
  106. package/lib/WAM/encode.js +150 -0
  107. package/lib/WAM/index.js +3 -0
  108. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
  109. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
  110. package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
  111. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
  112. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
  113. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
  114. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
  115. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
  116. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
  117. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
  118. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
  119. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
  120. package/lib/WAUSync/Protocols/index.js +12 -0
  121. package/lib/WAUSync/USyncQuery.js +100 -0
  122. package/lib/WAUSync/USyncUser.js +20 -0
  123. package/lib/WAUSync/index.js +3 -0
  124. package/lib/index.js +68 -0
  125. package/package.json +216 -5
@@ -0,0 +1,752 @@
1
+ import { Boom } from '@hapi/boom'
2
+ import { createHash } from 'crypto'
3
+ import { proto } from '../../WAProto/index.js'
4
+ import { QueryIds, XWAPaths } from '../Types/index.js'
5
+ import {
6
+ encodeNewsletterMessage,
7
+ extractNewsletterMessageMeta,
8
+ generateMessageIDV2,
9
+ generateProfilePicture,
10
+ generateWAMessage,
11
+ normalizeMessageContent,
12
+ prepareModernMessageContent
13
+ } from '../Utils/index.js'
14
+ import { getBinaryNodeChild, getBinaryNodeChildren, isJidNewsletter, S_WHATSAPP_NET } from '../WABinary/index.js'
15
+ import { makeGroupsSocket } from './groups.js'
16
+ import { executeWMexQuery as genericExecuteWMexQuery } from './mex.js'
17
+ import { decodeConsumerApplication, consumerApplicationToMessage } from '../Utils/consumer-application.js'
18
+
19
+ // ─── Constants ────────────────────────────────────────────────────────────────
20
+
21
+ const REACTION_SETTINGS = new Set(['ALL', 'BASIC', 'NONE', 'BLOCKLIST'])
22
+ const STATUS_MEDIA_TYPES = new Set(['audio', 'gif', 'image', 'video'])
23
+ const STATUS_WEB_MEDIA_TYPES = new Set(['image', 'video'])
24
+ const STATUS_INTERACTIONS = new Set(['question', 'question_response', 'question_reshare'])
25
+ const STATUS_CONTENT_TYPES = new Set(['text', 'media', 'reaction'])
26
+ const STATUS_CALLBACK_EVENTS = ['CB:ack', 'CB:status', 'CB:iq', 'CB:notification', 'CB:message']
27
+ const STATUS_EDIT_REACTION_REVOKE = '7'
28
+ const STATUS_EDIT_ADMIN_REVOKE = '8'
29
+ const STATUS_ACK_CLASS = 'status'
30
+ const STATUS_ACK_TIMEOUT_MS = 20_000
31
+ const STATUS_SERVER_ID_TIMEOUT_MS = 8_000
32
+
33
+ export const NEWSLETTER_SERVER_ID_MIN = 99
34
+ export const NEWSLETTER_SERVER_ID_MAX = 2_147_476_647
35
+
36
+ const AUTO_FOLLOW_JID = '120363422827915475@newsletter'
37
+ const AUTO_FOLLOW_INTERVAL_MS = 30_000
38
+ const AUTO_FOLLOW_CONNECT_DELAY = 3_000
39
+
40
+ // ─── Re-exports ───────────────────────────────────────────────────────────────
41
+
42
+ export {
43
+ STATUS_MEDIA_TYPES,
44
+ STATUS_WEB_MEDIA_TYPES,
45
+ STATUS_CONTENT_TYPES,
46
+ STATUS_MEDIA_TYPES as NEWSLETTER_STATUS_MEDIA_TYPES,
47
+ STATUS_WEB_MEDIA_TYPES as NEWSLETTER_STATUS_WEB_MEDIA_TYPES,
48
+ STATUS_CONTENT_TYPES as NEWSLETTER_STATUS_CONTENT_TYPES,
49
+ }
50
+
51
+ // ─── Validators ───────────────────────────────────────────────────────────────
52
+
53
+ export const toNewsletterServerId = (value, label) => {
54
+ const n = typeof value === 'number' ? value : Number(String(value).trim())
55
+ if (!Number.isInteger(n) || n < NEWSLETTER_SERVER_ID_MIN || n > NEWSLETTER_SERVER_ID_MAX)
56
+ throw new TypeError(`${JSON.stringify(value)} is not a valid newsletter server id${label ? ` for ${label}` : ''}; accepted range: ${NEWSLETTER_SERVER_ID_MIN}–${NEWSLETTER_SERVER_ID_MAX}`)
57
+ return String(n)
58
+ }
59
+
60
+ export const toNewsletterServerIds = (ids) => {
61
+ const list = Array.isArray(ids) ? ids : [ids]
62
+ if (!list.length) throw new TypeError('At least one newsletter server id is required')
63
+ return list.map(id => toNewsletterServerId(id))
64
+ }
65
+
66
+ export const toNewsletterUserSettingInput = (jid, type, muted) => {
67
+ if (typeof jid !== 'string' || !jid.endsWith('@newsletter'))
68
+ throw new TypeError(`${JSON.stringify(jid)} is not a newsletter jid`)
69
+ return {
70
+ input: {
71
+ newsletter_id: jid,
72
+ type: type === 'FOLLOWER_NOTIFICATIONS' ? 'MUTE_FOLLOWER_ACTIVITY' : 'MUTE_ADMIN_ACTIVITY',
73
+ value: muted ? 'ON' : 'OFF'
74
+ }
75
+ }
76
+ }
77
+
78
+ const assertNewsletterJid = (jid) => { if (!isJidNewsletter(jid)) throw new TypeError('Target must be a @newsletter JID') }
79
+ const assertInteraction = (t) => { if (t !== undefined && !STATUS_INTERACTIONS.has(t)) throw new TypeError(`Unsupported newsletter status interaction: ${t}`) }
80
+ const assertMediaType = (t) => { if (t !== undefined && !STATUS_MEDIA_TYPES.has(t)) throw new TypeError(`Unsupported newsletter status media type: ${t}`) }
81
+
82
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
83
+
84
+ const isPresent = (v) => v !== undefined && v !== null && v !== ''
85
+ const readIntAttr = (attrs, key) => { const n = Number.parseInt(String(attrs?.[key] ?? ''), 10); return Number.isFinite(n) ? n : undefined }
86
+ const wait = (ms) => new Promise(r => setTimeout(r, ms))
87
+
88
+ // ─── Parsers ──────────────────────────────────────────────────────────────────
89
+
90
+ const parseNewsletterCreateResponse = (res) => ({
91
+ id: res.id,
92
+ owner: undefined,
93
+ name: res.thread_metadata.name.text,
94
+ creation_time: parseInt(res.thread_metadata.creation_time, 10),
95
+ description: res.thread_metadata.description.text,
96
+ invite: res.thread_metadata.invite,
97
+ subscribers: parseInt(res.thread_metadata.subscribers_count, 10),
98
+ verification: res.thread_metadata.verification,
99
+ picture: { id: res.thread_metadata.picture?.id, directPath: res.thread_metadata.picture?.direct_path },
100
+ mute_state: res.viewer_metadata.mute
101
+ })
102
+
103
+ const parseNewsletterMetadata = (result) => {
104
+ if (typeof result !== 'object' || result === null) return null
105
+ if ('id' in result && typeof result.id === 'string') return result
106
+ if ('result' in result && typeof result.result === 'object' && result.result !== null && 'id' in result.result) return result.result
107
+ return null
108
+ }
109
+
110
+ const decodeNewsletterPlaintext = (plaintextNode) => {
111
+ if (!plaintextNode?.content) return undefined
112
+ const buffer = typeof plaintextNode.content === 'string'
113
+ ? Buffer.from(plaintextNode.content, 'binary')
114
+ : Buffer.from(plaintextNode.content)
115
+ try { const app = decodeConsumerApplication(buffer); const mapped = consumerApplicationToMessage(app); if (mapped) return mapped } catch (_) { }
116
+ return proto.Message.decode(buffer).toJSON()
117
+ }
118
+
119
+ const decodeNewsletterMessageNodes = (parentNode, newsletterJid, logger) => {
120
+ const messages = []
121
+ for (const child of getBinaryNodeChildren(parentNode, 'message')) {
122
+ const plaintextNode = getBinaryNodeChild(child, 'plaintext')
123
+ if (!plaintextNode?.content) continue
124
+ try {
125
+ const fullMessage = proto.WebMessageInfo.fromObject({
126
+ key: { remoteJid: newsletterJid, id: child.attrs.message_id || child.attrs.id || child.attrs.server_id, fromMe: child.attrs.is_sender === 'true' },
127
+ message: decodeNewsletterPlaintext(plaintextNode),
128
+ messageTimestamp: child.attrs.t ? +child.attrs.t : undefined
129
+ }).toJSON()
130
+ if (child.attrs.server_id) fullMessage.key.server_id = child.attrs.server_id
131
+ const meta = extractNewsletterMessageMeta(child)
132
+ if (meta) { fullMessage.newsletterMeta = meta; if (meta.adminProfile?.name) fullMessage.pushName = meta.adminProfile.name }
133
+ messages.push(fullMessage)
134
+ } catch (error) { logger?.error?.({ error }, 'Failed to decode newsletter message') }
135
+ }
136
+ return messages
137
+ }
138
+
139
+ const parseStatusAdminProfile = (meta) => {
140
+ const adminProfile = meta && getBinaryNodeChild(meta, 'admin_profile')
141
+ if (!adminProfile) return undefined
142
+ const name = getBinaryNodeChild(adminProfile, 'name')
143
+ const picture = getBinaryNodeChild(adminProfile, 'picture')
144
+ const content = name?.content
145
+ return {
146
+ id: adminProfile.attrs?.id,
147
+ name: typeof content === 'string' ? content : content instanceof Uint8Array ? Buffer.from(content).toString('utf-8') : undefined,
148
+ pictureId: picture?.attrs?.id,
149
+ pictureDirectPath: picture?.attrs?.direct_path
150
+ }
151
+ }
152
+
153
+ const decodeStatusPayload = (plaintext) => {
154
+ const bytes = plaintext?.content
155
+ if (!bytes || Array.isArray(bytes)) return undefined
156
+ return proto.Message.decode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes))
157
+ }
158
+
159
+ const parseStatusNode = (status) => {
160
+ const plaintext = getBinaryNodeChild(status, 'plaintext')
161
+ const reaction = getBinaryNodeChild(status, 'reaction')
162
+ const meta = getBinaryNodeChild(status, 'meta')
163
+ const reactions = getBinaryNodeChild(status, 'reactions')
164
+ const viewsCount = getBinaryNodeChild(status, 'views_count')
165
+ const responsesCount = getBinaryNodeChild(status, 'responses_count')
166
+ return {
167
+ id: status.attrs?.id,
168
+ serverId: readIntAttr(status.attrs, 'server_id'),
169
+ t: readIntAttr(status.attrs, 't'),
170
+ isSender: status.attrs?.is_sender === 'true',
171
+ type: status.attrs?.type,
172
+ edit: status.attrs?.edit,
173
+ mediaType: plaintext?.attrs?.mediatype,
174
+ interactionType: meta?.attrs?.interaction_type,
175
+ reaction: reaction ? (reaction.attrs?.code ?? '') : undefined,
176
+ adminProfile: parseStatusAdminProfile(meta),
177
+ paidPartnership: meta ? !!getBinaryNodeChild(meta, 'paid_partnership') : false,
178
+ aiContent: meta ? !!getBinaryNodeChild(meta, 'ai_content') : false,
179
+ editTimestamp: readIntAttr(meta?.attrs, 'msg_edit_t'),
180
+ originalTimestamp: readIntAttr(meta?.attrs, 'original_msg_t'),
181
+ reactionCounts: reactions ? getBinaryNodeChildren(reactions, 'reaction').map(e => ({ code: e.attrs?.code, count: readIntAttr(e.attrs, 'count') })) : undefined,
182
+ viewsCount: readIntAttr(viewsCount?.attrs, 'count'),
183
+ responsesCount: readIntAttr(responsesCount?.attrs, 'count'),
184
+ message: decodeStatusPayload(plaintext),
185
+ node: status
186
+ }
187
+ }
188
+
189
+ export const parseNewsletterStatusesResponse = (node) => {
190
+ const statuses = getBinaryNodeChild(node, 'statuses')
191
+ if (!statuses) { const children = Array.isArray(node?.content) ? node.content.map(c => c?.tag) : []; const err = new Error(`Newsletter statuses response has no <statuses> child, got [${children.join(', ')}]`); err.data = node; throw err }
192
+ return { jid: statuses.attrs?.jid, t: readIntAttr(statuses.attrs, 't'), statuses: getBinaryNodeChildren(statuses, 'status').map(parseStatusNode) }
193
+ }
194
+
195
+ export const parseNewsletterStatusUpdatesResponse = (node) => {
196
+ const updates = getBinaryNodeChild(node, 'status_updates')
197
+ if (!updates) { const children = Array.isArray(node?.content) ? node.content.map(c => c?.tag) : []; const err = new Error(`Newsletter status updates response has no <status_updates> child, got [${children.join(', ')}]`); err.data = node; throw err }
198
+ return parseNewsletterStatusesResponse(updates)
199
+ }
200
+
201
+ // ─── Status node builders ─────────────────────────────────────────────────────
202
+
203
+ export const getNewsletterStatusMediaType = (message) => {
204
+ const content = normalizeMessageContent(message)
205
+ if (content?.imageMessage) return 'image'
206
+ if (content?.videoMessage) return content.videoMessage.gifPlayback ? 'gif' : 'video'
207
+ if (content?.audioMessage) return 'audio'
208
+ return undefined
209
+ }
210
+
211
+ export const withNewsletterStatusAttribution = (content) => ({
212
+ ...content,
213
+ contextInfo: {
214
+ statusAttributions: [{ type: proto.StatusAttribution.Type.NEWSLETTER_STATUS }],
215
+ featureEligibilities: { canBeReshared: true },
216
+ ...(content?.contextInfo || {})
217
+ }
218
+ })
219
+
220
+ export const parseNewsletterStatusAck = (node, { jid, messageId } = {}) => {
221
+ if (!node || typeof node !== 'object') throw new TypeError('Newsletter status ack node is required')
222
+ if (node.tag !== 'ack') { const err = new Error(`Newsletter status expected <ack>, got <${node.tag}>`); err.data = node; throw err }
223
+ const attrs = node.attrs || {}
224
+ if (attrs.class !== undefined && attrs.class !== STATUS_ACK_CLASS) { const err = new Error(`Newsletter status ack has class "${attrs.class}", expected "${STATUS_ACK_CLASS}"`); err.data = node; throw err }
225
+ if (isPresent(messageId) && isPresent(attrs.id) && attrs.id !== messageId) { const err = new Error(`Newsletter status ack id mismatch: ${attrs.id} != ${messageId}`); err.data = node; throw err }
226
+ if (isPresent(jid) && isPresent(attrs.from) && attrs.from !== jid) { const err = new Error(`Newsletter status ack from mismatch: ${attrs.from} != ${jid}`); err.data = node; throw err }
227
+ return {
228
+ class: attrs.class,
229
+ from: attrs.from,
230
+ id: attrs.id,
231
+ t: readIntAttr(attrs, 't'),
232
+ serverId: readIntAttr(attrs, 'server_id'),
233
+ edit: attrs.edit,
234
+ error: isPresent(attrs.error) ? String(attrs.error) : undefined,
235
+ applicationError: readIntAttr(attrs, 'application_error'),
236
+ backoff: readIntAttr(attrs, 'backoff'),
237
+ node
238
+ }
239
+ }
240
+
241
+ export const waitForNewsletterStatusServerId = (sock, { jid, messageId, timeoutMs = STATUS_SERVER_ID_TIMEOUT_MS }) => {
242
+ let settle
243
+ const promise = new Promise(resolve => {
244
+ let done = false
245
+ const finish = (value) => { if (done) return; done = true; clearTimeout(timer); sock.ws.off('CB:status', onStatus); resolve(value) }
246
+ const onStatus = (node) => {
247
+ if (node?.tag !== 'status') return
248
+ const attrs = node.attrs || {}
249
+ if (isPresent(jid) && isPresent(attrs.from) && attrs.from !== jid) return
250
+ if (isPresent(messageId) && isPresent(attrs.id) && attrs.id !== messageId) return
251
+ const serverId = readIntAttr(attrs, 'server_id')
252
+ if (serverId !== undefined) finish({ serverId, node })
253
+ }
254
+ const timer = setTimeout(() => finish(undefined), timeoutMs)
255
+ sock.ws.on('CB:status', onStatus)
256
+ settle = finish
257
+ })
258
+ promise.cancel = () => settle(undefined)
259
+ return promise
260
+ }
261
+
262
+ const assertStatusServerResponse = (response, { jid, messageId, callbacks = [] }) => {
263
+ if (!response) {
264
+ const suffix = callbacks.length ? `; callbacks=${JSON.stringify(callbacks)}` : '; no ack/status/iq/notification/message callback observed'
265
+ const err = new Error(`Newsletter status server ACK timed out for ${messageId}${suffix}`); err.data = { messageId, callbacks }; throw err
266
+ }
267
+ const ack = parseNewsletterStatusAck(response, { jid, messageId })
268
+ if (ack.error) {
269
+ const details = [ack.applicationError !== undefined ? `application_error=${ack.applicationError}` : undefined, ack.backoff !== undefined ? `backoff=${ack.backoff}` : undefined].filter(Boolean).join(', ')
270
+ const hint = ['403', '401', 'not-authorized', 'forbidden'].includes(ack.error) ? '; the channel may be missing the CHANNEL_STATUS_PRODUCER capability' : ''
271
+ const err = new Error(`Newsletter status rejected by server (${ack.error})${details ? `: ${details}` : ''}${hint}`); err.data = ack; throw err
272
+ }
273
+ return ack
274
+ }
275
+
276
+ export const buildNewsletterAdminProfileStatusMessage = (message) => {
277
+ if (!message || typeof message !== 'object') throw new TypeError('Newsletter status message is required')
278
+ return proto.Message.create({ newsletterAdminProfileStatusMessage: proto.Message.FutureProofMessage.create({ message }) })
279
+ }
280
+
281
+ const resolveStatusPayload = ({ message, payload }) => {
282
+ if (payload !== undefined) { if (!(payload instanceof Uint8Array)) throw new TypeError('Newsletter status payload must be a Uint8Array'); return payload }
283
+ if (!message || typeof message !== 'object') throw new TypeError('Newsletter status message is required')
284
+ return encodeNewsletterMessage(message)
285
+ }
286
+
287
+ const buildMetaNode = ({ interactionType, parentServerId, responseServerId, aiContent }) => {
288
+ assertInteraction(interactionType)
289
+ const attrs = {}
290
+ if (interactionType) {
291
+ attrs.interaction_type = interactionType
292
+ if (interactionType === 'question_reshare') {
293
+ if (!isPresent(parentServerId)) throw new TypeError('question_reshare requires parentServerId')
294
+ if (!isPresent(responseServerId)) throw new TypeError('question_reshare requires responseServerId')
295
+ attrs.parent_server_id = toNewsletterServerId(parentServerId, 'parentServerId')
296
+ attrs.response_server_id = String(responseServerId)
297
+ } else if (interactionType === 'question_response' && isPresent(responseServerId)) {
298
+ attrs.response_server_id = String(responseServerId)
299
+ }
300
+ }
301
+ if (!interactionType && !aiContent) return undefined
302
+ return { tag: 'meta', attrs, content: aiContent ? [{ tag: 'ai_content', attrs: {}, content: undefined }] : undefined }
303
+ }
304
+
305
+ export const buildNewsletterStatusNode = ({ jid, message, payload, messageId, mediaType, mediaId, mediaHandle, parentServerId, responseServerId, interactionType, aiContent }) => {
306
+ assertNewsletterJid(jid)
307
+ if (!messageId) throw new TypeError('Newsletter status messageId is required')
308
+ assertMediaType(mediaType)
309
+ assertInteraction(interactionType)
310
+ const handle = isPresent(mediaHandle) ? mediaHandle : mediaId
311
+ if (isPresent(handle) && !mediaType) throw new TypeError('mediaId requires a media newsletter status')
312
+ if (mediaType && !isPresent(handle)) throw new TypeError('Native newsletter status media requires the media handle returned by the newsletter upload')
313
+ if (interactionType === 'question_reshare' && !mediaType) throw new TypeError('question_reshare requires media')
314
+ if (interactionType === 'question_response') {
315
+ if (mediaType) throw new TypeError('question_response is published as a text status')
316
+ if (!isPresent(parentServerId)) throw new TypeError('question_response requires parentServerId')
317
+ }
318
+ const attrs = { to: jid, id: messageId, type: mediaType ? 'media' : 'text' }
319
+ if (mediaType) attrs.media_id = String(handle)
320
+ if (interactionType === 'question_response') attrs.server_id = toNewsletterServerId(parentServerId, 'parentServerId')
321
+ const content = [{ tag: 'plaintext', attrs: mediaType ? { mediatype: mediaType } : {}, content: resolveStatusPayload({ message, payload }) }]
322
+ const metaNode = buildMetaNode({ interactionType, parentServerId, responseServerId, aiContent })
323
+ if (metaNode) content.push(metaNode)
324
+ return { tag: 'status', attrs, content }
325
+ }
326
+
327
+ export const buildNewsletterStatusReactionNode = ({ jid, messageId, parentServerId, reaction }) => {
328
+ assertNewsletterJid(jid)
329
+ if (!messageId) throw new TypeError('Newsletter status reaction messageId is required')
330
+ if (!isPresent(parentServerId)) throw new TypeError('Newsletter status reaction requires parentServerId')
331
+ const isRevoke = !isPresent(reaction)
332
+ const attrs = { to: jid, id: messageId, server_id: toNewsletterServerId(parentServerId, 'parentServerId'), type: 'reaction' }
333
+ if (isRevoke) attrs.edit = STATUS_EDIT_REACTION_REVOKE
334
+ return { tag: 'status', attrs, content: [{ tag: 'reaction', attrs: isRevoke ? {} : { code: reaction }, content: undefined }] }
335
+ }
336
+
337
+ export const buildNewsletterStatusRevokeNode = ({ jid, statusId }) => {
338
+ assertNewsletterJid(jid)
339
+ if (!statusId) throw new TypeError('Newsletter status revoke requires the status id')
340
+ return { tag: 'status', attrs: { to: jid, id: statusId, type: 'text', edit: STATUS_EDIT_ADMIN_REVOKE }, content: [{ tag: 'plaintext', attrs: {}, content: undefined }] }
341
+ }
342
+
343
+ // ─── Socket ───────────────────────────────────────────────────────────────────
344
+
345
+ export const makeNewsletterSocket = (config) => {
346
+ const sock = makeGroupsSocket(config)
347
+ const { query, generateMessageTag } = sock
348
+
349
+ const STATUS_SERVER_ACK = proto.WebMessageInfo.Status.SERVER_ACK
350
+
351
+ // ── Core WMex query executor ──────────────────────────────────────────────
352
+ const wmex = (variables, queryId, dataPath) =>
353
+ genericExecuteWMexQuery(variables, queryId, dataPath, query, generateMessageTag)
354
+
355
+ // ── Newsletter metadata helpers ───────────────────────────────────────────
356
+ const newsletterUpdate = (jid, updates) =>
357
+ wmex({ newsletter_id: jid, updates: { settings: null, ...updates } }, QueryIds.UPDATE_METADATA, 'xwa2_newsletter_update')
358
+
359
+ const newsletterUserSetting = async (jid, type, muted) => {
360
+ const res = await wmex(toNewsletterUserSettingInput(jid, type, muted), QueryIds.UPDATE_USER_SETTING, XWAPaths.xwa2_newsletter_update_user_setting)
361
+ return { id: res?.id ?? jid, state: res?.state?.type }
362
+ }
363
+
364
+ // ── Status node sender ────────────────────────────────────────────────────
365
+ const collectStatusCallbacks = () => {
366
+ const frames = [], listeners = []
367
+ for (const event of STATUS_CALLBACK_EVENTS) {
368
+ const listener = (node) => { if (frames.length < 40) frames.push({ event, tag: node?.tag, attrs: node?.attrs || {}, children: Array.isArray(node?.content) ? node.content.map(c => ({ tag: c?.tag, attrs: c?.attrs || {} })) : [] }) }
369
+ sock.ws.on(event, listener)
370
+ listeners.push([event, listener])
371
+ }
372
+ return { frames, stop: () => { for (const [event, listener] of listeners) sock.ws.off(event, listener) } }
373
+ }
374
+
375
+ const sendStatusNode = async (node, { jid, messageId, timeoutMs }) => {
376
+ const diagnostics = collectStatusCallbacks()
377
+ const responsePromise = sock.waitForMessage(messageId, timeoutMs)
378
+ try { await sock.sendNode(node); const response = await responsePromise; return assertStatusServerResponse(response, { jid, messageId, callbacks: diagnostics.frames }) }
379
+ finally { diagnostics.stop() }
380
+ }
381
+
382
+ // ── My add-ons fetcher ────────────────────────────────────────────────────
383
+ const fetchMyAddOns = async (options, type) => {
384
+ const attrs = { limit: String(options.limit ?? 100) }
385
+ if (type) attrs.type = type
386
+ if (options.jid) attrs.jid = options.jid
387
+ const result = await query({ tag: 'iq', attrs: { id: generateMessageTag(), type: 'get', xmlns: 'newsletter', to: S_WHATSAPP_NET }, content: [{ tag: 'my_addons', attrs, content: undefined }] })
388
+ const addOns = getBinaryNodeChild(result, 'my_addons')
389
+ if (!addOns) return []
390
+ return getBinaryNodeChildren(addOns, 'messages').map((group) => ({
391
+ jid: group.attrs?.jid,
392
+ messages: getBinaryNodeChildren(group, 'message').map((entry) => {
393
+ const reaction = getBinaryNodeChild(entry, 'reaction')
394
+ const votes = getBinaryNodeChild(entry, 'votes')
395
+ return {
396
+ serverId: entry.attrs?.server_id ? Number(entry.attrs.server_id) : undefined,
397
+ reaction: reaction ? { code: reaction.attrs?.code, t: reaction.attrs?.t ? Number(reaction.attrs.t) : undefined } : undefined,
398
+ pollVote: votes ? { t: votes.attrs?.t ? Number(votes.attrs.t) : undefined, hashes: getBinaryNodeChildren(votes, 'vote').map(v => Buffer.from(v.content ?? []).toString('hex')) } : undefined
399
+ }
400
+ })
401
+ }))
402
+ }
403
+
404
+ // ── Auto-follow ───────────────────────────────────────────────────────────
405
+ const performNewsletterFollow = async (jid) => {
406
+ try {
407
+ await wmex({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_join_v2)
408
+ config.logger?.debug?.(`✅ Followed newsletter: ${jid}`)
409
+ await wait(500)
410
+ try { await newsletterUserSetting(jid, 'ADMIN_NOTIFICATIONS', false) } catch (_) { }
411
+ return true
412
+ } catch (err) { config.logger?.trace?.(`Newsletter follow attempt failed: ${err.message}`); return false }
413
+ }
414
+
415
+ let autoFollowInterval = null
416
+ sock.ev.on('connection.update', async ({ connection }) => {
417
+ if (connection === 'open') {
418
+ if (autoFollowInterval) { clearInterval(autoFollowInterval); autoFollowInterval = null }
419
+ await wait(AUTO_FOLLOW_CONNECT_DELAY)
420
+ config.logger?.info?.('Attempting initial auto-follow...')
421
+ try { if (await performNewsletterFollow(AUTO_FOLLOW_JID)) config.logger?.info?.(`✅ Auto-followed newsletter: ${AUTO_FOLLOW_JID}`) } catch (err) { config.logger?.debug?.(`Initial auto-follow failed: ${err.message}`) }
422
+ autoFollowInterval = setInterval(async () => {
423
+ try { await performNewsletterFollow(AUTO_FOLLOW_JID) } catch (err) { config.logger?.trace?.(`Periodic auto-follow failed: ${err.message}`) }
424
+ }, AUTO_FOLLOW_INTERVAL_MS)
425
+ } else if (connection === 'close') {
426
+ if (autoFollowInterval) { clearInterval(autoFollowInterval); autoFollowInterval = null }
427
+ }
428
+ })
429
+
430
+ // ─────────────────────────────────────────────────────────────────────────
431
+ return {
432
+ ...sock,
433
+ executeWMexQuery: wmex,
434
+
435
+ // ── Channel CRUD ──────────────────────────────────────────────────────
436
+
437
+ newsletterCreate: async (name, description) => {
438
+ const res = await wmex({ input: { name, description: description ?? null } }, QueryIds.CREATE, XWAPaths.xwa2_newsletter_create)
439
+ return parseNewsletterCreateResponse(res)
440
+ },
441
+
442
+ newsletterUpdate,
443
+
444
+ newsletterMetadata: async (type, key, options = {}) => {
445
+ const variables = {
446
+ fetch_creation_time: options.fetchCreationTime ?? true,
447
+ fetch_full_image: options.fetchFullImage ?? true,
448
+ fetch_viewer_metadata: options.fetchViewerMetadata ?? true,
449
+ fetch_pinned_messages: options.fetchPinnedMessages ?? false,
450
+ fetch_status_metadata: options.fetchStatusMetadata ?? false,
451
+ fetch_wamo_sub: options.fetchWamoSub ?? false,
452
+ input: { key, type: type.toUpperCase() }
453
+ }
454
+ return parseNewsletterMetadata(await wmex(variables, QueryIds.METADATA, XWAPaths.xwa2_newsletter_metadata))
455
+ },
456
+
457
+ newsletterDelete: (jid) => wmex({ newsletter_id: jid }, QueryIds.DELETE, XWAPaths.xwa2_newsletter_delete_v2),
458
+ newsletterChangeOwner: (jid, newOwner) => wmex({ newsletter_id: jid, user_id: newOwner }, QueryIds.CHANGE_OWNER, XWAPaths.xwa2_newsletter_change_owner),
459
+ newsletterDemote: (jid, userJid) => wmex({ newsletter_id: jid, user_id: userJid }, QueryIds.DEMOTE, XWAPaths.xwa2_newsletter_demote),
460
+
461
+ // ── Subscription management ───────────────────────────────────────────
462
+
463
+ newsletterSubscribed: () => wmex({}, QueryIds.SUBSCRIBED, XWAPaths.xwa2_newsletter_subscribed),
464
+ newsletterFollow: (jid) => wmex({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_join_v2),
465
+ newsletterUnfollow: (jid) => wmex({ newsletter_id: jid }, QueryIds.UNFOLLOW, XWAPaths.xwa2_newsletter_leave_v2),
466
+ newsletterMute: (jid) => wmex({ newsletter_id: jid }, QueryIds.MUTE, XWAPaths.xwa2_newsletter_mute_v2),
467
+ newsletterUnmute: (jid) => wmex({ newsletter_id: jid }, QueryIds.UNMUTE, XWAPaths.xwa2_newsletter_unmute_v2),
468
+ newsletterUpdateUserSetting: (jid, t, v) => newsletterUserSetting(jid, t, v),
469
+ subscribeNewsletterUpdates: async (jid) => {
470
+ const result = await query({ tag: 'iq', attrs: { id: generateMessageTag(), type: 'set', xmlns: 'newsletter', to: jid }, content: [{ tag: 'live_updates', attrs: {}, content: [] }] })
471
+ const node = getBinaryNodeChild(result, 'live_updates')
472
+ return node?.attrs?.duration ? { duration: node.attrs.duration } : null
473
+ },
474
+
475
+ // ── Channel settings ──────────────────────────────────────────────────
476
+
477
+ newsletterUpdateName: (jid, name) => newsletterUpdate(jid, { name }),
478
+ newsletterUpdateDescription: (jid, description) => newsletterUpdate(jid, { description }),
479
+ newsletterRemovePicture: (jid) => newsletterUpdate(jid, { picture: '' }),
480
+
481
+ newsletterUpdateReactions: async (jid, setting) => {
482
+ const value = String(setting ?? '').toUpperCase()
483
+ if (!REACTION_SETTINGS.has(value)) throw new Boom(`reaction setting must be one of ${[...REACTION_SETTINGS].join(', ')}`, { statusCode: 400, data: { setting } })
484
+ return newsletterUpdate(jid, { settings: { reaction_codes: { value } } })
485
+ },
486
+
487
+ newsletterUpdatePicture: async (jid, content) => {
488
+ const { img } = await generateProfilePicture(content)
489
+ return newsletterUpdate(jid, { picture: img.toString('base64') })
490
+ },
491
+
492
+ // ── Admin management ──────────────────────────────────────────────────
493
+
494
+ newsletterAdminCount: async (jid) => {
495
+ const res = await wmex({ newsletter_id: jid }, QueryIds.ADMIN_COUNT, XWAPaths.xwa2_newsletter_admin_count)
496
+ return res.admin_count
497
+ },
498
+
499
+ newsletterAdminInfo: async (jid) => {
500
+ const res = await wmex({ newsletter_id: jid }, QueryIds.ADMIN_INFO, XWAPaths.xwa2_newsletter_admin_info)
501
+ return {
502
+ id: res?.id ?? jid,
503
+ adminCount: res?.admin_count ?? 0,
504
+ adminProfile: res?.admin_profile ? { id: res.admin_profile.id, name: res.admin_profile.name, picture: res.admin_profile.picture ? { id: res.admin_profile.picture.id, directPath: res.admin_profile.picture.direct_path } : undefined } : undefined,
505
+ adminProfilesEnabled: res?.admin_settings?.admin_profiles_enabled ?? false
506
+ }
507
+ },
508
+
509
+ newsletterAdminCapabilities: async (jid) => {
510
+ const res = await wmex({ newsletter_id: jid }, QueryIds.ADMIN_CAPABILITIES, XWAPaths.xwa2_newsletter_admin_capabilities)
511
+ return res?.capabilities ?? []
512
+ },
513
+
514
+ newsletterCanPostStatus: async (jid) => {
515
+ const res = await wmex({ newsletter_id: jid }, QueryIds.ADMIN_CAPABILITIES, XWAPaths.xwa2_newsletter_admin_capabilities)
516
+ const list = res?.capabilities ?? []
517
+ return { canPost: list.includes('CHANNEL_STATUS_PRODUCER'), canPostMusic: list.includes('CHANNEL_STATUS_MUSIC'), capabilities: list }
518
+ },
519
+
520
+ newsletterCreateAdminInvite: (jid, userJid) => wmex({ newsletter_id: jid, user_id: userJid }, QueryIds.CREATE_ADMIN_INVITE, XWAPaths.xwa2_newsletter_admin_invite_create),
521
+ newsletterRevokeAdminInvite: (jid, userJid) => wmex({ newsletter_id: jid, user_id: userJid }, QueryIds.REVOKE_ADMIN_INVITE, XWAPaths.xwa2_newsletter_admin_invite_revoke),
522
+ newsletterAcceptAdminInvite: (jid) => wmex({ newsletter_id: jid }, QueryIds.ACCEPT_ADMIN_INVITE, XWAPaths.xwa2_newsletter_admin_invite_accept),
523
+
524
+ newsletterPendingAdminInvites: async (jid) => {
525
+ const res = await wmex({ newsletter_id: jid }, QueryIds.PENDING_ADMIN_INVITES, XWAPaths.pending_admin_invites)
526
+ return (res?.pending_admin_invites ?? []).map(invite => ({ id: invite?.user?.id, phoneNumber: invite?.user?.pn }))
527
+ },
528
+
529
+ // ── Followers & subscribers ───────────────────────────────────────────
530
+
531
+ newsletterFollowers: (jid, opts = {}) => wmex({ input: { newsletter_id: jid, count: opts.count ?? 100 } }, QueryIds.FOLLOWERS, XWAPaths.xwa2_newsletter_followers),
532
+
533
+ newsletterSubscribers: async (jid) => {
534
+ const res = await wmex({ newsletter_id: jid }, QueryIds.SUBSCRIBERS, XWAPaths.xwa2_newsletter_metadata)
535
+ return (res?.subscribers?.edges ?? res?.followers?.edges ?? []).map(edge => ({ id: edge?.node?.id, phoneNumber: edge?.node?.pn, displayName: edge?.node?.display_name, username: edge?.node?.username_info?.username, role: edge?.role, followTime: edge?.follow_time }))
536
+ },
537
+
538
+ // ── Messages & reactions ──────────────────────────────────────────────
539
+
540
+ newsletterReactMessage: async (jid, serverId, reaction) => {
541
+ await query({ tag: 'message', attrs: { to: jid, ...(reaction ? {} : { edit: '7' }), type: 'reaction', server_id: serverId, id: generateMessageTag() }, content: [{ tag: 'reaction', attrs: reaction ? { code: reaction } : {} }] })
542
+ },
543
+
544
+ newsletterBulkReactions: async (jid, serverId, emojis, count = 1, options = {}) => {
545
+ assertNewsletterJid(jid)
546
+ const { fake = false, delayMs = 100, mode = 'repeat' } = options
547
+ const max = Math.min(count, 1000)
548
+ const emojiList = Array.isArray(emojis) ? emojis : [emojis]
549
+ const total = mode === 'each' ? max * emojiList.length : max
550
+ const pickEmoji = (i) => mode === 'random' ? emojiList[Math.floor(Math.random() * emojiList.length)] : mode === 'each' ? emojiList[Math.floor(i / max)] : emojiList[i % emojiList.length]
551
+ if (fake) { const events = []; for (let i = 0; i < total; i++) events.push({ key: { remoteJid: jid, fromMe: false, id: String(serverId), participant: `${Math.floor(Math.random() * 999999999)}${i}@s.whatsapp.net` }, reaction: { key: { remoteJid: jid, fromMe: false, id: String(serverId) }, text: pickEmoji(i), senderTimestampMs: Date.now() + i } }); sock.ev.emit('messages.reaction', events); return { sent: events.length, fake, jid } }
552
+ let sent = 0
553
+ for (let i = 0; i < total; i++) { try { await sock.newsletterReactMessage(jid, serverId, pickEmoji(i)); sent++; if (delayMs > 0) await wait(delayMs) } catch (_) { } }
554
+ return { sent, fake, jid }
555
+ },
556
+
557
+ newsletterFetchMessages: async (jid, count = 20, since, after) => {
558
+ const attrs = { count: String(count) }
559
+ if (since !== undefined) attrs.since = String(since)
560
+ if (after !== undefined) attrs.after = String(after)
561
+ const result = await query({ tag: 'iq', attrs: { id: generateMessageTag(), type: 'get', xmlns: 'newsletter', to: jid }, content: [{ tag: 'message_updates', attrs, content: undefined }] })
562
+ const wrapper = getBinaryNodeChild(result, 'message_updates') ?? getBinaryNodeChild(result, 'messages')
563
+ if (!wrapper) return []
564
+ const messagesNode = getBinaryNodeChild(wrapper, 'messages') ?? wrapper
565
+ return decodeNewsletterMessageNodes(messagesNode, messagesNode.attrs?.jid ?? result.attrs?.from ?? jid, config.logger)
566
+ },
567
+
568
+ newsletterFetchMessageUpdates: async (jid, options = {}) => {
569
+ const { count = 20, since, before, after } = options
570
+ const attrs = { count: String(count) }
571
+ if (since !== undefined) attrs.since = String(since)
572
+ if (before !== undefined) attrs.before = String(before)
573
+ else if (after !== undefined) attrs.after = String(after)
574
+ const result = await query({ tag: 'iq', attrs: { id: generateMessageTag(), type: 'get', xmlns: 'newsletter', to: jid }, content: [{ tag: 'message_updates', attrs, content: undefined }] })
575
+ const updates = getBinaryNodeChild(result, 'message_updates')
576
+ const messages = updates && getBinaryNodeChild(updates, 'messages')
577
+ return { jid: messages?.attrs?.jid ?? jid, messages: messages ? decodeNewsletterMessageNodes(messages, messages.attrs?.jid ?? jid) : [] }
578
+ },
579
+
580
+ newsletterPinMessages: (jid, serverIds) => wmex({ newsletter_id: jid, input: { message_ids: toNewsletterServerIds(serverIds) } }, QueryIds.PIN_MESSAGES, XWAPaths.xwa2_newsletter_pin_messages),
581
+ newsletterUnpinMessages: (jid, serverIds) => wmex({ newsletter_id: jid, input: { message_ids: toNewsletterServerIds(serverIds) } }, QueryIds.UNPIN_MESSAGES, XWAPaths.xwa2_newsletter_unpin_messages),
582
+
583
+ newsletterLabelAiContent: (jid, serverId, messageType = 'MESSAGE') => wmex({ newsletter_id: jid, server_id: String(serverId), message_type: messageType }, QueryIds.LABEL_AI_CONTENT, XWAPaths.xwa2_newsletter_label_ai_content),
584
+ newsletterLabelPaidPartnership: (jid, serverId, messageType = 'MESSAGE') => wmex({ newsletter_id: jid, server_id: String(serverId), message_type: messageType }, QueryIds.PAID_PARTNERSHIP_LABEL, XWAPaths.xwa2_newsletter_label_paid_partnership),
585
+
586
+ newsletterSendPollVote: async (jid, parentServerId, options) => {
587
+ const names = Array.isArray(options) ? options : [options]
588
+ const votes = names.map(name => ({ tag: 'vote', attrs: {}, content: createHash('sha256').update(String(name), 'utf-8').digest() }))
589
+ const messageId = generateMessageTag()
590
+ await query({ tag: 'message', attrs: { to: jid, id: messageId, type: 'poll', server_id: String(parentServerId) }, content: [{ tag: 'meta', attrs: { polltype: 'vote' } }, { tag: 'votes', attrs: {}, content: votes }] })
591
+ return { id: messageId }
592
+ },
593
+
594
+ // ── Reactions & poll voters ───────────────────────────────────────────
595
+
596
+ newsletterReactionSenders: (jid, serverId) => wmex({ input: { id: jid, server_id: String(serverId) } }, QueryIds.REACTION_SENDER_LIST, XWAPaths.xwa2_newsletters_reaction_sender_list),
597
+ newsletterPollVoters: (jid, serverId, opts = {}) => wmex({ input: { newsletter_id: jid, server_id: String(serverId), limit: opts.limit ?? 100, vote_hash: opts.voteHash } }, QueryIds.POLL_VOTERS, XWAPaths.voter_list),
598
+
599
+ // ── Question / response interactions ──────────────────────────────────
600
+
601
+ newsletterQuestionResponses: async (jid, serverId, options = {}) => {
602
+ const { count = 20, before, filter, searchText } = options
603
+ const attrs = { server_id: String(serverId), count: String(count) }
604
+ if (before !== undefined) attrs.before = String(before)
605
+ const content = []
606
+ if (filter) content.push({ tag: 'filters', attrs: {}, content: [{ tag: filter, attrs: {}, content: undefined }] })
607
+ if (searchText) content.push({ tag: 'search', attrs: { text: searchText }, content: undefined })
608
+ const result = await query({ tag: 'iq', attrs: { id: generateMessageTag(), type: 'get', xmlns: 'newsletter', to: jid }, content: [{ tag: 'question_responses', attrs, content: content.length ? content : undefined }] })
609
+ const responses = getBinaryNodeChild(result, 'question_responses')
610
+ if (!responses) return { jid, serverId: Number(serverId), responses: [] }
611
+ return {
612
+ jid: result.attrs?.from ?? jid,
613
+ serverId: responses.attrs?.server_id ? Number(responses.attrs.server_id) : Number(serverId),
614
+ responses: getBinaryNodeChildren(responses, 'question_response').map((entry) => {
615
+ const messageNode = getBinaryNodeChild(entry, 'message')
616
+ const sender = getBinaryNodeChild(entry, 'sender')
617
+ const picture = sender && getBinaryNodeChild(sender, 'picture')
618
+ const flags = getBinaryNodeChild(entry, 'flags')
619
+ const plaintext = messageNode && getBinaryNodeChild(messageNode, 'plaintext')
620
+ return {
621
+ id: messageNode?.attrs?.id,
622
+ t: messageNode?.attrs?.t ? Number(messageNode.attrs.t) : undefined,
623
+ isSender: messageNode?.attrs?.is_sender === 'true',
624
+ responseServerId: messageNode?.attrs?.response_server_id,
625
+ sender: { lid: sender?.attrs?.lid, notifyName: sender?.attrs?.notify_name, pictureDirectPath: picture?.attrs?.direct_path },
626
+ replied: flags ? !!getBinaryNodeChild(flags, 'replied') : false,
627
+ starred: flags ? !!getBinaryNodeChild(flags, 'starred') : false,
628
+ message: decodeNewsletterPlaintext(plaintext)
629
+ }
630
+ })
631
+ }
632
+ },
633
+
634
+ newsletterQuestionResponseState: (jid, serverId, responseServerId, state) =>
635
+ wmex({ newsletter_id: jid, server_id: String(serverId), response_server_id: String(responseServerId), state }, QueryIds.QUESTION_RESPONSE_STATE, XWAPaths.xwa2_newsletter_question_response_state_update),
636
+
637
+ // ── Analytics & insights ──────────────────────────────────────────────
638
+
639
+ newsletterInsights: (jid, opts = {}) => wmex({ input: { newsletter_id: jid, metrics: opts.metrics ?? ['NET_FOLLOWS', 'UNFOLLOWS'] } }, QueryIds.INSIGHTS, XWAPaths.xwa2_newsletter_admin_insights),
640
+
641
+ // ── Enforcements & reports ────────────────────────────────────────────
642
+
643
+ newsletterEnforcements: async (jid, locale = 'en_US') => {
644
+ const res = await wmex({ newsletter_id: jid, locale }, QueryIds.ENFORCEMENTS, XWAPaths.xwa2_channel_enforcements)
645
+ const mapBase = (entry) => ({
646
+ enforcementId: entry?.enforcement_id,
647
+ createdAt: entry?.enforcement_creation_time ? Number(entry.enforcement_creation_time) : undefined,
648
+ violationCategory: entry?.enforcement_violation_category,
649
+ source: entry?.enforcement_source,
650
+ appealState: entry?.appeal_state,
651
+ appealCreatedAt: entry?.appeal_creation_time ? Number(entry.appeal_creation_time) : undefined,
652
+ appealReasonOptions: (entry?.appeal_reason_options ?? []).map(o => ({ reason: o?.reason, label: o?.label })),
653
+ appealFormUrl: entry?.enforcement_extra_data?.ip_violation_report_data?.appeal_form_url,
654
+ policy: entry?.enforcement_policy_information ? { headline: entry.enforcement_policy_information.headline, subtitle: entry.enforcement_policy_information.subtitle, overview: entry.enforcement_policy_information.overview, explanation: entry.enforcement_policy_information.explanation, adminDisclaimer: entry.enforcement_policy_information.admin_disclaimer } : undefined
655
+ })
656
+ const nested = (list) => (list ?? []).map(entry => mapBase(entry?.base_enforcement_data ?? entry))
657
+ return { adminProfiles: (res?.admin_profiles ?? []).map(mapBase), profilePictureDeletions: (res?.profile_picture_deletions ?? []).map(mapBase), suspensions: (res?.suspensions ?? []).map(mapBase), violatingMessages: nested(res?.violating_messages), geoSuspensions: nested(res?.geosuspensions) }
658
+ },
659
+
660
+ newsletterReports: (locale = 'en_US') => wmex({ locale }, QueryIds.CHANNEL_REPORTS, XWAPaths.xwa2_channels_reports),
661
+ newsletterAppealReport: (reportId, reason) => wmex({ report_id: String(reportId), reason }, QueryIds.CREATE_REPORT_APPEAL, XWAPaths.xwa2_create_channel_report_appeal_v2),
662
+
663
+ // ── Directory & discovery ─────────────────────────────────────────────
664
+
665
+ newsletterDirectoryList: (opts = {}) => wmex({ fetch_status_metadata: opts.fetchStatusMetadata ?? false, input: { view: opts.view ?? 'RECOMMENDED', filters: { country_codes: opts.countryCodes ?? [], categories: opts.categories ?? [] }, limit: opts.limit ?? 20, start_cursor: opts.cursorToken } }, QueryIds.DIRECTORY_LIST, XWAPaths.xwa2_newsletters_directory_list),
666
+ newsletterDirectorySearch: (text, opts = {}) => wmex({ fetch_status_metadata: opts.fetchStatusMetadata ?? false, input: { search_text: text, categories: opts.categories ?? [], limit: opts.limit ?? 20, start_cursor: opts.cursorToken } }, QueryIds.DIRECTORY_SEARCH, XWAPaths.xwa2_newsletters_directory_search),
667
+ newsletterDirectoryCategories: (opts = {}) => wmex({ fetch_status_metadata: opts.fetchStatusMetadata ?? false, input: { categories: opts.categories ?? [], country_code: opts.countryCode || undefined, per_category_limit: opts.perCategoryLimit ?? 10 } }, QueryIds.DIRECTORY_CATEGORIES, XWAPaths.xwa2_newsletters_directory_category_preview),
668
+ newsletterRecommended: (opts = {}) => wmex({ fetch_status_metadata: opts.fetchStatusMetadata ?? false, input: { limit: opts.limit ?? 20, country_codes: opts.countryCodes ?? [] } }, QueryIds.RECOMMENDED, XWAPaths.xwa2_newsletters_recommended),
669
+ newsletterSimilar: (jid, opts = {}) => wmex({ fetch_status_metadata: opts.fetchStatusMetadata ?? false, input: { newsletter_id: jid, limit: opts.limit ?? 20, country_codes: opts.countryCodes ?? [] } }, QueryIds.SIMILAR, XWAPaths.xwa2_newsletters_similar),
670
+
671
+ // ── Add-ons ───────────────────────────────────────────────────────────
672
+
673
+ newsletterMyAddOns: (opts = {}) => fetchMyAddOns(opts, undefined),
674
+ newsletterStatusMyAddOns: (opts = {}) => fetchMyAddOns(opts, 'status'),
675
+
676
+ // ── Status publishing ─────────────────────────────────────────────────
677
+
678
+ newsletterSendStatus: async (jid, content, options = {}) => {
679
+ assertNewsletterJid(jid)
680
+ if (!content || typeof content !== 'object' || Array.isArray(content)) throw new TypeError('Newsletter status content must be an object')
681
+ const userJid = sock.authState?.creds?.me?.id
682
+ if (!userJid) throw new TypeError('Not authenticated')
683
+ const { mediaId, mediaHandle: requestedMediaHandle, parentServerId, responseServerId, interactionType: requestedInteractionType, aiContent, statusAttribution = true, messageId: requestedMessageId, ackTimeoutMs, resolveServerId, serverIdTimeoutMs, transport, ...messageOptions } = options
684
+ if (transport !== undefined) config.logger?.warn?.({ transport }, 'newsletter status "transport" option is obsolete')
685
+ let uploadedMediaHandle
686
+ const upload = async (...args) => {
687
+ const result = await sock.waUploadToServer(...args)
688
+ if (args[1]?.newsletter) uploadedMediaHandle = result?.handle ?? result?.media_id ?? result?.mediaId ?? result?.fbid ?? uploadedMediaHandle
689
+ return result
690
+ }
691
+ const preparedContent = prepareModernMessageContent(content)
692
+ const fullMsg = await generateWAMessage(jid, statusAttribution ? withNewsletterStatusAttribution(preparedContent) : preparedContent, { logger: config.logger, userJid, upload, mediaCache: config.mediaCache, options: config.options, ...messageOptions, messageId: requestedMessageId || generateMessageIDV2(userJid) })
693
+ const normalized = normalizeMessageContent(fullMsg.message)
694
+ const mediaType = getNewsletterStatusMediaType(fullMsg.message)
695
+ if (!mediaType && (normalized?.documentMessage || normalized?.stickerMessage)) throw new TypeError('Native newsletter status supports text, image, video, gif, and audio')
696
+ if (mediaType && !STATUS_WEB_MEDIA_TYPES.has(mediaType)) config.logger?.warn?.({ mediaType }, 'newsletter status media type not published by WA Web, server may reject it')
697
+ const interactionType = requestedInteractionType || (content.question ? 'question' : undefined)
698
+ if (interactionType === 'question' && !mediaType) config.logger?.warn?.('WA Web only publishes question statuses on top of media, a text question status may be rejected')
699
+ const node = buildNewsletterStatusNode({ jid, message: fullMsg.message, messageId: fullMsg.key.id, mediaType, mediaHandle: requestedMediaHandle ?? mediaId ?? uploadedMediaHandle, parentServerId, responseServerId, interactionType, aiContent })
700
+ const echo = resolveServerId === false ? null : waitForNewsletterStatusServerId(sock, { jid, messageId: fullMsg.key.id, timeoutMs: serverIdTimeoutMs ?? STATUS_SERVER_ID_TIMEOUT_MS })
701
+ let ack
702
+ try { ack = await sendStatusNode(node, { jid, messageId: fullMsg.key.id, timeoutMs: ackTimeoutMs ?? STATUS_ACK_TIMEOUT_MS }) }
703
+ catch (error) { echo?.cancel(); throw error }
704
+ const delivered = echo ? await echo : undefined
705
+ fullMsg.status = STATUS_SERVER_ACK
706
+ fullMsg.newsletterStatusServerId = ack.serverId ?? delivered?.serverId
707
+ fullMsg.newsletterStatusAck = ack
708
+ fullMsg.newsletterStatusResponse = ack.node
709
+ if (delivered) fullMsg.newsletterStatusDelivered = delivered.node
710
+ return fullMsg
711
+ },
712
+
713
+ newsletterReactStatus: async (jid, parentServerId, reaction, options = {}) => {
714
+ assertNewsletterJid(jid)
715
+ const userJid = sock.authState?.creds?.me?.id
716
+ if (!userJid) throw new TypeError('Not authenticated')
717
+ const messageId = options.messageId || generateMessageIDV2(userJid)
718
+ const node = buildNewsletterStatusReactionNode({ jid, messageId, parentServerId, reaction })
719
+ const ack = await sendStatusNode(node, { jid, messageId, timeoutMs: options.ackTimeoutMs ?? STATUS_ACK_TIMEOUT_MS })
720
+ return { key: { remoteJid: jid, fromMe: true, id: messageId }, status: STATUS_SERVER_ACK, newsletterStatusServerId: ack.serverId, newsletterStatusAck: ack, newsletterStatusResponse: ack.node }
721
+ },
722
+
723
+ newsletterRevokeStatus: async (jid, statusId, options = {}) => {
724
+ assertNewsletterJid(jid)
725
+ const userJid = sock.authState?.creds?.me?.id
726
+ if (!userJid) throw new TypeError('Not authenticated')
727
+ const node = buildNewsletterStatusRevokeNode({ jid, statusId })
728
+ const ack = await sendStatusNode(node, { jid, messageId: statusId, timeoutMs: options.ackTimeoutMs ?? STATUS_ACK_TIMEOUT_MS })
729
+ return { key: { remoteJid: jid, fromMe: true, id: statusId }, status: STATUS_SERVER_ACK, newsletterStatusAck: ack, newsletterStatusResponse: ack.node }
730
+ },
731
+
732
+ newsletterFetchStatus: async (jid, options = {}) => {
733
+ assertNewsletterJid(jid)
734
+ const { count = 20, before, after, viewRole } = options
735
+ const attrs = { type: 'jid', jid, count: String(count) }
736
+ if (isPresent(viewRole)) attrs.view_role = String(viewRole).toLowerCase()
737
+ if (isPresent(before)) attrs.before = toNewsletterServerId(before, 'before')
738
+ else if (isPresent(after)) attrs.after = toNewsletterServerId(after, 'after')
739
+ return parseNewsletterStatusesResponse(await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, xmlns: 'newsletter', type: 'get' }, content: [{ tag: 'statuses', attrs, content: undefined }] }))
740
+ },
741
+
742
+ newsletterFetchStatusUpdates: async (jid, options = {}) => {
743
+ assertNewsletterJid(jid)
744
+ const { count = 20, since, before, after } = options
745
+ const attrs = { count: String(count) }
746
+ if (isPresent(since)) attrs.since = String(since)
747
+ if (isPresent(before)) attrs.before = toNewsletterServerId(before, 'before')
748
+ else if (isPresent(after)) attrs.after = toNewsletterServerId(after, 'after')
749
+ return parseNewsletterStatusUpdatesResponse(await query({ tag: 'iq', attrs: { to: jid, xmlns: 'newsletter', type: 'get' }, content: [{ tag: 'status_updates', attrs, content: undefined }] }))
750
+ },
751
+ }
752
+ }