@zervonex/baileys 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +3206 -3
- package/WAProto/WAProtoCompile.js +291 -0
- package/WAProto/fetcher.js +69 -0
- package/WAProto/index.js +88 -0
- package/WAProto/parser.js +313 -0
- package/engine-requirements.js +10 -0
- package/lib/Builders/AIRich.js +643 -0
- package/lib/Builders/base.js +130 -0
- package/lib/Builders/builders.js +449 -0
- package/lib/Builders/index.js +3 -0
- package/lib/Defaults/index.js +225 -0
- package/lib/Defaults/phonenumber-mcc.json +223 -0
- package/lib/Signal/index.js +2 -0
- package/lib/Signal/libsignal.js +523 -0
- package/lib/Signal/lid-mapping.js +283 -0
- package/lib/Socket/Client/index.js +2 -0
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.js +84 -0
- package/lib/Socket/business.js +152 -0
- package/lib/Socket/chats.js +855 -0
- package/lib/Socket/communities.js +147 -0
- package/lib/Socket/groups.js +294 -0
- package/lib/Socket/index.js +15 -0
- package/lib/Socket/messages-recv.js +981 -0
- package/lib/Socket/messages-send.js +708 -0
- package/lib/Socket/mex.js +34 -0
- package/lib/Socket/newsletter.js +781 -0
- package/lib/Socket/registration.js +155 -0
- package/lib/Socket/socket.js +797 -0
- package/lib/Socket/spam-report.js +65 -0
- package/lib/Socket/username.js +82 -0
- package/lib/Socket/zervonex-handler.js +55 -0
- package/lib/Store/index.js +6 -0
- package/lib/Store/make-cache-manager-store.js +81 -0
- package/lib/Store/make-in-memory-store.js +425 -0
- package/lib/Store/make-ordered-dictionary.js +82 -0
- package/lib/Store/object-repository.js +31 -0
- package/lib/Store/reporting-info-store.js +38 -0
- package/lib/Types/Auth.js +2 -0
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Call.js +2 -0
- package/lib/Types/Chat.js +8 -0
- package/lib/Types/Contact.js +2 -0
- package/lib/Types/Events.js +2 -0
- package/lib/Types/GroupMetadata.js +2 -0
- package/lib/Types/Label.js +25 -0
- package/lib/Types/LabelAssociation.js +7 -0
- package/lib/Types/Message.js +15 -0
- package/lib/Types/Newsletter.js +100 -0
- package/lib/Types/Product.js +2 -0
- package/lib/Types/Signal.js +2 -0
- package/lib/Types/Socket.js +14 -0
- package/lib/Types/State.js +26 -0
- package/lib/Types/USync.js +2 -0
- package/lib/Types/index.js +30 -0
- package/lib/Utils/auth-utils.js +142 -0
- package/lib/Utils/baileys-event-stream.js +33 -0
- package/lib/Utils/bot-capabilities.js +40 -0
- package/lib/Utils/bot-planning-replay.js +44 -0
- package/lib/Utils/browser-utils.js +26 -0
- package/lib/Utils/business.js +75 -0
- package/lib/Utils/chat-utils.js +317 -0
- package/lib/Utils/companion-reg-client-utils.js +20 -0
- package/lib/Utils/consumer-application.js +38 -0
- package/lib/Utils/crypto.js +52 -0
- package/lib/Utils/decode-wa-message.js +197 -0
- package/lib/Utils/event-buffer.js +273 -0
- package/lib/Utils/extras.js +186 -0
- package/lib/Utils/generics.js +195 -0
- package/lib/Utils/history.js +51 -0
- package/lib/Utils/identity-change-handler.js +19 -0
- package/lib/Utils/index.js +38 -0
- package/lib/Utils/link-preview.js +51 -0
- package/lib/Utils/logger.js +2 -0
- package/lib/Utils/lt-hash.js +3 -0
- package/lib/Utils/make-mutex.js +16 -0
- package/lib/Utils/message-composer.js +59 -0
- package/lib/Utils/message-retry-manager.js +48 -0
- package/lib/Utils/messages-media.js +520 -0
- package/lib/Utils/messages.js +752 -0
- package/lib/Utils/meta-ai-msmsg.js +122 -0
- package/lib/Utils/meta-compositing.js +75 -0
- package/lib/Utils/modern-messages.js +235 -0
- package/lib/Utils/native-flow.js +55 -0
- package/lib/Utils/noise-handler.js +161 -0
- package/lib/Utils/pre-key-manager.js +76 -0
- package/lib/Utils/process-message.js +399 -0
- package/lib/Utils/reporting-utils.js +171 -0
- package/lib/Utils/signal.js +167 -0
- package/lib/Utils/spam-report-utils.js +135 -0
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +154 -0
- package/lib/Utils/use-multi-file-auth-state.js +307 -0
- package/lib/Utils/validate-connection.js +181 -0
- package/lib/WABinary/constants.js +1321 -0
- package/lib/WABinary/decode.js +152 -0
- package/lib/WABinary/encode.js +140 -0
- package/lib/WABinary/generic-utils.js +179 -0
- package/lib/WABinary/index.js +6 -0
- package/lib/WABinary/jid-utils.js +235 -0
- package/lib/WABinary/types.js +4 -0
- package/lib/WAM/BinaryInfo.js +10 -0
- package/lib/WAM/constants.js +22853 -0
- package/lib/WAM/encode.js +150 -0
- package/lib/WAM/index.js +3 -0
- package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
- package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
- package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
- package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
- package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
- package/lib/WAUSync/Protocols/index.js +12 -0
- package/lib/WAUSync/USyncQuery.js +100 -0
- package/lib/WAUSync/USyncUser.js +20 -0
- package/lib/WAUSync/index.js +3 -0
- package/lib/index.js +67 -0
- package/package.json +220 -5
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { KEY_BUNDLE_TYPE } from '../Defaults/index.js'
|
|
2
|
+
import { assertNodeErrorFree, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, getServerFromDomainType, jidDecode, S_WHATSAPP_NET, WAJIDDomains } from '../WABinary/index.js'
|
|
3
|
+
import { Curve, generateSignalPubKey } from './crypto.js'
|
|
4
|
+
import { encodeBigEndian } from './generics.js'
|
|
5
|
+
|
|
6
|
+
const chunk = (array, size) => {
|
|
7
|
+
const chunks = []
|
|
8
|
+
for (let i = 0; i < array.length; i += size) chunks.push(array.slice(i, i + size))
|
|
9
|
+
return chunks
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const createSignalIdentity = (wid, accountSignatureKey) => ({
|
|
13
|
+
identifier: { name: wid, deviceId: 0 },
|
|
14
|
+
identifierKey: generateSignalPubKey(accountSignatureKey),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const getPreKeys = async ({ get }, min, limit) => {
|
|
18
|
+
const idList = []
|
|
19
|
+
for (let id = min; id < limit; id++) idList.push(id.toString())
|
|
20
|
+
return get('pre-key', idList)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const generateOrGetPreKeys = (creds, range) => {
|
|
24
|
+
const available = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId
|
|
25
|
+
const remaining = range - available
|
|
26
|
+
const lastPreKeyId = creds.nextPreKeyId + remaining - 1
|
|
27
|
+
const newPreKeys = {}
|
|
28
|
+
if (remaining > 0) {
|
|
29
|
+
for (let i = creds.nextPreKeyId; i <= lastPreKeyId; i++) newPreKeys[i] = Curve.generateKeyPair()
|
|
30
|
+
}
|
|
31
|
+
return { newPreKeys, lastPreKeyId, preKeysRange: [creds.firstUnuploadedPreKeyId, range] }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const xmppSignedPreKey = key => ({
|
|
35
|
+
tag: 'skey',
|
|
36
|
+
attrs: {},
|
|
37
|
+
content: [
|
|
38
|
+
{ tag: 'id', attrs: {}, content: encodeBigEndian(key.keyId, 3) },
|
|
39
|
+
{ tag: 'value', attrs: {}, content: key.keyPair.public },
|
|
40
|
+
{ tag: 'signature', attrs: {}, content: key.signature },
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export const xmppPreKey = (pair, id) => ({
|
|
45
|
+
tag: 'key',
|
|
46
|
+
attrs: {},
|
|
47
|
+
content: [
|
|
48
|
+
{ tag: 'id', attrs: {}, content: encodeBigEndian(id, 3) },
|
|
49
|
+
{ tag: 'value', attrs: {}, content: pair.public },
|
|
50
|
+
],
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const isValidUInt = n => typeof n === 'number' && Number.isInteger(n) && n >= 0
|
|
54
|
+
|
|
55
|
+
export const extractE2ESessionFromRetryReceipt = receipt => {
|
|
56
|
+
const keysNode = getBinaryNodeChild(receipt, 'keys')
|
|
57
|
+
if (!keysNode) return null
|
|
58
|
+
|
|
59
|
+
const typeBuf = getBinaryNodeChildBuffer(keysNode, 'type')
|
|
60
|
+
if (!typeBuf || typeBuf.length !== 1 || typeBuf[0] !== KEY_BUNDLE_TYPE[0]) return null
|
|
61
|
+
|
|
62
|
+
const identity = getBinaryNodeChildBuffer(keysNode, 'identity')
|
|
63
|
+
const skey = getBinaryNodeChild(keysNode, 'skey')
|
|
64
|
+
if (!identity || identity.length !== 32 || !skey) return null
|
|
65
|
+
|
|
66
|
+
const registrationId = getBinaryNodeChildUInt(receipt, 'registration', 4)
|
|
67
|
+
if (!isValidUInt(registrationId)) return null
|
|
68
|
+
|
|
69
|
+
const signedPubKey = getBinaryNodeChildBuffer(skey, 'value')
|
|
70
|
+
const signedSig = getBinaryNodeChildBuffer(skey, 'signature')
|
|
71
|
+
const signedKeyId = getBinaryNodeChildUInt(skey, 'id', 3)
|
|
72
|
+
if (!signedPubKey || signedPubKey.length !== 32 || !signedSig || !isValidUInt(signedKeyId)) return null
|
|
73
|
+
|
|
74
|
+
const preKeyNode = getBinaryNodeChild(keysNode, 'key')
|
|
75
|
+
let preKey
|
|
76
|
+
if (preKeyNode) {
|
|
77
|
+
const preKeyPub = getBinaryNodeChildBuffer(preKeyNode, 'value')
|
|
78
|
+
const preKeyId = getBinaryNodeChildUInt(preKeyNode, 'id', 3)
|
|
79
|
+
if (!preKeyPub || preKeyPub.length !== 32 || !isValidUInt(preKeyId)) return null
|
|
80
|
+
preKey = { keyId: preKeyId, publicKey: generateSignalPubKey(preKeyPub) }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
registrationId,
|
|
85
|
+
identityKey: generateSignalPubKey(identity),
|
|
86
|
+
signedPreKey: { keyId: signedKeyId, publicKey: generateSignalPubKey(signedPubKey), signature: signedSig },
|
|
87
|
+
preKey,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const parseAndInjectE2ESessions = async (node, repository) => {
|
|
92
|
+
const extractKey = key => key ? {
|
|
93
|
+
keyId: getBinaryNodeChildUInt(key, 'id', 3),
|
|
94
|
+
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')),
|
|
95
|
+
signature: getBinaryNodeChildBuffer(key, 'signature'),
|
|
96
|
+
} : undefined
|
|
97
|
+
|
|
98
|
+
const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user').filter(node => {
|
|
99
|
+
try { assertNodeErrorFree(node); return true } catch { return false }
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
for (const nodesChunk of chunk(nodes, 100)) {
|
|
103
|
+
await Promise.all(nodesChunk.map(node => repository.injectE2ESession({
|
|
104
|
+
jid: node.attrs.jid,
|
|
105
|
+
session: {
|
|
106
|
+
registrationId: getBinaryNodeChildUInt(node, 'registration', 4),
|
|
107
|
+
identityKey: generateSignalPubKey(getBinaryNodeChildBuffer(node, 'identity')),
|
|
108
|
+
signedPreKey: extractKey(getBinaryNodeChild(node, 'skey')),
|
|
109
|
+
preKey: extractKey(getBinaryNodeChild(node, 'key')),
|
|
110
|
+
},
|
|
111
|
+
})))
|
|
112
|
+
await new Promise(resolve => setImmediate(resolve)) // yield between chunks to avoid blocking on large batches
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const extractDeviceJids = (result, myJid, myLid, excludeZeroDevices) => {
|
|
117
|
+
const { user: myUser, device: myDevice } = jidDecode(myJid)
|
|
118
|
+
const extracted = []
|
|
119
|
+
for (const { devices, id } of result) {
|
|
120
|
+
const decoded = jidDecode(id)
|
|
121
|
+
const { user, server } = decoded
|
|
122
|
+
let { domainType } = decoded
|
|
123
|
+
const deviceList = devices?.deviceList
|
|
124
|
+
if (!Array.isArray(deviceList)) continue
|
|
125
|
+
for (const { id: device, keyIndex, isHosted } of deviceList) {
|
|
126
|
+
if ((!excludeZeroDevices || device !== 0) &&
|
|
127
|
+
((myUser !== user && myLid !== user) || myDevice !== device) &&
|
|
128
|
+
(device === 0 || !!keyIndex)) {
|
|
129
|
+
if (isHosted) domainType = domainType === WAJIDDomains.LID ? WAJIDDomains.HOSTED_LID : WAJIDDomains.HOSTED
|
|
130
|
+
extracted.push({ user, device, domainType, server: getServerFromDomainType(server, domainType) })
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return extracted
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const getNextPreKeys = async ({ creds, keys }, count) => {
|
|
138
|
+
const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(creds, count)
|
|
139
|
+
await keys.set({ 'pre-key': newPreKeys })
|
|
140
|
+
const preKeys = await getPreKeys(keys, preKeysRange[0], preKeysRange[0] + preKeysRange[1])
|
|
141
|
+
return {
|
|
142
|
+
update: {
|
|
143
|
+
nextPreKeyId: Math.max(lastPreKeyId + 1, creds.nextPreKeyId),
|
|
144
|
+
firstUnuploadedPreKeyId: Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId + 1),
|
|
145
|
+
},
|
|
146
|
+
preKeys,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const getNextPreKeysNode = async (state, count) => {
|
|
151
|
+
const { creds } = state
|
|
152
|
+
const { update, preKeys } = await getNextPreKeys(state, count)
|
|
153
|
+
return {
|
|
154
|
+
update,
|
|
155
|
+
node: {
|
|
156
|
+
tag: 'iq',
|
|
157
|
+
attrs: { xmlns: 'encrypt', type: 'set', to: S_WHATSAPP_NET },
|
|
158
|
+
content: [
|
|
159
|
+
{ tag: 'registration', attrs: {}, content: encodeBigEndian(creds.registrationId) },
|
|
160
|
+
{ tag: 'type', attrs: {}, content: KEY_BUNDLE_TYPE },
|
|
161
|
+
{ tag: 'identity', attrs: {}, content: creds.signedIdentityKey.public },
|
|
162
|
+
{ tag: 'list', attrs: {}, content: Object.keys(preKeys).map(k => xmppPreKey(preKeys[+k], +k)) },
|
|
163
|
+
xmppSignedPreKey(creds.signedPreKey),
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +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
|
+
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
|
+
})),
|
|
135
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isLidUser, isPnUser } from '../WABinary/index.js'
|
|
2
|
+
|
|
3
|
+
export const processContactAction = (action, id, logger) => {
|
|
4
|
+
const results = []
|
|
5
|
+
if (!id) {
|
|
6
|
+
logger?.warn({ hasFullName: !!action.fullName, hasLidJid: !!action.lidJid, hasPnJid: !!action.pnJid }, 'contactAction sync: missing id in index')
|
|
7
|
+
return results
|
|
8
|
+
}
|
|
9
|
+
const lidJid = action.lidJid
|
|
10
|
+
const idIsPn = isPnUser(id)
|
|
11
|
+
const phoneNumber = idIsPn ? id : action.pnJid || undefined
|
|
12
|
+
results.push({
|
|
13
|
+
event: 'contacts.upsert',
|
|
14
|
+
data: [{
|
|
15
|
+
id,
|
|
16
|
+
name: action.fullName || action.firstName || action.username || undefined,
|
|
17
|
+
username: action.username || undefined,
|
|
18
|
+
lid: lidJid || undefined,
|
|
19
|
+
phoneNumber
|
|
20
|
+
}]
|
|
21
|
+
})
|
|
22
|
+
if (lidJid && isLidUser(lidJid) && idIsPn) {
|
|
23
|
+
results.push({ event: 'lid-mapping.update', data: { lid: lidJid, pn: id } })
|
|
24
|
+
}
|
|
25
|
+
return results
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const emitSyncActionResults = (ev, results) => {
|
|
29
|
+
for (const result of results) {
|
|
30
|
+
if (result.event === 'contacts.upsert') ev.emit('contacts.upsert', result.data)
|
|
31
|
+
else ev.emit('lid-mapping.update', result.data)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto'
|
|
2
|
+
import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidMetaAI, isLidUser, isPnUser, jidNormalizedUser } from '../WABinary/index.js'
|
|
3
|
+
import { unixTimestampSeconds } from './generics.js'
|
|
4
|
+
|
|
5
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
6
|
+
export const TC_TOKEN_INDEX_KEY = '__index'
|
|
7
|
+
const BOT_PHONE_REGEX = /^1313555\d{4}$|^131655500\d{2}$/
|
|
8
|
+
const TC_TOKEN_BUCKET_DURATION = 604800 // 7 days
|
|
9
|
+
const TC_TOKEN_NUM_BUCKETS = 4 // ~28-day rolling window
|
|
10
|
+
const TC_TOKEN_MIN_BYTES = 1
|
|
11
|
+
const TC_TOKEN_MAX_BYTES = 120 // DMW layer constraint
|
|
12
|
+
const TC_TOKEN_MAX_RECIPIENTS = 5 // feature flag 20606 default cap
|
|
13
|
+
|
|
14
|
+
const currentBucket = () => Math.floor(unixTimestampSeconds() / TC_TOKEN_BUCKET_DURATION)
|
|
15
|
+
const generateTcToken = () => randomBytes(32) // local pre-seed fallback only; real tokens are server-issued
|
|
16
|
+
|
|
17
|
+
// ─── JID helpers ──────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export const isRegularUser = jid => {
|
|
20
|
+
if (!jid) return false
|
|
21
|
+
const user = jid.split('@')[0] ?? ''
|
|
22
|
+
if (user === '0' || BOT_PHONE_REGEX.test(user) || isJidMetaAI(jid)) return false
|
|
23
|
+
return !!(isPnUser(jid) || isLidUser(jid) || isHostedPnUser(jid) || isHostedLidUser(jid) || jid.endsWith('@c.us'))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// storage key is always LID when resolvable
|
|
27
|
+
export const resolveTcTokenJid = async (jid, getLIDForPN) =>
|
|
28
|
+
isLidUser(jid) ? jid : (await getLIDForPN(jid) || null)
|
|
29
|
+
|
|
30
|
+
// bidirectional — respects useLidForEncryption flag
|
|
31
|
+
export const resolveIssuanceJid = async (jid, issueToLid, getLIDForPN, getPNForLID) => {
|
|
32
|
+
if (issueToLid) return isLidUser(jid) ? jid : (await getLIDForPN(jid)) ?? jid
|
|
33
|
+
if (!isLidUser(jid)) return jid
|
|
34
|
+
return getPNForLID ? (await getPNForLID(jid)) ?? jid : jid
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Expiry ───────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export const isTcTokenExpired = timestamp => {
|
|
40
|
+
if (timestamp == null) return true
|
|
41
|
+
const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
|
|
42
|
+
return isNaN(ts) || ts < (currentBucket() - (TC_TOKEN_NUM_BUCKETS - 1)) * TC_TOKEN_BUCKET_DURATION
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// re-issue only if sender crossed into a new bucket since last issuance
|
|
46
|
+
export const shouldSendNewTcToken = senderTimestamp => {
|
|
47
|
+
if (senderTimestamp === undefined) return true
|
|
48
|
+
return currentBucket() > Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Index ────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export const readTcTokenIndex = async keys => {
|
|
54
|
+
for (const key of [TC_TOKEN_INDEX_KEY, 'index']) {
|
|
55
|
+
const entry = (await keys.get('tctoken', [key]))[key]
|
|
56
|
+
if (!entry?.token?.length) continue
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(Buffer.from(entry.token).toString())
|
|
59
|
+
if (!Array.isArray(parsed)) continue
|
|
60
|
+
return parsed.filter(j => typeof j === 'string' && j.length > 0 && j !== TC_TOKEN_INDEX_KEY && j !== 'index')
|
|
61
|
+
} catch { continue }
|
|
62
|
+
}
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const buildMergedTcTokenIndexWrite = async (keys, addedJids) => {
|
|
67
|
+
const merged = new Set(await readTcTokenIndex(keys))
|
|
68
|
+
for (const jid of addedJids) if (jid && jid !== TC_TOKEN_INDEX_KEY && jid !== 'index') merged.add(jid)
|
|
69
|
+
return { [TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...merged])) } }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Pre-seed ─────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export const preSeedTcToken = async ({ authState, jid, getLIDForPN, logger }) => {
|
|
75
|
+
const tcTokenJid = await resolveTcTokenJid(jid, getLIDForPN)
|
|
76
|
+
const existing = await authState.keys.get('tctoken', [tcTokenJid])
|
|
77
|
+
if (existing[tcTokenJid]?.token?.length) return { token: existing[tcTokenJid].token, storageJid: tcTokenJid }
|
|
78
|
+
const token = generateTcToken()
|
|
79
|
+
const timestamp = String(unixTimestampSeconds())
|
|
80
|
+
const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid])
|
|
81
|
+
await authState.keys.set({ tctoken: { [tcTokenJid]: { ...(existing[tcTokenJid] || {}), token, timestamp }, ...indexWrite } })
|
|
82
|
+
logger?.debug({ jid, tcTokenJid }, 'pre-seeded locally generated tctoken')
|
|
83
|
+
return { token, storageJid: tcTokenJid }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Outgoing nodes ───────────────────────────────────────────────────────────
|
|
87
|
+
// confirmed wire format: <tctoken>bytes</tctoken> — flat, identical to fanout format, no smax:any wrapper
|
|
88
|
+
// timestamp attr is internal only, never sent on wire
|
|
89
|
+
export const buildTcTokenFromJid = async ({ authState, jid, baseContent = [], getLIDForPN }) => {
|
|
90
|
+
try {
|
|
91
|
+
const storageJid = await resolveTcTokenJid(jid, getLIDForPN)
|
|
92
|
+
const entry = (await authState.keys.get('tctoken', [storageJid]))?.[storageJid]
|
|
93
|
+
const { token: tcTokenBuffer, timestamp, senderTimestamp } = entry ?? {}
|
|
94
|
+
if (!tcTokenBuffer?.length || timestamp === undefined || isTcTokenExpired(timestamp)) {
|
|
95
|
+
if (tcTokenBuffer) {
|
|
96
|
+
const cleared = senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp } : null
|
|
97
|
+
await authState.keys.set({ tctoken: { [storageJid]: cleared } })
|
|
98
|
+
}
|
|
99
|
+
return baseContent.length > 0 ? baseContent : undefined
|
|
100
|
+
}
|
|
101
|
+
if (tcTokenBuffer.length < TC_TOKEN_MIN_BYTES || tcTokenBuffer.length > TC_TOKEN_MAX_BYTES) return baseContent.length > 0 ? baseContent : undefined
|
|
102
|
+
baseContent.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
|
|
103
|
+
return baseContent
|
|
104
|
+
} catch { return baseContent.length > 0 ? baseContent : undefined }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// fanout <to> node — confirmed wire format: <tctoken>bytes</tctoken> (no smax:any wrapper)
|
|
108
|
+
// enforces per-send JID dedup (EOC.A01 Set guard) and flag-20606 recipient cap
|
|
109
|
+
export const buildFanoutTcTokenNode = ({ tokenBuffer, recipientsSeen, userJid }) => {
|
|
110
|
+
if (
|
|
111
|
+
!tokenBuffer?.length
|
|
112
|
+
|| tokenBuffer.length < TC_TOKEN_MIN_BYTES
|
|
113
|
+
|| tokenBuffer.length > TC_TOKEN_MAX_BYTES
|
|
114
|
+
|| recipientsSeen.size >= TC_TOKEN_MAX_RECIPIENTS
|
|
115
|
+
|| recipientsSeen.has(userJid)
|
|
116
|
+
) return null
|
|
117
|
+
recipientsSeen.add(userJid)
|
|
118
|
+
return { tag: 'tctoken', attrs: {}, content: tokenBuffer }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Incoming storage ─────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
// handles both IQ result and privacy_token notification (same <tokens> structure)
|
|
124
|
+
// wire format: <token type="trusted_contact" t="timestamp">bytes</token>
|
|
125
|
+
export const storeTcTokensFromIqResult = async ({ result, fallbackJid, keys, getLIDForPN, onNewJidStored }) => {
|
|
126
|
+
const tokensNode = getBinaryNodeChild(result, 'tokens')
|
|
127
|
+
if (!tokensNode) return
|
|
128
|
+
for (const tokenNode of getBinaryNodeChildren(tokensNode, 'token')) {
|
|
129
|
+
if (tokenNode.attrs.type !== 'trusted_contact') continue
|
|
130
|
+
if (!(tokenNode.content instanceof Uint8Array) || !tokenNode.content.length) continue
|
|
131
|
+
const tokenBytes = tokenNode.content
|
|
132
|
+
if (tokenBytes.length > TC_TOKEN_MAX_BYTES) continue
|
|
133
|
+
const rawJid = jidNormalizedUser(fallbackJid || tokenNode.attrs.jid)
|
|
134
|
+
if (!isRegularUser(rawJid)) continue
|
|
135
|
+
const storageJid = await resolveTcTokenJid(rawJid, getLIDForPN)
|
|
136
|
+
const existingEntry = (await keys.get('tctoken', [storageJid]))[storageJid]
|
|
137
|
+
const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0
|
|
138
|
+
const incomingTs = tokenNode.attrs.t ? Number(tokenNode.attrs.t) : 0
|
|
139
|
+
if (!incomingTs || (existingTs > 0 && existingTs > incomingTs)) continue
|
|
140
|
+
await keys.set({ tctoken: { [storageJid]: { ...existingEntry, token: Buffer.from(tokenBytes), timestamp: tokenNode.attrs.t } } })
|
|
141
|
+
onNewJidStored?.(storageJid)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Content binding ──────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
// URL-type messages only — confirmed from AbstractC2052DmX.A00()
|
|
148
|
+
// wire format: <content_binding>bytes</content_binding> (flat, no wrapper)
|
|
149
|
+
export const buildContentBindingNode = ({ userJid, contentBindingMap, messageType }) => {
|
|
150
|
+
if (messageType !== 'url' || !contentBindingMap?.has(userJid)) return null
|
|
151
|
+
const bytes = contentBindingMap.get(userJid)
|
|
152
|
+
if (!bytes?.length) return null
|
|
153
|
+
return { tag: 'content_binding', attrs: {}, content: bytes }
|
|
154
|
+
}
|