@zervonex/baileys 3.0.8 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -18
- package/README.md +172 -13
- package/WAProto/WAProtoCompile.js +20 -7
- package/lib/Builders/AIRich.js +76 -45
- package/lib/Builders/base.js +14 -3
- package/lib/Builders/builders.js +140 -48
- package/lib/Defaults/index.js +4 -0
- package/lib/Socket/spam-report.js +104 -25
- package/lib/Socket/zervonex-handler.js +9 -14
- package/lib/Store/make-cache-manager-store.js +2 -3
- package/lib/Store/make-in-memory-store.js +1 -0
- package/lib/Utils/auth-utils.js +19 -2
- package/lib/Utils/bot-capabilities.js +1 -0
- package/lib/Utils/crypto.js +2 -1
- package/lib/Utils/extras.js +1 -1
- package/lib/Utils/generics.js +1 -1
- package/lib/Utils/index.js +1 -0
- package/lib/Utils/message-composer.js +8 -5
- package/lib/Utils/messages.js +9 -3
- package/lib/Utils/native-flow.js +26 -0
- package/lib/Utils/spam-report-utils.js +100 -69
- package/lib/Utils/use-keyv-auth-state.js +169 -0
- package/lib/Utils/use-multi-file-auth-state.js +3 -194
- package/lib/index.js +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from 'crypto'
|
|
2
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'
|
|
3
|
+
import { buildDispatch, buildPlugins, norm, handleGroupStatus, handleStatusMention, handlePollResult, handleNativeFlow, PRIMITIVE_SHORTCUTS, HANDLER_SHORTCUTS, attachAIRich } from '../Builders/index.js'
|
|
4
4
|
import { makeUsernameSocket } from './username.js'
|
|
5
5
|
|
|
6
6
|
export const makeMessageBuilderSocket = (config) => {
|
|
@@ -23,24 +23,19 @@ export const makeMessageBuilderSocket = (config) => {
|
|
|
23
23
|
|
|
24
24
|
result.sendMessage = async (jid, content, options = {}) => {
|
|
25
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)
|
|
26
|
+
const aiData = content.aiRich ?? content.airich ?? content.richResponse ?? content.AIRich ?? content.richMessage
|
|
27
|
+
if (aiData) return result.sendRichFromObject(jid, Array.isArray(aiData) ? { ...content, parts: aiData } : { ...content, ...aiData }, options)
|
|
28
28
|
|
|
29
29
|
// dispatch to registered builder handlers
|
|
30
30
|
for (const [key, handler] of Object.entries(DISPATCH)) {
|
|
31
31
|
if (key in content) return handler(result, jid, content, options, config)
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
}
|
|
33
|
+
// templateButtons check
|
|
34
|
+
if (content.templateButtons) return result.sendMessage(jid, { text: content.text ?? content.caption ?? '', footer: content.footer, ...(content.image ? { image: content.image } : {}), nativeFlow: content.templateButtons.map(b => b.url ? { text: b.text, url: b.url } : b.call ? { text: b.text, call: b.call } : { text: b.text, id: b.id }) }, options)
|
|
35
|
+
// carousel && card message check
|
|
36
|
+
if (content.cards && !content.carouselMessage) return result.sendMessage(jid, { carouselMessage: { caption: content.text ?? content.caption ?? '', footer: content.footer, cards: content.cards } }, options)
|
|
37
|
+
// bare nativeFlow array/object
|
|
38
|
+
if (content.nativeFlow && !content.interactiveMessage) return handleNativeFlow(result, jid, content, options)
|
|
44
39
|
|
|
45
40
|
const { ai, aiLabel: contentAiLabel, ...cleanContent } = content
|
|
46
41
|
const isAi = !!(ai ?? contentAiLabel ?? options.aiLabel ?? options.ai ?? config.aiLabel)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as cacheManager from 'cache-manager';
|
|
2
2
|
import * as WAProto from '../../WAProto/index.js';
|
|
3
|
-
import { BufferJSON, initAuthCreds } from '../Utils/index.js';
|
|
4
|
-
import logger from '../Utils/logger.js';
|
|
3
|
+
import { BufferJSON, initAuthCreds, logger } from '../Utils/index.js';
|
|
5
4
|
|
|
6
5
|
const makeCacheManagerAuthState = async (store, sessionKey) => {
|
|
7
6
|
const defaultKey = (file) => `${sessionKey}:${file}`;
|
|
@@ -40,7 +39,7 @@ const makeCacheManagerAuthState = async (store, sessionKey) => {
|
|
|
40
39
|
try {
|
|
41
40
|
const result = await databaseConn.store.keys(`${sessionKey}*`);
|
|
42
41
|
await Promise.all(result.map(async (key) => await databaseConn.del(key)));
|
|
43
|
-
} catch {}
|
|
42
|
+
} catch { }
|
|
44
43
|
};
|
|
45
44
|
|
|
46
45
|
const creds = (await readData('creds')) || initAuthCreds();
|
|
@@ -43,6 +43,7 @@ const makeInMemoryStore = (config) => {
|
|
|
43
43
|
const contactsUpsert = (newContacts) => {
|
|
44
44
|
const oldContacts = new Set(Object.keys(contacts))
|
|
45
45
|
for (const contact of newContacts) {
|
|
46
|
+
if (!contact || typeof contact.id !== 'string') continue
|
|
46
47
|
oldContacts.delete(contact.id)
|
|
47
48
|
contacts[contact.id] = Object.assign(contacts[contact.id] || {}, contact)
|
|
48
49
|
}
|
package/lib/Utils/auth-utils.js
CHANGED
|
@@ -6,8 +6,9 @@ import PQueue from 'p-queue'
|
|
|
6
6
|
import { v4 } from 'uuid'
|
|
7
7
|
import { DEFAULT_CACHE_TTLS } from '../Defaults/index.js'
|
|
8
8
|
import { Curve, signedKeyPair } from './crypto.js'
|
|
9
|
-
import { delay, generateRegistrationId } from './generics.js'
|
|
9
|
+
import { delay, generateRegistrationId, BufferJSON } from './generics.js'
|
|
10
10
|
import { PreKeyManager } from './pre-key-manager.js'
|
|
11
|
+
import { proto } from '../../WAProto/index.js'
|
|
11
12
|
|
|
12
13
|
export function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
13
14
|
const cache = _cache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, useClones: false, deleteOnExpire: true })
|
|
@@ -139,4 +140,20 @@ export const initAuthCreds = () => {
|
|
|
139
140
|
routingInfo: undefined,
|
|
140
141
|
additionalData: undefined
|
|
141
142
|
}
|
|
142
|
-
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
export const bootstrapCreds = raw => raw ?? initAuthCreds()
|
|
147
|
+
export const patchAppStateKey = (type, v) => type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
|
|
148
|
+
export const ser = v => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.replacer))
|
|
149
|
+
export const deser = raw => {
|
|
150
|
+
if (raw == null) return null
|
|
151
|
+
let p = typeof raw === 'string'
|
|
152
|
+
? (() => { try { return JSON.parse(raw) } catch { return raw } })()
|
|
153
|
+
: raw
|
|
154
|
+
let depth = 0
|
|
155
|
+
while (depth++ < 3 && p && typeof p === 'object' && !Array.isArray(p) && Object.keys(p).length === 1 && 'value' in p) p = p.value
|
|
156
|
+
if (p == null) return null
|
|
157
|
+
if (typeof p === 'object' && '__checksum' in p && 'data' in p) p = p.data
|
|
158
|
+
return JSON.parse(JSON.stringify(p), BufferJSON.reviver)
|
|
159
|
+
}
|
|
@@ -2,6 +2,7 @@ import { proto } from '../../WAProto/index.js'
|
|
|
2
2
|
import { BOT_RENDERING_CONFIG_METADATA } from '../Defaults/index.js'
|
|
3
3
|
|
|
4
4
|
export const BotCapabilityType = proto.BotCapabilityMetadata.BotCapabilityType
|
|
5
|
+
export const RichSubMessageType = proto.AIRichResponseSubMessageType
|
|
5
6
|
|
|
6
7
|
const capabilityNames = new Set(Object.keys(BotCapabilityType).filter(n => Number.isNaN(Number(n))))
|
|
7
8
|
|
package/lib/Utils/crypto.js
CHANGED
|
@@ -28,7 +28,7 @@ export const aesEncryptGCM = (plaintext, key, iv, additionalData) => {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export const aesDecryptGCM = (ciphertext, key, iv, additionalData) => {
|
|
31
|
-
const decipher = createDecipheriv('aes-256-gcm', key, iv)
|
|
31
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: GCM_TAG_LENGTH })
|
|
32
32
|
decipher.setAAD(additionalData)
|
|
33
33
|
decipher.setAuthTag(ciphertext.subarray(-GCM_TAG_LENGTH))
|
|
34
34
|
return Buffer.concat([decipher.update(ciphertext.subarray(0, -GCM_TAG_LENGTH)), decipher.final()])
|
|
@@ -37,6 +37,7 @@ export const aesDecryptGCM = (ciphertext, key, iv, additionalData) => {
|
|
|
37
37
|
export const aesEncryptCTR = (plaintext, key, iv) => { const c = createCipheriv('aes-256-ctr', key, iv); return Buffer.concat([c.update(plaintext), c.final()]) }
|
|
38
38
|
export const aesDecryptCTR = (ciphertext, key, iv) => { const d = createDecipheriv('aes-256-ctr', key, iv); return Buffer.concat([d.update(ciphertext), d.final()]) }
|
|
39
39
|
|
|
40
|
+
// aes-256-cbc is required by the WhatsApp media encryption spec (CBC + HMAC-SHA256)
|
|
40
41
|
export const aesDecryptWithIV = (buffer, key, IV) => { const d = createDecipheriv('aes-256-cbc', key, IV); return Buffer.concat([d.update(buffer), d.final()]) }
|
|
41
42
|
export const aesDecrypt = (buffer, key) => aesDecryptWithIV(buffer.subarray(16), key, buffer.subarray(0, 16))
|
|
42
43
|
|
package/lib/Utils/extras.js
CHANGED
|
@@ -25,7 +25,7 @@ export const decryptWithRevealKey = (encPayload, encIv, revealKey) => {
|
|
|
25
25
|
const key = Buffer.from(revealKey)
|
|
26
26
|
const payload = Buffer.from(encPayload)
|
|
27
27
|
if (payload.length < SCHEDULED_MSG_TAG_BYTES) throw new TypeError('encrypted payload is too short to carry an auth tag')
|
|
28
|
-
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(encIv))
|
|
28
|
+
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(encIv), { authTagLength: SCHEDULED_MSG_TAG_BYTES })
|
|
29
29
|
decipher.setAuthTag(payload.subarray(payload.length - SCHEDULED_MSG_TAG_BYTES))
|
|
30
30
|
return Buffer.concat([decipher.update(payload.subarray(0, payload.length - SCHEDULED_MSG_TAG_BYTES)), decipher.final()])
|
|
31
31
|
}
|
package/lib/Utils/generics.js
CHANGED
|
@@ -44,7 +44,7 @@ export const generateRegistrationId = () => Uint16Array.from(randomBytes(2))[0]
|
|
|
44
44
|
|
|
45
45
|
export const encodeBigEndian = (e, t = 4) => { let r = e; const a = new Uint8Array(t); for (let i = t - 1; i >= 0; i--) { a[i] = 255 & r; r >>>= 8 }; return a }
|
|
46
46
|
|
|
47
|
-
export const toNumber = t => typeof t === 'object' && t
|
|
47
|
+
export const toNumber = t => { if (typeof t === 'bigint') return Number(t); if (typeof t === 'object' && t) return 'toNumber' in t ? t.toNumber() : (t.low ?? 0); return t || 0 }
|
|
48
48
|
export const unixTimestampSeconds = (date = new Date()) => Math.floor(date.getTime() / 1000)
|
|
49
49
|
|
|
50
50
|
export const debouncedTimeout = (intervalMs = 1000, task) => {
|
package/lib/Utils/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export * from './lt-hash.js'
|
|
|
14
14
|
export * from './auth-utils.js'
|
|
15
15
|
export * from './baileys-event-stream.js'
|
|
16
16
|
export * from './use-multi-file-auth-state.js'
|
|
17
|
+
export * from './use-keyv-auth-state.js'
|
|
17
18
|
export * from './link-preview.js'
|
|
18
19
|
export * from './event-buffer.js'
|
|
19
20
|
export * from './process-message.js'
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { generateMessageIDV2, tokenizeCode } from './generics.js'
|
|
2
|
+
import { RichSubMessageType } from './bot-capabilities.js'
|
|
3
|
+
import { proto } from '../../WAProto/index.js'
|
|
2
4
|
|
|
3
5
|
const buildRichContextInfo = (quoted, options = {}) => {
|
|
4
6
|
const ctx = { forwardingScore: 1, isForwarded: true, forwardedAiBotMessageInfo: { botJid: options.botJid || '867051314767696@bot' }, forwardOrigin: 4, ...(options.mentions ? { mentionedJid: options.mentions } : {}) }
|
|
@@ -7,14 +9,14 @@ const buildRichContextInfo = (quoted, options = {}) => {
|
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
const buildBotForwardedMessage = (submessages, contextInfo, unifiedResponse) => {
|
|
10
|
-
const rich = { messageType:
|
|
12
|
+
const rich = { messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD, submessages, contextInfo }
|
|
11
13
|
if (unifiedResponse) rich.unifiedResponse = unifiedResponse
|
|
12
14
|
return { botForwardedMessage: { message: { richResponseMessage: rich } } }
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const wrap = (sub, quoted, options) => ({ message: buildBotForwardedMessage(sub, buildRichContextInfo(quoted, options)), messageId: generateMessageIDV2() })
|
|
16
|
-
const text = msg => ({ messageType:
|
|
17
|
-
const table = (title, rows) => ({ messageType:
|
|
18
|
+
const text = msg => ({ messageType: RichSubMessageType.AI_RICH_RESPONSE_TEXT, messageText: msg })
|
|
19
|
+
const table = (title, rows) => ({ messageType: RichSubMessageType.AI_RICH_RESPONSE_TABLE, tableMetadata: { title, rows } })
|
|
18
20
|
|
|
19
21
|
export const generateTableContent = (title, headers, rows, quoted, options = {}) => {
|
|
20
22
|
const { footer, headerText } = options
|
|
@@ -30,7 +32,8 @@ export const generateListContent = (title, items, quoted, options = {}) => {
|
|
|
30
32
|
|
|
31
33
|
export const generateCodeBlockContent = (code, quoted, options = {}) => {
|
|
32
34
|
const { title, footer, language = 'javascript' } = options
|
|
33
|
-
const
|
|
35
|
+
const { codeBlocks } = tokenizeCode(code, language)
|
|
36
|
+
const sub = [...(title ? [text(title)] : []), { messageType: RichSubMessageType.AI_RICH_RESPONSE_CODE, codeMetadata: { codeLanguage: language, codeBlocks } }, ...(footer ? [text(footer)] : [])]
|
|
34
37
|
return wrap(sub, quoted, options)
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -45,7 +48,7 @@ export const generateLatexContent = (quoted, options = {}) => {
|
|
|
45
48
|
if (imageTrailingPadding !== undefined) e.imageTrailingPadding = imageTrailingPadding
|
|
46
49
|
return e
|
|
47
50
|
})
|
|
48
|
-
const sub = [...(headerText ? [text(headerText)] : []), { messageType:
|
|
51
|
+
const sub = [...(headerText ? [text(headerText)] : []), { messageType: RichSubMessageType.AI_RICH_RESPONSE_LATEX, latexMetadata: { text: txt || '', expressions: latexExpressions } }, ...(footer ? [text(footer)] : [])]
|
|
49
52
|
return wrap(sub, quoted, options)
|
|
50
53
|
}
|
|
51
54
|
|
package/lib/Utils/messages.js
CHANGED
|
@@ -16,6 +16,7 @@ import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT
|
|
|
16
16
|
import { WAMessageStatus, WAProto } from '../Types/index.js'
|
|
17
17
|
import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js'
|
|
18
18
|
import { sha256 } from './crypto.js'
|
|
19
|
+
import { resolveNativeFlowButtons } from './native-flow.js'
|
|
19
20
|
import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js'
|
|
20
21
|
import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getStream, toBuffer, normalizeToBuffer } from './messages-media.js'
|
|
21
22
|
import { shouldIncludeReportingToken } from './reporting-utils.js'
|
|
@@ -546,9 +547,9 @@ const applyContextInfoAndMentions = async (interactive, msg) => {
|
|
|
546
547
|
|
|
547
548
|
const OVERLAY_HANDLERS = {
|
|
548
549
|
buttons: async (m, msg) => {
|
|
549
|
-
const hasNativeFlow = msg.buttons.some(b => b.nativeFlowInfo || b.name || b.buttonParamsJson)
|
|
550
|
+
const hasNativeFlow = msg.buttons.some(b => b.nativeFlowInfo || b.name || b.buttonParamsJson || b.url || b.call || b.sections)
|
|
550
551
|
if (hasNativeFlow) {
|
|
551
|
-
const interactive = { body: { text: msg.text || msg.caption || msg.contentText || '' }, footer: { text: msg.footer || msg.footerText || '' }, nativeFlowMessage: { buttons: msg.buttons
|
|
552
|
+
const interactive = { body: { text: msg.text || msg.caption || msg.contentText || '' }, footer: { text: msg.footer || msg.footerText || '' }, nativeFlowMessage: { buttons: resolveNativeFlowButtons(msg.buttons) } }
|
|
552
553
|
if (msg.title) interactive.header = { title: msg.title, subtitle: msg.subtitle || '', hasMediaAttachment: msg.hasMediaAttachment || false }
|
|
553
554
|
if (Object.keys(m).length) { interactive.header ??= { title: msg.title || '', hasMediaAttachment: true }; Object.assign(interactive.header, m) }
|
|
554
555
|
await applyContextInfoAndMentions(interactive, msg)
|
|
@@ -575,6 +576,11 @@ const OVERLAY_HANDLERS = {
|
|
|
575
576
|
if (msg.mentions?.length) ht.contextInfo = { ...(ht.contextInfo || {}), mentionedJid: msg.mentions }
|
|
576
577
|
return { templateMessage: { fourRowTemplate: ht, hydratedTemplate: ht } }
|
|
577
578
|
},
|
|
579
|
+
audioFooter: async (m, msg, opts) => {
|
|
580
|
+
const audio = await prepareWAMessageMedia({ audio: msg.audioFooter }, { upload: opts.upload ?? opts.waUploadToServer })
|
|
581
|
+
const buttons = resolveNativeFlowButtons(Array.isArray(msg.nativeFlow) ? msg.nativeFlow : [msg.nativeFlow])
|
|
582
|
+
return { interactiveMessage: { body: { text: msg.text || '' }, footer: { text: msg.footer || '' }, header: { hasMediaAttachment: false }, nativeFlowMessage: { buttons }, footerColumns: [{ audioMessage: audio.audioMessage }] } }
|
|
583
|
+
},
|
|
578
584
|
interactiveButtons: async (m, msg) => {
|
|
579
585
|
const interactive = { nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({ buttons: msg.interactiveButtons }) }
|
|
580
586
|
if ('text' in msg) { interactive.body = { text: msg.text }; interactive.header = { title: msg.title || '', subtitle: msg.subtitle || '', hasMediaAttachment: false } }
|
|
@@ -628,7 +634,7 @@ export const generateWAMessageContent = async (message, options = {}) => {
|
|
|
628
634
|
}
|
|
629
635
|
|
|
630
636
|
// Apply overlays: any overlay key present in message replaces m
|
|
631
|
-
for (const key of overlayKeys) { if (key in message && message[key]) { m = await OVERLAY_HANDLERS[key](m, message); break } }
|
|
637
|
+
for (const key of overlayKeys) { if (key in message && message[key]) { m = await OVERLAY_HANDLERS[key](m, message, options); break } }
|
|
632
638
|
|
|
633
639
|
// Merge contextInfo / mentions onto the primary content key — deep-merges externalAdReply to prevent thumbnail/field loss
|
|
634
640
|
const finalKey = Object.keys(m)[0]
|
package/lib/Utils/native-flow.js
CHANGED
|
@@ -49,6 +49,32 @@ export const buildWhatsAppFlowButton = ({ flowId, flowToken, cta = 'Open form',
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export const resolveNativeFlowButtons = (rawButtons) => {
|
|
53
|
+
const buttons = Array.isArray(rawButtons) ? rawButtons : [rawButtons]
|
|
54
|
+
return buttons.map(b => {
|
|
55
|
+
if (b.name && b.buttonParamsJson) return b // already formatted
|
|
56
|
+
const text = b.text ?? b.display_text ?? ''
|
|
57
|
+
if (b.sections) return { name: 'single_select', buttonParamsJson: JSON.stringify({ title: text, sections: b.sections }) }
|
|
58
|
+
if (b.url && b.useWebview) return { name: 'cta_open_webview', buttonParamsJson: JSON.stringify({ display_text: text, url: b.url }) }
|
|
59
|
+
if (b.url) return { name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: text, url: b.url, merchant_url: b.merchantUrl ?? b.url }) }
|
|
60
|
+
if (b.call) return { name: 'cta_call', buttonParamsJson: JSON.stringify({ display_text: text, phone_number: b.call }) }
|
|
61
|
+
if (b.copy) return { name: 'cta_copy', buttonParamsJson: JSON.stringify({ display_text: text, copy_code: b.copy }) }
|
|
62
|
+
if (b.flow_id ?? b.flowId) return { name: 'flow', buttonParamsJson: JSON.stringify({ display_text: text, flow_id: b.flow_id ?? b.flowId, flow_token: b.flow_token ?? b.flowToken ?? crypto.randomUUID() }) }
|
|
63
|
+
if (b.remind) return { name: 'cta_reminder', buttonParamsJson: JSON.stringify({ display_text: text, ...b.remind }) }
|
|
64
|
+
if (b.cancelReminder) return { name: 'cta_cancel_reminder', buttonParamsJson: JSON.stringify({ display_text: text }) }
|
|
65
|
+
if (b.location) return { name: 'send_location', buttonParamsJson: JSON.stringify({ display_text: text }) }
|
|
66
|
+
if (b.address) return { name: 'address_message', buttonParamsJson: JSON.stringify({ display_text: text }) }
|
|
67
|
+
if (b.catalog) return { name: 'catalog_message', buttonParamsJson: JSON.stringify({ display_text: text, ...b.catalog }) }
|
|
68
|
+
if (b.reviewAndPay) return { name: 'review_and_pay', buttonParamsJson: JSON.stringify({ display_text: text, ...b.reviewAndPay }) }
|
|
69
|
+
if (b.reviewOrder) return { name: 'review_order', buttonParamsJson: JSON.stringify({ display_text: text, ...b.reviewOrder }) }
|
|
70
|
+
if (b.orderDetails) return { name: 'order_details', buttonParamsJson: JSON.stringify({ display_text: text, ...b.orderDetails }) }
|
|
71
|
+
if (b.paymentStatus) return { name: 'payment_status', buttonParamsJson: JSON.stringify({ display_text: text, ...b.paymentStatus }) }
|
|
72
|
+
if (b.transactionDetails) return { name: 'transaction_details', buttonParamsJson: JSON.stringify({ display_text: text, ...b.transactionDetails }) }
|
|
73
|
+
// default: quick_reply
|
|
74
|
+
return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: text, id: b.id ?? text }) }
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
export const makeWhatsAppFlowButton = flow => ({
|
|
53
79
|
name: flow?.nativeFlowName || 'galaxy_message',
|
|
54
80
|
buttonParamsJson: JSON.stringify(buildWhatsAppFlowButton(flow)),
|
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
import { getBinaryNodeChild, getBinaryNodeChildString, isLidUser, isHostedLidUser, S_WHATSAPP_NET } from '../WABinary/index.js'
|
|
1
|
+
import { getBinaryNodeChild, getBinaryNodeChildString, isLidUser, isHostedLidUser, isJidGroup, S_WHATSAPP_NET } from '../WABinary/index.js'
|
|
2
2
|
import { extractMessageContent } from './messages.js'
|
|
3
3
|
|
|
4
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
// Controls how WA routes the report internally — pass via options.spamFlow to override auto-selection.
|
|
7
|
+
// ACCOUNT_INFO_REPORT → tag-based 1:1 contact report (default)
|
|
8
|
+
// ACCOUNT_INFO_BLOCK → tag-based contact report signalling intent to block
|
|
9
|
+
// GROUP_INFO_REPORT → tag-based group report (auto-selected for group JIDs)
|
|
10
|
+
// CONTACT_INFO_REPORT → legacy lightweight report, no reporting_tag needed
|
|
11
|
+
// ONE_TO_ONE_SPAM_BANNER_REPORT → from the in-chat spam banner
|
|
12
|
+
// MESSAGE_MENU → from long-press message context menu
|
|
13
|
+
// OVERFLOW_MENU_REPORT → from the chat overflow (⋮) menu
|
|
4
14
|
export const SPAM_FLOWS = {
|
|
5
15
|
ACCOUNT_INFO_REPORT: 'account_info_report',
|
|
6
16
|
ACCOUNT_INFO_BLOCK: 'account_info_block',
|
|
17
|
+
GROUP_INFO_REPORT: 'group_info_report',
|
|
18
|
+
CONTACT_INFO_REPORT: 'contact_info_report',
|
|
7
19
|
ONE_TO_ONE_SPAM_BANNER_REPORT: '1_1_spam_banner_report',
|
|
8
20
|
MESSAGE_MENU: 'message_menu',
|
|
9
21
|
OVERFLOW_MENU_REPORT: 'overflow_menu_report',
|
|
10
22
|
}
|
|
11
23
|
|
|
24
|
+
// Numeric reason codes for the w:report IQ endpoint — used by reportUser() and reportMessage().
|
|
25
|
+
export const REPORT_REASON_CODES = { spam: 1, harassment: 2, impersonation: 3, scam: 4, other: 5 }
|
|
26
|
+
|
|
27
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
28
|
+
|
|
12
29
|
const toBuffer = data => {
|
|
13
30
|
if (!data) return null
|
|
14
31
|
if (Buffer.isBuffer(data)) return data
|
|
@@ -17,32 +34,44 @@ const toBuffer = data => {
|
|
|
17
34
|
return null
|
|
18
35
|
}
|
|
19
36
|
|
|
37
|
+
// ─── JID resolution ───────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
// Resolves a PN JID to its LID equivalent for tag-based spam IQs.
|
|
40
|
+
// Groups and already-LID/hosted-LID JIDs pass through unchanged.
|
|
20
41
|
export const resolveReportJid = async (jid, lidMapping) => {
|
|
21
|
-
if (!jid) throw new Error('
|
|
22
|
-
if (isLidUser(jid) || isHostedLidUser(jid)) return jid
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
if (!jid) throw new Error('resolveReportJid: jid required')
|
|
43
|
+
if (isJidGroup(jid) || isLidUser(jid) || isHostedLidUser(jid)) return jid
|
|
44
|
+
const lid = lidMapping?.getLIDForPN ? await lidMapping.getLIDForPN(jid) : null
|
|
45
|
+
return lid ?? jid
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Auto-detects JID type and returns the resolved JID + correct spam_flow for reportSpam().
|
|
49
|
+
// Returns { reportJid, spamFlow, isGroup }.
|
|
50
|
+
export const resolveReportContext = async (jid, lidMapping) => {
|
|
51
|
+
if (!jid) throw new Error('resolveReportContext: jid required')
|
|
52
|
+
if (isJidGroup(jid)) return { reportJid: jid, spamFlow: SPAM_FLOWS.GROUP_INFO_REPORT, isGroup: true }
|
|
53
|
+
return { reportJid: await resolveReportJid(jid, lidMapping), spamFlow: SPAM_FLOWS.ACCOUNT_INFO_REPORT, isGroup: false }
|
|
28
54
|
}
|
|
29
55
|
|
|
56
|
+
// ─── Tag / text extraction ────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
// Normalizes a raw reporting_tag to a Buffer — returns null if invalid or empty.
|
|
30
59
|
export const normalizeReportingTag = tag => {
|
|
31
60
|
const buf = toBuffer(tag)
|
|
32
61
|
return buf?.length ? buf : null
|
|
33
62
|
}
|
|
34
63
|
|
|
64
|
+
// Extracts plain text from a WAMessage across common message types.
|
|
35
65
|
export const extractMessageText = msg => {
|
|
36
66
|
if (!msg?.message) return ''
|
|
37
|
-
const
|
|
38
|
-
if (!
|
|
39
|
-
|
|
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 ''
|
|
67
|
+
const c = extractMessageContent(msg.message)
|
|
68
|
+
if (!c) return ''
|
|
69
|
+
return c.conversation ?? c.extendedTextMessage?.text ?? c.imageMessage?.caption ?? c.videoMessage?.caption ?? ''
|
|
44
70
|
}
|
|
45
71
|
|
|
72
|
+
// Extracts { stanzaId, timestamp, reportingTag, reportingToken, tokenVersion, validationPolicy }
|
|
73
|
+
// from a raw incoming binary stanza. Call inside messages.upsert and persist via reportingInfoStore
|
|
74
|
+
// so reportSpam() can use the tags later.
|
|
46
75
|
export const extractReportingInfoFromStanza = stanza => {
|
|
47
76
|
const reporting = getBinaryNodeChild(stanza, 'reporting')
|
|
48
77
|
if (!reporting) return null
|
|
@@ -56,60 +85,71 @@ export const extractReportingInfoFromStanza = stanza => {
|
|
|
56
85
|
const stanzaId = tagNode.attrs?.id || stanza.attrs?.id
|
|
57
86
|
if (!stanzaId) return null
|
|
58
87
|
const tsRaw = tagNode.attrs?.ts_s || stanza.attrs?.t
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
return {
|
|
89
|
+
stanzaId,
|
|
90
|
+
timestamp: tsRaw ? Number(tsRaw) : 0,
|
|
91
|
+
reportingTag,
|
|
92
|
+
reportingToken: tokenNode?.content ? toBuffer(tokenNode.content) : null,
|
|
93
|
+
tokenVersion: tokenNode?.attrs?.v ? parseInt(tokenNode.attrs.v) : null,
|
|
94
|
+
validationPolicy: reporting.attrs?.validation_policy || null,
|
|
95
|
+
}
|
|
63
96
|
}
|
|
64
97
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
attrs: { id: m.stanzaId, ts_s: String(m.timestamp) },
|
|
92
|
-
content: reportingTag,
|
|
93
|
-
}],
|
|
94
|
-
}],
|
|
95
|
-
},
|
|
96
|
-
],
|
|
98
|
+
// ─── Node builders ────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
// Builds <message> child nodes for a spam_list IQ.
|
|
101
|
+
// Skips messages missing a reportingTag (warn) — throws only if all are filtered (error).
|
|
102
|
+
export const buildSpamReportMessageNodes = (messages, logger) => {
|
|
103
|
+
const ts = () => Math.floor(Date.now() / 1000)
|
|
104
|
+
const nodes = messages.slice(0, 5).reduce((acc, m) => {
|
|
105
|
+
const reportingTag = normalizeReportingTag(m.reportingTag)
|
|
106
|
+
if (!reportingTag) {
|
|
107
|
+
logger?.warn?.({ stanzaId: m.stanzaId }, 'buildSpamReportMessageNodes: skipping — missing reportingTag')
|
|
108
|
+
return acc
|
|
109
|
+
}
|
|
110
|
+
const t = String(m.timestamp || ts())
|
|
111
|
+
acc.push({
|
|
112
|
+
tag: 'message',
|
|
113
|
+
attrs: { t, id: m.stanzaId, reported_push_name: m.pushName || '', type: m.messageType || 'text', from: m.fromJid },
|
|
114
|
+
content: [
|
|
115
|
+
{ tag: 'raw', attrs: { local_message_type: '0', v: '2' }, content: m.text != null ? String(m.text) : '' },
|
|
116
|
+
{ tag: 'reporting', attrs: {}, content: [{ tag: 'reporting_validation', attrs: {}, content: [{ tag: 'reporting_tag', attrs: { id: m.stanzaId, ts_s: t }, content: reportingTag }] }] },
|
|
117
|
+
],
|
|
118
|
+
})
|
|
119
|
+
return acc
|
|
120
|
+
}, [])
|
|
121
|
+
if (!nodes.length) {
|
|
122
|
+
logger?.error?.({}, 'buildSpamReportMessageNodes: all messages filtered — none had a valid reportingTag')
|
|
123
|
+
throw new Error('reportSpam: no valid messages with reportingTag after filtering')
|
|
97
124
|
}
|
|
98
|
-
|
|
125
|
+
return nodes
|
|
126
|
+
}
|
|
99
127
|
|
|
100
|
-
|
|
101
|
-
|
|
128
|
+
// Builds the full xmlns="spam" IQ stanza for tag-based reporting — used by reportSpam().
|
|
129
|
+
export const buildSpamReportIq = (jid, messages, spamFlow = SPAM_FLOWS.ACCOUNT_INFO_REPORT, logger) => {
|
|
130
|
+
if (!messages?.length) {
|
|
131
|
+
logger?.error?.({ jid, spamFlow }, 'buildSpamReportIq: called with empty messages array')
|
|
132
|
+
throw new Error('reportSpam: at least one message with reporting_tag required')
|
|
133
|
+
}
|
|
102
134
|
return {
|
|
103
135
|
tag: 'iq',
|
|
104
136
|
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
|
-
}],
|
|
137
|
+
content: [{ tag: 'spam_list', attrs: { jid, spam_flow: spamFlow }, content: buildSpamReportMessageNodes(messages, logger) }],
|
|
110
138
|
}
|
|
111
139
|
}
|
|
112
140
|
|
|
141
|
+
// Builds the simpler xmlns="spam" <report> node — used by reportContact() and reportGroup().
|
|
142
|
+
// Does NOT require reporting_tags, only message key ids or bare id strings.
|
|
143
|
+
export const buildSimpleReportNode = (jid, messageKeys) => ({
|
|
144
|
+
tag: 'report',
|
|
145
|
+
attrs: { jid, type: 'spam' },
|
|
146
|
+
content: (messageKeys || []).map(key => ({ tag: 'message', attrs: { id: key.id || key, ...(key.fromMe ? {} : { from: jid }) } })),
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// ─── Response parser ──────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
// Parses the IQ result node from any spam report query.
|
|
152
|
+
// Returns { success: true, reportId? } on success or { success: false, error } on failure.
|
|
113
153
|
export const parseSpamReportResponse = result => {
|
|
114
154
|
if (!result) return { success: false, error: 'empty response' }
|
|
115
155
|
if (result.attrs?.type === 'error') {
|
|
@@ -123,13 +163,4 @@ export const parseSpamReportResponse = result => {
|
|
|
123
163
|
if (reportId) return { success: true, reportId: String(reportId) }
|
|
124
164
|
if (result.attrs?.type === 'result') return { success: true }
|
|
125
165
|
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
|
-
})),
|
|
135
|
-
})
|
|
166
|
+
}
|