@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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3206 -3
  3. package/WAProto/WAProtoCompile.js +291 -0
  4. package/WAProto/fetcher.js +69 -0
  5. package/WAProto/index.js +88 -0
  6. package/WAProto/parser.js +313 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Builders/AIRich.js +643 -0
  9. package/lib/Builders/base.js +130 -0
  10. package/lib/Builders/builders.js +449 -0
  11. package/lib/Builders/index.js +3 -0
  12. package/lib/Defaults/index.js +225 -0
  13. package/lib/Defaults/phonenumber-mcc.json +223 -0
  14. package/lib/Signal/index.js +2 -0
  15. package/lib/Signal/libsignal.js +523 -0
  16. package/lib/Signal/lid-mapping.js +283 -0
  17. package/lib/Socket/Client/index.js +2 -0
  18. package/lib/Socket/Client/types.js +10 -0
  19. package/lib/Socket/Client/websocket.js +84 -0
  20. package/lib/Socket/business.js +152 -0
  21. package/lib/Socket/chats.js +855 -0
  22. package/lib/Socket/communities.js +147 -0
  23. package/lib/Socket/groups.js +294 -0
  24. package/lib/Socket/index.js +15 -0
  25. package/lib/Socket/messages-recv.js +981 -0
  26. package/lib/Socket/messages-send.js +708 -0
  27. package/lib/Socket/mex.js +34 -0
  28. package/lib/Socket/newsletter.js +781 -0
  29. package/lib/Socket/registration.js +155 -0
  30. package/lib/Socket/socket.js +797 -0
  31. package/lib/Socket/spam-report.js +65 -0
  32. package/lib/Socket/username.js +82 -0
  33. package/lib/Socket/zervonex-handler.js +55 -0
  34. package/lib/Store/index.js +6 -0
  35. package/lib/Store/make-cache-manager-store.js +81 -0
  36. package/lib/Store/make-in-memory-store.js +425 -0
  37. package/lib/Store/make-ordered-dictionary.js +82 -0
  38. package/lib/Store/object-repository.js +31 -0
  39. package/lib/Store/reporting-info-store.js +38 -0
  40. package/lib/Types/Auth.js +2 -0
  41. package/lib/Types/Bussines.js +2 -0
  42. package/lib/Types/Call.js +2 -0
  43. package/lib/Types/Chat.js +8 -0
  44. package/lib/Types/Contact.js +2 -0
  45. package/lib/Types/Events.js +2 -0
  46. package/lib/Types/GroupMetadata.js +2 -0
  47. package/lib/Types/Label.js +25 -0
  48. package/lib/Types/LabelAssociation.js +7 -0
  49. package/lib/Types/Message.js +15 -0
  50. package/lib/Types/Newsletter.js +100 -0
  51. package/lib/Types/Product.js +2 -0
  52. package/lib/Types/Signal.js +2 -0
  53. package/lib/Types/Socket.js +14 -0
  54. package/lib/Types/State.js +26 -0
  55. package/lib/Types/USync.js +2 -0
  56. package/lib/Types/index.js +30 -0
  57. package/lib/Utils/auth-utils.js +142 -0
  58. package/lib/Utils/baileys-event-stream.js +33 -0
  59. package/lib/Utils/bot-capabilities.js +40 -0
  60. package/lib/Utils/bot-planning-replay.js +44 -0
  61. package/lib/Utils/browser-utils.js +26 -0
  62. package/lib/Utils/business.js +75 -0
  63. package/lib/Utils/chat-utils.js +317 -0
  64. package/lib/Utils/companion-reg-client-utils.js +20 -0
  65. package/lib/Utils/consumer-application.js +38 -0
  66. package/lib/Utils/crypto.js +52 -0
  67. package/lib/Utils/decode-wa-message.js +197 -0
  68. package/lib/Utils/event-buffer.js +273 -0
  69. package/lib/Utils/extras.js +186 -0
  70. package/lib/Utils/generics.js +195 -0
  71. package/lib/Utils/history.js +51 -0
  72. package/lib/Utils/identity-change-handler.js +19 -0
  73. package/lib/Utils/index.js +38 -0
  74. package/lib/Utils/link-preview.js +51 -0
  75. package/lib/Utils/logger.js +2 -0
  76. package/lib/Utils/lt-hash.js +3 -0
  77. package/lib/Utils/make-mutex.js +16 -0
  78. package/lib/Utils/message-composer.js +59 -0
  79. package/lib/Utils/message-retry-manager.js +48 -0
  80. package/lib/Utils/messages-media.js +520 -0
  81. package/lib/Utils/messages.js +752 -0
  82. package/lib/Utils/meta-ai-msmsg.js +122 -0
  83. package/lib/Utils/meta-compositing.js +75 -0
  84. package/lib/Utils/modern-messages.js +235 -0
  85. package/lib/Utils/native-flow.js +55 -0
  86. package/lib/Utils/noise-handler.js +161 -0
  87. package/lib/Utils/pre-key-manager.js +76 -0
  88. package/lib/Utils/process-message.js +399 -0
  89. package/lib/Utils/reporting-utils.js +171 -0
  90. package/lib/Utils/signal.js +167 -0
  91. package/lib/Utils/spam-report-utils.js +135 -0
  92. package/lib/Utils/sync-action-utils.js +33 -0
  93. package/lib/Utils/tc-token-utils.js +154 -0
  94. package/lib/Utils/use-multi-file-auth-state.js +307 -0
  95. package/lib/Utils/validate-connection.js +181 -0
  96. package/lib/WABinary/constants.js +1321 -0
  97. package/lib/WABinary/decode.js +152 -0
  98. package/lib/WABinary/encode.js +140 -0
  99. package/lib/WABinary/generic-utils.js +179 -0
  100. package/lib/WABinary/index.js +6 -0
  101. package/lib/WABinary/jid-utils.js +235 -0
  102. package/lib/WABinary/types.js +4 -0
  103. package/lib/WAM/BinaryInfo.js +10 -0
  104. package/lib/WAM/constants.js +22853 -0
  105. package/lib/WAM/encode.js +150 -0
  106. package/lib/WAM/index.js +3 -0
  107. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
  108. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
  109. package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
  110. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
  111. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
  112. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
  113. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
  114. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
  115. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
  116. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
  117. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
  118. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
  119. package/lib/WAUSync/Protocols/index.js +12 -0
  120. package/lib/WAUSync/USyncQuery.js +100 -0
  121. package/lib/WAUSync/USyncUser.js +20 -0
  122. package/lib/WAUSync/index.js +3 -0
  123. package/lib/index.js +67 -0
  124. 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
+ }