@zervonex/baileys 3.0.1 → 3.0.2

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.
@@ -530,7 +530,6 @@ export const makeSocket = (config) => {
530
530
  // ─── Pairing ─────────────────────────────────────────────────────────────────
531
531
 
532
532
  const requestPairingCode = async (phoneNumber, customPairingCode) => {
533
- if (authState.creds.pairingCode && !customPairingCode) return authState.creds.pairingCode
534
533
  await waitForSocketOpen()
535
534
  await new Promise((resolve) => setTimeout(resolve, 500))
536
535
  const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5))
@@ -1,55 +1,55 @@
1
- import crypto from 'crypto'
2
- import * as Builders from '../Builders/index.js'
3
- import { buildDispatch, buildPlugins, norm, handleGroupStatus, handleStatusMention, handlePollResult, PRIMITIVE_SHORTCUTS, HANDLER_SHORTCUTS, attachAIRich } from '../Builders/index.js'
4
- import { makeUsernameSocket } from './username.js'
5
-
6
- export const makeMessageBuilderSocket = (config) => {
7
- const sock = makeUsernameSocket(config)
8
- const _send = sock.sendMessage.bind(sock)
9
-
10
- const ALL_BUILDERS = Object.values(Builders).filter(B => typeof B === 'function' && (B.dispatchKeys || B.shortcuts || B.name))
11
-
12
- const DISPATCH = buildDispatch(ALL_BUILDERS, {
13
- groupStatus: (sock, jid, content, opts, config) => handleGroupStatus(sock, jid, norm(content.groupStatus), config),
14
- statusMentionMessage: (sock, jid, content, opts, config) => handleStatusMention(sock, jid, norm(content.statusMentionMessage), config),
15
- pollResultMessage: (sock, jid, content) => handlePollResult(sock, jid, norm(content.pollResultMessage))
16
- })
17
-
18
- const PLUGINS = buildPlugins(ALL_BUILDERS, { ...PRIMITIVE_SHORTCUTS, ...HANDLER_SHORTCUTS })
19
-
20
- const result = { ...sock }
21
-
22
- for (const [name, factory] of Object.entries(PLUGINS)) result[name] = factory(result, config)
23
-
24
- result.sendMessage = async (jid, content, options = {}) => {
25
- // aiRich shorthand — multiple aliases supported
26
- const aiData = content.aiRich ?? content.airich ?? content.richResponse ?? content.AIRich
27
- if (aiData) return result.sendRichFromObject(jid, aiData, options)
28
-
29
- // dispatch to registered builder handlers
30
- for (const [key, handler] of Object.entries(DISPATCH)) {
31
- if (key in content) return handler(result, jid, content, options, config)
32
- }
33
-
34
- // bare nativeFlow array/object — wrap into interactiveMessage before sending
35
- if (content.nativeFlow && !content.interactiveMessage) {
36
- const buttons = Array.isArray(content.nativeFlow) ? content.nativeFlow : [content.nativeFlow]
37
- const { generateWAMessageFromContent } = await import('../Utils/index.js')
38
- const msg = await generateWAMessageFromContent(jid, {
39
- interactiveMessage: { body: { text: content.text || '' }, footer: { text: content.footer || '' }, header: { hasMediaAttachment: false }, nativeFlowMessage: { buttons } }
40
- }, { userJid: sock.user?.id })
41
- await result.relayMessage(jid, msg.message, { messageId: msg.key.id, aiLabel: !!(content.ai ?? content.aiLabel ?? options.aiLabel ?? options.ai ?? config.aiLabel ?? config.ai) })
42
- return msg
43
- }
44
-
45
- const { ai, aiLabel: contentAiLabel, ...cleanContent } = content
46
- const isAi = !!(ai ?? contentAiLabel ?? options.aiLabel ?? options.ai ?? config.aiLabel)
47
- if (isAi) {
48
- const secret = cleanContent.messageContextInfo?.messageSecret || crypto.randomBytes(32)
49
- cleanContent.messageContextInfo = { ...(cleanContent.messageContextInfo || {}), messageSecret: secret, supportPayload: JSON.stringify({ version: 2, is_ai_message: true, should_show_system_message: true, ticket_id: crypto.randomBytes(16).toString('hex') }) }
50
- }
51
- return _send(jid, isAi ? cleanContent : content, { ...options, aiLabel: isAi })
52
- }
53
-
54
- return attachAIRich(result, config)
1
+ import crypto from 'crypto'
2
+ import * as Builders from '../Builders/index.js'
3
+ import { buildDispatch, buildPlugins, norm, handleGroupStatus, handleStatusMention, handlePollResult, PRIMITIVE_SHORTCUTS, HANDLER_SHORTCUTS, attachAIRich } from '../Builders/index.js'
4
+ import { makeUsernameSocket } from './username.js'
5
+
6
+ export const makeMessageBuilderSocket = (config) => {
7
+ const sock = makeUsernameSocket(config)
8
+ const _send = sock.sendMessage.bind(sock)
9
+
10
+ const ALL_BUILDERS = Object.values(Builders).filter(B => typeof B === 'function' && (B.dispatchKeys || B.shortcuts || B.name))
11
+
12
+ const DISPATCH = buildDispatch(ALL_BUILDERS, {
13
+ groupStatus: (sock, jid, content, opts, config) => handleGroupStatus(sock, jid, norm(content.groupStatus), config),
14
+ statusMentionMessage: (sock, jid, content, opts, config) => handleStatusMention(sock, jid, norm(content.statusMentionMessage), config),
15
+ pollResultMessage: (sock, jid, content) => handlePollResult(sock, jid, norm(content.pollResultMessage))
16
+ })
17
+
18
+ const PLUGINS = buildPlugins(ALL_BUILDERS, { ...PRIMITIVE_SHORTCUTS, ...HANDLER_SHORTCUTS })
19
+
20
+ const result = { ...sock }
21
+
22
+ for (const [name, factory] of Object.entries(PLUGINS)) result[name] = factory(result, config)
23
+
24
+ result.sendMessage = async (jid, content, options = {}) => {
25
+ // aiRich shorthand — multiple aliases supported
26
+ const aiData = content.aiRich ?? content.airich ?? content.richResponse ?? content.AIRich
27
+ if (aiData) return result.sendRichFromObject(jid, aiData, options)
28
+
29
+ // dispatch to registered builder handlers
30
+ for (const [key, handler] of Object.entries(DISPATCH)) {
31
+ if (key in content) return handler(result, jid, content, options, config)
32
+ }
33
+
34
+ // bare nativeFlow array/object — wrap into interactiveMessage before sending
35
+ if (content.nativeFlow && !content.interactiveMessage) {
36
+ const buttons = Array.isArray(content.nativeFlow) ? content.nativeFlow : [content.nativeFlow]
37
+ const { generateWAMessageFromContent } = await import('../Utils/index.js')
38
+ const msg = await generateWAMessageFromContent(jid, {
39
+ interactiveMessage: { body: { text: content.text || '' }, footer: { text: content.footer || '' }, header: { hasMediaAttachment: false }, nativeFlowMessage: { buttons } }
40
+ }, { userJid: sock.user?.id })
41
+ await result.relayMessage(jid, msg.message, { messageId: msg.key.id, aiLabel: !!(content.ai ?? content.aiLabel ?? options.aiLabel ?? options.ai ?? config.aiLabel ?? config.ai) })
42
+ return msg
43
+ }
44
+
45
+ const { ai, aiLabel: contentAiLabel, ...cleanContent } = content
46
+ const isAi = !!(ai ?? contentAiLabel ?? options.aiLabel ?? options.ai ?? config.aiLabel)
47
+ if (isAi) {
48
+ const secret = cleanContent.messageContextInfo?.messageSecret || crypto.randomBytes(32)
49
+ cleanContent.messageContextInfo = { ...(cleanContent.messageContextInfo || {}), messageSecret: secret, supportPayload: JSON.stringify({ version: 2, is_ai_message: true, should_show_system_message: true, ticket_id: crypto.randomBytes(16).toString('hex') }) }
50
+ }
51
+ return _send(jid, isAi ? cleanContent : content, { ...options, aiLabel: isAi })
52
+ }
53
+
54
+ return attachAIRich(result, config)
55
55
  }
@@ -102,6 +102,7 @@ export const decodeSyncdMutations = async (msgMutations, initialState, getAppSta
102
102
  const record = 'record' in msgMutation && msgMutation.record ? msgMutation.record : msgMutation
103
103
  let key
104
104
  try { key = await getKey(record.keyId.id) } catch (err) { if (isMissingKeyError(err)) throw err; continue }
105
+ if (!record?.value?.blob) continue
105
106
  const content = record.value.blob
106
107
  const encContent = content.subarray(0, -32)
107
108
  const ogValueMac = content.subarray(-32)
@@ -1,39 +1,38 @@
1
- export * from './generics.js'
2
- export * from './decode-wa-message.js'
3
- export * from './messages.js'
4
- export * from './messages-media.js'
5
- export * from './validate-connection.js'
6
- export * from './crypto.js'
7
- export * from './signal.js'
8
- export * from './make-mutex.js'
9
- export * from './logger.js'
10
- export * from './noise-handler.js'
11
- export * from './history.js'
12
- export * from './chat-utils.js'
13
- export * from './lt-hash.js'
14
- export * from './auth-utils.js'
15
- export * from './baileys-event-stream.js'
16
- export * from './use-multi-file-auth-state.js'
17
- export * from './link-preview.js'
18
- export * from './event-buffer.js'
19
- export * from './process-message.js'
20
- export * from './message-retry-manager.js'
21
- export * from './browser-utils.js'
22
- export * from './identity-change-handler.js'
23
- export * from './key-store.js'
24
- export * from './reporting-utils.js'
25
- export * from './companion-reg-client-utils.js'
26
- export * from './tc-token-utils.js'
27
- export * from './meta-ai-msmsg.js'
28
- export * from './spam-report-utils.js'
29
- export * from './modern-messages.js'
30
- export * from './extras.js'
31
- export * from './native-flow.js'
32
- export * from './consumer-application.js'
33
- export * from './bot-capabilities.js'
34
- export * from './meta-compositing.js'
35
- export * from './bot-planning-replay.js'
36
- export * from './message-composer.js'
37
- export * from './business.js'
38
- export * from './pre-key-manager.js'
39
- export * from './sync-action-utils.js'
1
+ export * from './generics.js'
2
+ export * from './decode-wa-message.js'
3
+ export * from './messages.js'
4
+ export * from './messages-media.js'
5
+ export * from './validate-connection.js'
6
+ export * from './crypto.js'
7
+ export * from './signal.js'
8
+ export * from './make-mutex.js'
9
+ export * from './logger.js'
10
+ export * from './noise-handler.js'
11
+ export * from './history.js'
12
+ export * from './chat-utils.js'
13
+ export * from './lt-hash.js'
14
+ export * from './auth-utils.js'
15
+ export * from './baileys-event-stream.js'
16
+ export * from './use-multi-file-auth-state.js'
17
+ export * from './link-preview.js'
18
+ export * from './event-buffer.js'
19
+ export * from './process-message.js'
20
+ export * from './message-retry-manager.js'
21
+ export * from './browser-utils.js'
22
+ export * from './identity-change-handler.js'
23
+ export * from './reporting-utils.js'
24
+ export * from './companion-reg-client-utils.js'
25
+ export * from './tc-token-utils.js'
26
+ export * from './meta-ai-msmsg.js'
27
+ export * from './spam-report-utils.js'
28
+ export * from './modern-messages.js'
29
+ export * from './extras.js'
30
+ export * from './native-flow.js'
31
+ export * from './consumer-application.js'
32
+ export * from './bot-capabilities.js'
33
+ export * from './meta-compositing.js'
34
+ export * from './bot-planning-replay.js'
35
+ export * from './message-composer.js'
36
+ export * from './business.js'
37
+ export * from './pre-key-manager.js'
38
+ export * from './sync-action-utils.js'
@@ -95,8 +95,9 @@ const compressWebp = async (buf, { animated, quality = 80, stickerType = Sticker
95
95
  return { buffer: converted.length <= STICKER_MAX_BYTES ? converted : await shrinkToLimit(converted, { animated: false }), isAnimated: false }
96
96
  }
97
97
 
98
- const shrinkToLimit = async (buf, { animated } = {}) => {
98
+ const shrinkToLimit = async (buf, { animated } = {}, _depth = 0) => {
99
99
  if (buf.length <= STICKER_MAX_BYTES) return buf
100
+ if (_depth >= 2) return buf
100
101
  const ratio = STICKER_MAX_BYTES / buf.length
101
102
  const q = Math.max(10, Math.min(60, Math.floor(ratio * 80 * 0.85)))
102
103
  const out = (await compressWebp(buf, { animated, quality: q })).buffer
@@ -134,11 +135,15 @@ const tgsToWebp = async (tgsBuffer, { quality = 80, fps = 30 } = {}) => {
134
135
  const rgba = Buffer.from(api.HEAPU8.buffer, bufPtr, W * H * 4)
135
136
  await sharp(Buffer.from(rgba), { raw: { width: W, height: H, channels: 4 } }).png().toFile(path.join(tgsTmpDir, `f${String(i).padStart(4, '0')}.png`))
136
137
  }
137
- api.lottie_destroy(handle)
138
- const outPath = path.join(tgsTmpDir, 'out.webp')
139
- await ffmpegRun(['-y', '-framerate', String(fps), '-i', path.join(tgsTmpDir, 'f%04d.png'), '-vcodec', 'libwebp', '-vf', `scale=${W}:${H}`, '-lossless', '0', '-compression_level', '6', '-q:v', String(quality), '-loop', '0', '-preset', 'default', '-an', '-vsync', '0', outPath])
140
- return readFileSync(outPath)
141
- } finally { rmSync(tgsTmpDir, { recursive: true, force: true }) }
138
+ const encode = async (outFps, q) => { const outPath = path.join(tgsTmpDir, `out_${outFps}_${q}.webp`); await ffmpegRun(['-y', '-framerate', String(fps), '-i', path.join(tgsTmpDir, 'f%04d.png'), '-vcodec', 'libwebp', '-vf', `scale=512:512,fps=${outFps}`, '-lossless', '0', '-compression_level', '6', '-q:v', String(q), '-loop', '0', '-preset', 'default', '-an', '-vsync', '0', outPath]); return readFileSync(outPath) }
139
+ let out = await encode(fps, quality)
140
+ if (out.length <= STICKER_MAX_BYTES) return out
141
+ const targetFps = Math.max(8, Math.floor(fps * (STICKER_MAX_BYTES / out.length) * 0.9))
142
+ out = await encode(targetFps, quality)
143
+ if (out.length <= STICKER_MAX_BYTES) return out
144
+ const targetQ = Math.max(10, Math.floor(quality * (STICKER_MAX_BYTES / out.length) * 0.9))
145
+ return encode(targetFps, targetQ)
146
+ } finally { api.lottie_destroy(handle); rmSync(tgsTmpDir, { recursive: true, force: true }) }
142
147
  }
143
148
 
144
149
  // ─── EXPORTS: URL / LINK PREVIEW ─────────────────────────────────────────────
@@ -158,11 +163,12 @@ export const getContentType = (content) => {
158
163
  return Object.keys(content).find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage')
159
164
  }
160
165
 
161
- export const normalizeMessageContent = (content) => {
166
+ export const normalizeMessageContent = (content, logger) => {
162
167
  if (!content) return
163
168
  for (let i = 0; i < 5; i++) {
164
169
  const inner = WRAPPER_KEYS.reduce((found, k) => found || content?.[k], null)
165
170
  if (!inner) break
171
+ if (!inner.message) { logger?.warn({ i, keys: Object.keys(inner) }, 'normalizeMessageContent: inner wrapper has no message field'); break }
166
172
  content = inner.message
167
173
  }
168
174
  return content
@@ -183,6 +189,18 @@ export const extractMessageContent = (content) => {
183
189
  return content
184
190
  }
185
191
 
192
+ const normalizeExternalAdReplyThumbnail = async (ext) => {
193
+ if (!ext?.thumbnail || !Buffer.isBuffer(ext.thumbnail) || ext.thumbnail.length <= 50_000) return ext
194
+ let quality = 90
195
+ while (quality >= 10) {
196
+ const thumbnail = await sharp(ext.thumbnail, { sequentialRead: true }).resize(200, 200, { fit: 'inside' }).jpeg({ quality }).toBuffer().catch(() => null)
197
+ if (!thumbnail) return ext
198
+ if (thumbnail.length <= 50_000) return { ...ext, thumbnail }
199
+ quality = Math.max(10, Math.floor(quality * (50_000 / thumbnail.length) * 0.85))
200
+ }
201
+ return ext
202
+ }
203
+
186
204
  export const generateForwardMessageContent = (message, forceForward) => {
187
205
  let content = normalizeMessageContent(message.message)
188
206
  if (!content) throw new Boom('no content in message', { statusCode: 400 })
@@ -486,15 +504,7 @@ const MESSAGE_HANDLERS = {
486
504
  // Interactive overlays: keyed by the input field, each returns a complete replacement for m.
487
505
  // Looked up dynamically — adding a new overlay type = one new entry, nothing else.
488
506
  const applyContextInfoAndMentions = async (interactive, msg) => {
489
- if (msg.contextInfo) {
490
- const ctx = { ...(interactive.contextInfo || {}), ...msg.contextInfo }
491
- if (ctx.externalAdReply?.thumbnail && Buffer.isBuffer(ctx.externalAdReply.thumbnail) && ctx.externalAdReply.thumbnail.length > 50_000) {
492
- try {
493
- ctx.externalAdReply.thumbnail = await sharp(ctx.externalAdReply.thumbnail).resize(200, 200, { fit: 'cover', position: 'center' }).jpeg({ quality: 60 }).toBuffer()
494
- } catch { }
495
- }
496
- interactive.contextInfo = ctx
497
- }
507
+ if (msg.contextInfo) { const ctx = { ...(interactive.contextInfo || {}), ...msg.contextInfo }; if (ctx.externalAdReply) ctx.externalAdReply = await normalizeExternalAdReplyThumbnail(ctx.externalAdReply); interactive.contextInfo = ctx }
498
508
  if (msg.mentions?.length) interactive.contextInfo = { ...(interactive.contextInfo || {}), mentionedJid: msg.mentions }
499
509
  }
500
510
 
@@ -582,14 +592,14 @@ export const generateWAMessageContent = async (message, options = {}) => {
582
592
  }
583
593
 
584
594
  // Apply overlays: any overlay key present in message replaces m
585
- for (const key of overlayKeys) { if (key in message && message[key]) { m = OVERLAY_HANDLERS[key](m, message); break } }
595
+ for (const key of overlayKeys) { if (key in message && message[key]) { m = await OVERLAY_HANDLERS[key](m, message); break } }
586
596
 
587
597
  // Merge contextInfo / mentions onto the primary content key — deep-merges externalAdReply to prevent thumbnail/field loss
588
598
  const finalKey = Object.keys(m)[0]
589
599
  if ((message.contextInfo || message.mentions?.length) && finalKey && m[finalKey] && typeof m[finalKey] === 'object') {
590
- const existing = m[finalKey].contextInfo || {}
591
- const incoming = message.contextInfo || {}
592
- m[finalKey].contextInfo = { ...existing, ...incoming, ...(existing.externalAdReply || incoming.externalAdReply ? { externalAdReply: { ...(existing.externalAdReply || {}), ...(incoming.externalAdReply || {}) } } : {}), ...(message.mentions?.length ? { mentionedJid: message.mentions } : {}) }
600
+ const existing = m[finalKey].contextInfo || {}; const incoming = message.contextInfo || {}
601
+ const mergedExt = (existing.externalAdReply || incoming.externalAdReply) ? await normalizeExternalAdReplyThumbnail({ ...(existing.externalAdReply || {}), ...(incoming.externalAdReply || {}) }) : undefined
602
+ m[finalKey].contextInfo = { ...existing, ...incoming, ...(mergedExt ? { externalAdReply: mergedExt } : {}), ...(message.mentions?.length ? { mentionedJid: message.mentions } : {}) }
593
603
  }
594
604
 
595
605
  if ('viewOnce' in message && message.viewOnce || 'viewOnceMessage' in message) m = { viewOnceMessage: { message: m } }
@@ -1,142 +1,135 @@
1
- import { getBinaryNodeChild, getBinaryNodeChildString, isLidUser, isHostedLidUser, S_WHATSAPP_NET } from '../WABinary/index.js'
2
- import { extractMessageContent } from './messages.js'
3
-
4
- export const SPAM_FLOWS = {
5
- ACCOUNT_INFO_REPORT: 'account_info_report',
6
- ACCOUNT_INFO_BLOCK: 'account_info_block',
7
- ONE_TO_ONE_SPAM_BANNER_REPORT: '1_1_spam_banner_report',
8
- MESSAGE_MENU: 'message_menu',
9
- OVERFLOW_MENU_REPORT: 'overflow_menu_report',
10
- }
11
-
12
- const toBuffer = data => {
13
- if (!data) return null
14
- if (Buffer.isBuffer(data)) return data
15
- if (data instanceof Uint8Array) return Buffer.from(data)
16
- if (typeof data === 'string') return Buffer.from(data, 'base64')
17
- return null
18
- }
19
-
20
- export const resolveReportJid = async (jid, lidMapping) => {
21
- if (!jid) throw new Error('reportSpam: jid required')
22
- if (isLidUser(jid) || isHostedLidUser(jid)) return jid
23
- if (lidMapping?.getLIDForPN) {
24
- const lid = await lidMapping.getLIDForPN(jid)
25
- if (lid) return lid
26
- }
27
- return jid
28
- }
29
-
30
- export const normalizeReportingTag = tag => {
31
- const buf = toBuffer(tag)
32
- return buf?.length ? buf : null
33
- }
34
-
35
- export const extractMessageText = msg => {
36
- if (!msg?.message) return ''
37
- const content = extractMessageContent(msg.message)
38
- if (!content) return ''
39
- if (typeof content.conversation === 'string') return content.conversation
40
- if (content.extendedTextMessage?.text) return content.extendedTextMessage.text
41
- if (content.imageMessage?.caption) return content.imageMessage.caption
42
- if (content.videoMessage?.caption) return content.videoMessage.caption
43
- return ''
44
- }
45
-
46
- export const extractReportingInfoFromStanza = stanza => {
47
- const reporting = getBinaryNodeChild(stanza, 'reporting')
48
- if (!reporting) return null
49
-
50
- let tagNode = getBinaryNodeChild(reporting, 'reporting_tag')
51
- if (!tagNode) {
52
- const validation = getBinaryNodeChild(reporting, 'reporting_validation')
53
- tagNode = validation ? getBinaryNodeChild(validation, 'reporting_tag') : undefined
54
- }
55
- if (!tagNode) return null
56
-
57
- const raw = tagNode.content
58
- const reportingTag = toBuffer(
59
- raw instanceof Uint8Array || Buffer.isBuffer(raw) ? raw
60
- : typeof raw === 'string' ? raw
61
- : null
62
- )
63
- if (!reportingTag?.length) return null
64
-
65
- const stanzaId = tagNode.attrs?.id || stanza.attrs?.id
66
- if (!stanzaId) return null
67
-
68
- const tsRaw = tagNode.attrs?.ts_s || stanza.attrs?.t
69
- return { stanzaId, timestamp: tsRaw ? Number(tsRaw) : 0, reportingTag }
70
- }
71
-
72
- export const buildSpamReportMessageNodes = messages => messages.slice(0, 5).map(m => {
73
- const reportingTag = normalizeReportingTag(m.reportingTag)
74
- if (!reportingTag) throw new Error(`reportSpam: missing reportingTag for ${m.stanzaId}`)
75
- return {
76
- tag: 'message',
77
- attrs: {
78
- t: String(m.timestamp),
79
- id: m.stanzaId,
80
- reported_push_name: m.pushName || '',
81
- type: m.messageType || 'text',
82
- from: m.fromJid,
83
- },
84
- content: [
85
- {
86
- tag: 'raw',
87
- attrs: { local_message_type: '0', v: '2' },
88
- content: m.text ?? '',
89
- },
90
- {
91
- tag: 'reporting',
92
- attrs: {},
93
- content: [{
94
- tag: 'reporting_validation',
95
- attrs: {},
96
- content: [{
97
- tag: 'reporting_tag',
98
- attrs: { id: m.stanzaId, ts_s: String(m.timestamp) },
99
- content: reportingTag,
100
- }],
101
- }],
102
- },
103
- ],
104
- }
105
- })
106
-
107
- export const buildSpamReportIq = (jid, messages, spamFlow = SPAM_FLOWS.ACCOUNT_INFO_REPORT) => {
108
- if (!messages?.length) throw new Error('reportSpam: at least one message with reporting_tag required')
109
- return {
110
- tag: 'iq',
111
- attrs: { type: 'set', xmlns: 'spam', to: S_WHATSAPP_NET },
112
- content: [{
113
- tag: 'spam_list',
114
- attrs: { jid, spam_flow: spamFlow },
115
- content: buildSpamReportMessageNodes(messages),
116
- }],
117
- }
118
- }
119
-
120
- export const parseSpamReportResponse = result => {
121
- if (!result) return { success: false, error: 'empty response' }
122
- if (result.attrs?.type === 'error') {
123
- const errNode = getBinaryNodeChild(result, 'error')
124
- const code = errNode?.attrs?.code
125
- const text = errNode?.content?.toString?.() || errNode?.attrs?.text
126
- return { success: false, error: `spam IQ error${code ? ` ${code}` : ''}${text ? `: ${text}` : ''}` }
127
- }
128
- const reportNode = getBinaryNodeChild(result, 'report')
129
- const reportId = reportNode?.attrs?.id || getBinaryNodeChildString(reportNode, 'id')
130
- if (reportId) return { success: true, reportId: String(reportId) }
131
- if (result.attrs?.type === 'result') return { success: true }
132
- return { success: false, error: 'unexpected spam IQ response' }
133
- }
134
-
135
- export const buildSimpleReportNode = (jid, messageKeys) => ({
136
- tag: 'report',
137
- attrs: { jid, type: 'spam' },
138
- content: (messageKeys || []).map(key => ({
139
- tag: 'message',
140
- attrs: { id: key.id || key, from: key.fromMe ? undefined : jid },
141
- })),
1
+ import { getBinaryNodeChild, getBinaryNodeChildString, isLidUser, isHostedLidUser, S_WHATSAPP_NET } from '../WABinary/index.js'
2
+ import { extractMessageContent } from './messages.js'
3
+
4
+ export const SPAM_FLOWS = {
5
+ ACCOUNT_INFO_REPORT: 'account_info_report',
6
+ ACCOUNT_INFO_BLOCK: 'account_info_block',
7
+ ONE_TO_ONE_SPAM_BANNER_REPORT: '1_1_spam_banner_report',
8
+ MESSAGE_MENU: 'message_menu',
9
+ OVERFLOW_MENU_REPORT: 'overflow_menu_report',
10
+ }
11
+
12
+ const toBuffer = data => {
13
+ if (!data) return null
14
+ if (Buffer.isBuffer(data)) return data
15
+ if (data instanceof Uint8Array) return Buffer.from(data)
16
+ if (typeof data === 'string') return Buffer.from(data, 'base64')
17
+ return null
18
+ }
19
+
20
+ export const resolveReportJid = async (jid, lidMapping) => {
21
+ if (!jid) throw new Error('reportSpam: jid required')
22
+ if (isLidUser(jid) || isHostedLidUser(jid)) return jid
23
+ if (lidMapping?.getLIDForPN) {
24
+ const lid = await lidMapping.getLIDForPN(jid)
25
+ if (lid) return lid
26
+ }
27
+ return jid
28
+ }
29
+
30
+ export const normalizeReportingTag = tag => {
31
+ const buf = toBuffer(tag)
32
+ return buf?.length ? buf : null
33
+ }
34
+
35
+ export const extractMessageText = msg => {
36
+ if (!msg?.message) return ''
37
+ const content = extractMessageContent(msg.message)
38
+ if (!content) return ''
39
+ if (typeof content.conversation === 'string') return content.conversation
40
+ if (content.extendedTextMessage?.text) return content.extendedTextMessage.text
41
+ if (content.imageMessage?.caption) return content.imageMessage.caption
42
+ if (content.videoMessage?.caption) return content.videoMessage.caption
43
+ return ''
44
+ }
45
+
46
+ export const extractReportingInfoFromStanza = stanza => {
47
+ const reporting = getBinaryNodeChild(stanza, 'reporting')
48
+ if (!reporting) return null
49
+ const tokenNode = getBinaryNodeChild(reporting, 'reporting_token')
50
+ const tagNode = getBinaryNodeChild(reporting, 'reporting_tag') ||
51
+ getBinaryNodeChild(getBinaryNodeChild(reporting, 'reporting_validation'), 'reporting_tag')
52
+ if (!tagNode?.content) return null
53
+ const raw = tagNode.content
54
+ const reportingTag = toBuffer(raw instanceof Uint8Array || Buffer.isBuffer(raw) ? raw : typeof raw === 'string' ? raw : null)
55
+ if (!reportingTag?.length) return null
56
+ const stanzaId = tagNode.attrs?.id || stanza.attrs?.id
57
+ if (!stanzaId) return null
58
+ const tsRaw = tagNode.attrs?.ts_s || stanza.attrs?.t
59
+ const reportingToken = tokenNode?.content ? toBuffer(tokenNode.content) : null
60
+ const tokenVersion = tokenNode?.attrs?.v ? parseInt(tokenNode.attrs.v) : null
61
+ const validationPolicy = reporting.attrs?.validation_policy || null
62
+ return { stanzaId, timestamp: tsRaw ? Number(tsRaw) : 0, reportingTag, reportingToken, tokenVersion, validationPolicy }
63
+ }
64
+
65
+ export const buildSpamReportMessageNodes = messages => messages.slice(0, 5).map(m => {
66
+ const reportingTag = normalizeReportingTag(m.reportingTag)
67
+ if (!reportingTag) throw new Error(`reportSpam: missing reportingTag for ${m.stanzaId}`)
68
+ return {
69
+ tag: 'message',
70
+ attrs: {
71
+ t: String(m.timestamp),
72
+ id: m.stanzaId,
73
+ reported_push_name: m.pushName || '',
74
+ type: m.messageType || 'text',
75
+ from: m.fromJid,
76
+ },
77
+ content: [
78
+ {
79
+ tag: 'raw',
80
+ attrs: { local_message_type: '0', v: '2' },
81
+ content: m.text ?? '',
82
+ },
83
+ {
84
+ tag: 'reporting',
85
+ attrs: {},
86
+ content: [{
87
+ tag: 'reporting_validation',
88
+ attrs: {},
89
+ content: [{
90
+ tag: 'reporting_tag',
91
+ attrs: { id: m.stanzaId, ts_s: String(m.timestamp) },
92
+ content: reportingTag,
93
+ }],
94
+ }],
95
+ },
96
+ ],
97
+ }
98
+ })
99
+
100
+ export const buildSpamReportIq = (jid, messages, spamFlow = SPAM_FLOWS.ACCOUNT_INFO_REPORT) => {
101
+ if (!messages?.length) throw new Error('reportSpam: at least one message with reporting_tag required')
102
+ return {
103
+ tag: 'iq',
104
+ attrs: { type: 'set', xmlns: 'spam', to: S_WHATSAPP_NET },
105
+ content: [{
106
+ tag: 'spam_list',
107
+ attrs: { jid, spam_flow: spamFlow },
108
+ content: buildSpamReportMessageNodes(messages),
109
+ }],
110
+ }
111
+ }
112
+
113
+ export const parseSpamReportResponse = result => {
114
+ if (!result) return { success: false, error: 'empty response' }
115
+ if (result.attrs?.type === 'error') {
116
+ const errNode = getBinaryNodeChild(result, 'error')
117
+ const code = errNode?.attrs?.code
118
+ const text = errNode?.content?.toString?.() || errNode?.attrs?.text
119
+ return { success: false, error: `spam IQ error${code ? ` ${code}` : ''}${text ? `: ${text}` : ''}` }
120
+ }
121
+ const reportNode = getBinaryNodeChild(result, 'report')
122
+ const reportId = reportNode?.attrs?.id || getBinaryNodeChildString(reportNode, 'id')
123
+ if (reportId) return { success: true, reportId: String(reportId) }
124
+ if (result.attrs?.type === 'result') return { success: true }
125
+ return { success: false, error: 'unexpected spam IQ response' }
126
+ }
127
+
128
+ export const buildSimpleReportNode = (jid, messageKeys) => ({
129
+ tag: 'report',
130
+ attrs: { jid, type: 'spam' },
131
+ content: (messageKeys || []).map(key => ({
132
+ tag: 'message',
133
+ attrs: { id: key.id || key, from: key.fromMe ? undefined : jid },
134
+ })),
142
135
  })