@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,520 @@
|
|
|
1
|
+
import { Boom } from '@hapi/boom'
|
|
2
|
+
import { spawn } from 'child_process'
|
|
3
|
+
import * as Crypto from 'crypto'
|
|
4
|
+
import { once } from 'events'
|
|
5
|
+
import { createReadStream, createWriteStream, mkdirSync, unlinkSync, promises as fs } from 'fs'
|
|
6
|
+
import { tmpdir as osTmpdir } from 'os'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
import { Readable, Transform } from 'stream'
|
|
9
|
+
import { URL } from 'url'
|
|
10
|
+
import { fileTypeFromBuffer } from 'file-type'
|
|
11
|
+
import https from 'node:https'
|
|
12
|
+
import http from 'node:http'
|
|
13
|
+
import { proto } from '../../WAProto/index.js'
|
|
14
|
+
import { DEFAULT_ORIGIN, MEDIA_HKDF_KEY_MAPPING, MEDIA_PATH_MAP } from '../Defaults/index.js'
|
|
15
|
+
import { getBinaryNodeChild, getBinaryNodeChildBuffer, jidNormalizedUser } from '../WABinary/index.js'
|
|
16
|
+
import { aesDecryptGCM, aesEncryptGCM, hkdf } from './crypto.js'
|
|
17
|
+
import { generateMessageIDV2 } from './generics.js'
|
|
18
|
+
|
|
19
|
+
// ─── TMP REGISTRY ─────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const _tmpRegistry = new Set()
|
|
22
|
+
process.on('exit', () => { for (const p of _tmpRegistry) try { unlinkSync(p) } catch { } })
|
|
23
|
+
const _trackTmp = p => { _tmpRegistry.add(p); return p }
|
|
24
|
+
const _untrackTmp = p => _tmpRegistry.delete(p)
|
|
25
|
+
|
|
26
|
+
const BAILEYS_TMP_DIR = (() => {
|
|
27
|
+
for (const dir of [join(process.cwd(), '.b-tmp'), join(osTmpdir(), 'b-tmp')]) {
|
|
28
|
+
try {
|
|
29
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
30
|
+
process.env.TMPDIR = dir
|
|
31
|
+
process.env.VIPS_TMPDIR = dir
|
|
32
|
+
process.env.VIPS_DISC_THRESHOLD = '999999999999'
|
|
33
|
+
return dir
|
|
34
|
+
} catch { }
|
|
35
|
+
}
|
|
36
|
+
throw new Error('No writable tmp directory found')
|
|
37
|
+
})()
|
|
38
|
+
|
|
39
|
+
export const tmpdir = () => BAILEYS_TMP_DIR
|
|
40
|
+
|
|
41
|
+
// ─── FFMPEG ───────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
let _ffmpegPath = null
|
|
44
|
+
const getFfmpegPath = async () => {
|
|
45
|
+
if (_ffmpegPath) return _ffmpegPath
|
|
46
|
+
try { const { default: p } = await import('ffmpeg-static'); if (p) return (_ffmpegPath = p) } catch { }
|
|
47
|
+
return (_ffmpegPath = 'ffmpeg')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const _spawnFfmpeg = (ffmpegPath, args) => new Promise((resolve, reject) => {
|
|
51
|
+
const ff = spawn(ffmpegPath, args, { stdio: ['ignore', 'ignore', 'pipe'] })
|
|
52
|
+
let stderr = ''
|
|
53
|
+
ff.stderr.on('data', d => { stderr += d })
|
|
54
|
+
ff.on('close', code => code === 0 ? resolve() : reject(new Error(`FFmpeg exited ${code}: ${stderr.slice(-500)}`)))
|
|
55
|
+
ff.on('error', reject)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// ─── IMAGE PROCESSING ─────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
export const getImageProcessingLibrary = async () => {
|
|
61
|
+
const [jimp, sharp] = await Promise.all([import('jimp').catch(() => null), import('sharp').catch(() => null)])
|
|
62
|
+
if (sharp) return { sharp }
|
|
63
|
+
if (jimp) return { jimp }
|
|
64
|
+
throw new Boom('No image processing library available')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const extractImageThumb = async (bufferOrFilePath, width = 32) => {
|
|
68
|
+
if (bufferOrFilePath instanceof Readable) bufferOrFilePath = await toBuffer(bufferOrFilePath)
|
|
69
|
+
const lib = await getImageProcessingLibrary()
|
|
70
|
+
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
|
|
71
|
+
const img = lib.sharp.default(bufferOrFilePath)
|
|
72
|
+
const { width: w, height: h } = await img.metadata()
|
|
73
|
+
return { buffer: await img.resize(width).jpeg({ quality: 95 }).toBuffer(), original: { width: w, height: h } }
|
|
74
|
+
}
|
|
75
|
+
if ('jimp' in lib && typeof lib.jimp?.Jimp === 'object') {
|
|
76
|
+
const jimp = await lib.jimp.Jimp.read(bufferOrFilePath)
|
|
77
|
+
return { buffer: await jimp.resize({ w: width, mode: lib.jimp.ResizeStrategy.BILINEAR }).getBuffer('image/jpeg', { quality: 95 }), original: { width: jimp.width, height: jimp.height } }
|
|
78
|
+
}
|
|
79
|
+
throw new Boom('No image processing library available')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const generateProfilePicture = async mediaUpload => {
|
|
83
|
+
const src = Buffer.isBuffer(mediaUpload) ? mediaUpload : 'url' in mediaUpload ? mediaUpload.url.toString() : await toBuffer(mediaUpload.stream)
|
|
84
|
+
const lib = await getImageProcessingLibrary()
|
|
85
|
+
if ('sharp' in lib && typeof lib.sharp?.default === 'function') return { img: await lib.sharp.default(src).resize(720, 720, { fit: 'inside' }).jpeg({ quality: 80 }).toBuffer() }
|
|
86
|
+
if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
|
|
87
|
+
const { read, MIME_JPEG } = lib.jimp
|
|
88
|
+
const image = await read(src)
|
|
89
|
+
return { img: await image.crop(0, 0, image.getWidth(), image.getHeight()).scaleToFit(720, 720).getBufferAsync(MIME_JPEG) }
|
|
90
|
+
}
|
|
91
|
+
throw new Boom('No image processing library available')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const generateThumbnail = async (file, mediaType, options) => {
|
|
95
|
+
if (mediaType === 'image') {
|
|
96
|
+
const { buffer, original } = await extractImageThumb(file)
|
|
97
|
+
return { thumbnail: buffer.toString('base64'), originalImageDimensions: (original.width && original.height) ? original : undefined }
|
|
98
|
+
}
|
|
99
|
+
if (mediaType === 'video') {
|
|
100
|
+
const imgFilename = join(tmpdir(), generateMessageIDV2() + '.jpg')
|
|
101
|
+
try {
|
|
102
|
+
const ff = await getFfmpegPath()
|
|
103
|
+
await new Promise((resolve, reject) => {
|
|
104
|
+
const proc = spawn(ff, ['-ss', '00:00:00', '-i', file, '-y', '-vf', 'scale=32:-1', '-vframes', '1', '-f', 'image2', imgFilename])
|
|
105
|
+
proc.on('close', code => code === 0 ? resolve() : reject(new Error(`FFmpeg thumb exit ${code}`)))
|
|
106
|
+
proc.on('error', reject)
|
|
107
|
+
})
|
|
108
|
+
const thumbnail = (await fs.readFile(imgFilename)).toString('base64')
|
|
109
|
+
await fs.unlink(imgFilename).catch(() => { })
|
|
110
|
+
return { thumbnail, originalImageDimensions: undefined }
|
|
111
|
+
} catch (err) { options.logger?.debug('could not generate video thumb: ' + err) }
|
|
112
|
+
}
|
|
113
|
+
return { thumbnail: undefined, originalImageDimensions: undefined }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── HKDF ─────────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export const hkdfInfoKey = type => `WhatsApp ${MEDIA_HKDF_KEY_MAPPING[type]} Keys`
|
|
119
|
+
|
|
120
|
+
export const getMediaKeys = async (buffer, mediaType) => {
|
|
121
|
+
if (!buffer) throw new Boom('Cannot derive from empty media key')
|
|
122
|
+
if (typeof buffer === 'string') buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64')
|
|
123
|
+
const k = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
|
|
124
|
+
return { iv: k.slice(0, 16), cipherKey: k.slice(16, 48), macKey: k.slice(48, 80) }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── STREAM UTILS ─────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
export const toReadable = buffer => { const r = new Readable({ read: () => { } }); r.push(buffer); r.push(null); return r }
|
|
130
|
+
|
|
131
|
+
export const toBuffer = async stream => {
|
|
132
|
+
if (Buffer.isBuffer(stream)) return stream
|
|
133
|
+
const chunks = []
|
|
134
|
+
for await (const chunk of stream) chunks.push(chunk)
|
|
135
|
+
stream.destroy?.()
|
|
136
|
+
return Buffer.concat(chunks)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const _isHttpUrl = str => { try { const { protocol } = new URL(str); return protocol === 'http:' || protocol === 'https:' } catch { return false } }
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
export const getStream = async (item, opts) => {
|
|
143
|
+
if (!item) throw new Boom('Item is required for getStream', { statusCode: 400 })
|
|
144
|
+
if (Buffer.isBuffer(item)) return { stream: toReadable(item), type: 'buffer' }
|
|
145
|
+
if (item?.stream?.pipe) return { stream: item.stream, type: 'readable' }
|
|
146
|
+
if (item?.pipe) return { stream: item, type: 'readable' }
|
|
147
|
+
if (item && typeof item === 'object' && 'url' in item) {
|
|
148
|
+
const urlStr = item.url.toString()
|
|
149
|
+
if (Buffer.isBuffer(item.url)) return { stream: toReadable(item.url), type: 'buffer' }
|
|
150
|
+
if (urlStr.startsWith('data:')) return { stream: toReadable(Buffer.from(urlStr.split(',')[1], 'base64')), type: 'buffer' }
|
|
151
|
+
if (_isHttpUrl(urlStr)) return { stream: await getHttpStream(item.url, opts), type: 'remote' }
|
|
152
|
+
return { stream: createReadStream(item.url), type: 'file' }
|
|
153
|
+
}
|
|
154
|
+
if (typeof item === 'string') {
|
|
155
|
+
if (item.startsWith('data:')) return { stream: toReadable(Buffer.from(item.split(',')[1], 'base64')), type: 'buffer' }
|
|
156
|
+
if (_isHttpUrl(item)) return { stream: await getHttpStream(item, opts), type: 'remote' }
|
|
157
|
+
return { stream: createReadStream(item), type: 'file' }
|
|
158
|
+
}
|
|
159
|
+
throw new Boom(`Invalid input type for getStream: ${typeof item}`, { statusCode: 400 })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const getHttpStream = (url, options = {}) => new Promise((resolve, reject) => {
|
|
163
|
+
const urlObj = new URL(url.toString())
|
|
164
|
+
const isHttps = urlObj.protocol === 'https:'
|
|
165
|
+
const mod = isHttps ? https : http
|
|
166
|
+
const req = mod.request({ hostname: urlObj.hostname, port: urlObj.port || (isHttps ? 443 : 80), path: urlObj.pathname + urlObj.search, method: 'GET', family: 4, headers: { ...(options.headers ?? {}), Origin: DEFAULT_ORIGIN } }, res => {
|
|
167
|
+
if (res.statusCode < 200 || res.statusCode >= 300) { res.destroy(); return reject(new Boom(`Failed to fetch stream from ${url}`, { statusCode: res.statusCode, data: { url } })) }
|
|
168
|
+
resolve(res)
|
|
169
|
+
})
|
|
170
|
+
req.on('error', reject)
|
|
171
|
+
req.end()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
export const normalizeToBuffer = async (input) => {
|
|
175
|
+
if (!input) return undefined; if (Buffer.isBuffer(input)) return input; if (input?.url) return normalizeToBuffer(input.url.toString())
|
|
176
|
+
if (typeof input === 'string' && input.startsWith('data:')) return Buffer.from(input.split(',')[1], 'base64')
|
|
177
|
+
return toBuffer((await getStream(input)).stream)
|
|
178
|
+
}
|
|
179
|
+
// ─── RAW UPLOAD ───────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
export const getRawMediaUploadData = async (media, mediaType, logger) => {
|
|
182
|
+
const { stream } = await getStream(media)
|
|
183
|
+
const hasher = Crypto.createHash('sha256')
|
|
184
|
+
const filePath = join(tmpdir(), mediaType + generateMessageIDV2())
|
|
185
|
+
const fileWriteStream = createWriteStream(filePath)
|
|
186
|
+
let fileLength = 0
|
|
187
|
+
try {
|
|
188
|
+
for await (const data of stream) { fileLength += data.length; hasher.update(data); if (!fileWriteStream.write(data)) await once(fileWriteStream, 'drain') }
|
|
189
|
+
fileWriteStream.end()
|
|
190
|
+
await once(fileWriteStream, 'finish')
|
|
191
|
+
stream.destroy()
|
|
192
|
+
return { filePath, fileSha256: hasher.digest(), fileLength }
|
|
193
|
+
} catch (error) {
|
|
194
|
+
fileWriteStream.destroy(); stream.destroy()
|
|
195
|
+
await fs.unlink(filePath).catch(() => { })
|
|
196
|
+
throw error
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── AUDIO / VIDEO ────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
export const mediaMessageSHA256B64 = message => { const media = Object.values(message)[0]; return media?.fileSha256 && Buffer.from(media.fileSha256).toString('base64') }
|
|
203
|
+
|
|
204
|
+
export const getAudioDuration = async (buffer, logger) => {
|
|
205
|
+
try {
|
|
206
|
+
const mm = await import('music-metadata')
|
|
207
|
+
if (Buffer.isBuffer(buffer)) return (await mm.parseBuffer(buffer, undefined, { duration: true })).format.duration
|
|
208
|
+
if (typeof buffer === 'string') return (await mm.parseFile(buffer, { duration: true })).format.duration
|
|
209
|
+
return (await mm.parseStream(buffer, undefined, { duration: true })).format.duration
|
|
210
|
+
} catch (e) { logger?.debug({ trace: e?.stack || e }, 'failed to determine audio duration'); return undefined }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const getAudioWaveform = async (buffer, logger) => {
|
|
214
|
+
const fallback = new Uint8Array([0, 99, 0, 99, 0, 99, 0, 99, 88, 99, 0, 99, 0, 55, 0, 99, 0, 99, 0, 99, 0, 99, 0, 99, 88, 99, 0, 99, 0, 55, 0, 99, 0, 99, 0, 99, 0, 99, 88, 99, 0, 99, 0, 55, 0, 99, 0, 99, 0, 99, 0, 99, 0, 99, 88, 99, 0, 99, 0, 55, 0, 99, 0, 99])
|
|
215
|
+
const bars = 64
|
|
216
|
+
try {
|
|
217
|
+
const ffmpegPath = await getFfmpegPath()
|
|
218
|
+
const rawPCM = await new Promise(async (resolve, reject) => {
|
|
219
|
+
const chunks = []
|
|
220
|
+
let stderr = ''
|
|
221
|
+
const ff = spawn(ffmpegPath, ['-i', 'pipe:0', '-f', 's16le', '-ac', '1', '-ar', '16000', 'pipe:1'], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
222
|
+
ff.stdout.on('data', d => chunks.push(d))
|
|
223
|
+
ff.stderr.on('data', d => { stderr += d })
|
|
224
|
+
ff.on('close', code => code === 0 ? resolve(Buffer.concat(chunks)) : reject(new Error(`FFmpeg waveform exit ${code}: ${stderr.slice(-300)}`)))
|
|
225
|
+
ff.on('error', reject)
|
|
226
|
+
ff.stdin.end(Buffer.isBuffer(buffer) ? buffer : typeof buffer === 'string' ? await fs.readFile(buffer) : await toBuffer(buffer))
|
|
227
|
+
})
|
|
228
|
+
if (!rawPCM.length) throw new Error('empty PCM output')
|
|
229
|
+
const samples = rawPCM.length >> 1
|
|
230
|
+
const blockSize = Math.max(1, Math.floor(samples / bars))
|
|
231
|
+
const blockSums = new Float64Array(bars)
|
|
232
|
+
for (let i = 0; i < samples; i++) blockSums[Math.min(bars - 1, Math.floor(i / blockSize))] += Math.abs(rawPCM.readInt16LE(i * 2))
|
|
233
|
+
const avg = Array.from(blockSums, sum => sum / blockSize / 32768)
|
|
234
|
+
const max = Math.max(...avg, 0.0001)
|
|
235
|
+
return new Uint8Array(avg.map(v => Math.max(0, Math.min(100, Math.round((v / max) * 100)))))
|
|
236
|
+
} catch (e) { logger?.debug({ trace: e?.stack || e }, 'failed to generate waveform, using fallback'); return fallback }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── FFMPEG CONVERTERS ────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
const _cleanupFiles = async (...paths) => Promise.all(paths.map(p => p ? fs.unlink(p).catch(() => { }) : Promise.resolve()))
|
|
242
|
+
|
|
243
|
+
const _animatedWebpToMp4 = async (buffer, ffmpegPath) => {
|
|
244
|
+
const { default: sharp } = await import('sharp')
|
|
245
|
+
const meta = await sharp(buffer, { animated: true }).metadata()
|
|
246
|
+
const pages = meta.pages ?? 1, pageH = meta.pageHeight ?? Math.round(meta.height / pages)
|
|
247
|
+
const delays = meta.delay?.length === pages ? meta.delay : Array(pages).fill(100)
|
|
248
|
+
const uid = generateMessageIDV2()
|
|
249
|
+
const frameDir = join(tmpdir(), `webp-frames-${uid}`), concatFile = join(tmpdir(), `webp-concat-${uid}.txt`), outputPath = join(tmpdir(), `webp-out-${uid}.mp4`)
|
|
250
|
+
await fs.mkdir(frameDir, { recursive: true })
|
|
251
|
+
try {
|
|
252
|
+
const { data, info } = await sharp(buffer, { animated: true }).raw().toBuffer({ resolveWithObject: true })
|
|
253
|
+
const frameSize = info.width * pageH * info.channels
|
|
254
|
+
const framePaths = []
|
|
255
|
+
for (let i = 0; i < pages; i++) {
|
|
256
|
+
const fp = join(frameDir, `f${i}.png`)
|
|
257
|
+
await sharp(data.slice(i * frameSize, (i + 1) * frameSize), { raw: { width: info.width, height: pageH, channels: info.channels } }).png().toFile(fp)
|
|
258
|
+
framePaths.push(fp)
|
|
259
|
+
}
|
|
260
|
+
const lines = framePaths.flatMap((p, i) => [`file '${p}'`, `duration ${(delays[i] / 1000).toFixed(6)}`])
|
|
261
|
+
lines.push(`file '${framePaths[framePaths.length - 1]}'`)
|
|
262
|
+
await fs.writeFile(concatFile, lines.join('\n'))
|
|
263
|
+
await _spawnFfmpeg(ffmpegPath, ['-y', '-f', 'concat', '-safe', '0', '-i', concatFile, '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '18', '-pix_fmt', 'yuv420p', '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2', '-movflags', '+faststart', '-map_metadata', '-1', outputPath])
|
|
264
|
+
return await fs.readFile(outputPath)
|
|
265
|
+
} finally {
|
|
266
|
+
const files = await fs.readdir(frameDir).catch(() => [])
|
|
267
|
+
await Promise.all(files.map(f => fs.unlink(join(frameDir, f)).catch(() => { })))
|
|
268
|
+
await fs.rmdir(frameDir).catch(() => { })
|
|
269
|
+
await _cleanupFiles(concatFile, outputPath)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const _tmpConvert = async (buffer, inSuffix, outSuffix, ffmpegArgs, logger) => {
|
|
274
|
+
const inputPath = _trackTmp(join(tmpdir(), `in-${generateMessageIDV2()}${inSuffix}`))
|
|
275
|
+
const outputPath = _trackTmp(join(tmpdir(), `out-${generateMessageIDV2()}${outSuffix}`))
|
|
276
|
+
await fs.writeFile(inputPath, buffer)
|
|
277
|
+
try { await _spawnFfmpeg(await getFfmpegPath(), ['-y', '-i', inputPath, ...ffmpegArgs, outputPath]); return await fs.readFile(outputPath) }
|
|
278
|
+
finally { await _cleanupFiles(inputPath, outputPath); _untrackTmp(inputPath); _untrackTmp(outputPath) }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const convertToOpusBuffer = async (buffer, logger) => {
|
|
282
|
+
const detected = await fileTypeFromBuffer(buffer)
|
|
283
|
+
if (detected?.ext === 'opus') return buffer
|
|
284
|
+
return _tmpConvert(buffer, detected?.ext ? `.${detected.ext}` : '', '.ogg', ['-vn', '-c:a', 'libopus', '-b:a', '128k', '-ar', '48000', '-ac', '1', '-vbr', 'on', '-compression_level', '10', '-frame_duration', '20', '-application', 'audio', '-map_metadata', '-1'], logger)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const convertToMp4Buffer = async (buffer, logger) => {
|
|
288
|
+
try {
|
|
289
|
+
const { default: sharp } = await import('sharp')
|
|
290
|
+
const meta = await sharp(buffer, { animated: true }).metadata()
|
|
291
|
+
if (meta.format === 'webp' && (meta.pages ?? 1) > 1) return await _animatedWebpToMp4(buffer, await getFfmpegPath())
|
|
292
|
+
} catch { }
|
|
293
|
+
const detected = await fileTypeFromBuffer(buffer)
|
|
294
|
+
return _tmpConvert(buffer, detected?.ext ? `.${detected.ext}` : '', '.mp4', ['-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23', '-pix_fmt', 'yuv420p', '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2', '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', '-map_metadata', '-1'], logger)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ─── PREPARE / ENCRYPT STREAM ─────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
export const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, convertVideo } = {}) => {
|
|
300
|
+
const { stream, type } = await getStream(media, opts)
|
|
301
|
+
let buffer = await toBuffer(stream)
|
|
302
|
+
if (mediaType === 'video' && convertVideo) {
|
|
303
|
+
try { buffer = await convertToMp4Buffer(buffer, logger) } catch (e) { logger?.error('failed to convert video:', e) }
|
|
304
|
+
}
|
|
305
|
+
let bodyPath, didSaveToTmpPath = false
|
|
306
|
+
try {
|
|
307
|
+
if (type === 'file') bodyPath = media.url
|
|
308
|
+
else if (saveOriginalFileIfRequired) { bodyPath = join(tmpdir(), mediaType + generateMessageIDV2()); await fs.writeFile(bodyPath, buffer); didSaveToTmpPath = true }
|
|
309
|
+
return { mediaKey: undefined, encWriteStream: buffer, fileLength: buffer.length, fileSha256: Crypto.createHash('sha256').update(buffer).digest(), fileEncSha256: undefined, bodyPath, didSaveToTmpPath }
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (didSaveToTmpPath && bodyPath) await fs.unlink(bodyPath).catch(() => { })
|
|
312
|
+
throw error
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, mediaKey: providedMediaKey, isPtt, forceOpus, convertVideo } = {}) => {
|
|
317
|
+
const { stream, type } = await getStream(media, opts)
|
|
318
|
+
let finalStream = stream, opusConverted = false
|
|
319
|
+
if (mediaType === 'audio' && (isPtt === true || forceOpus === true)) {
|
|
320
|
+
try { finalStream = toReadable(await convertToOpusBuffer(await toBuffer(stream), logger)); opusConverted = true }
|
|
321
|
+
catch { finalStream = (await getStream(media, opts)).stream }
|
|
322
|
+
}
|
|
323
|
+
if (mediaType === 'video' && convertVideo === true) finalStream = toReadable(await convertToMp4Buffer(await toBuffer(finalStream), logger))
|
|
324
|
+
|
|
325
|
+
const mediaKey = providedMediaKey || Crypto.randomBytes(32)
|
|
326
|
+
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType)
|
|
327
|
+
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv)
|
|
328
|
+
const hmac = Crypto.createHmac('sha256', macKey).update(iv)
|
|
329
|
+
const sha256Plain = Crypto.createHash('sha256'), sha256Enc = Crypto.createHash('sha256')
|
|
330
|
+
const encChunks = [], plainChunks = saveOriginalFileIfRequired ? [] : null
|
|
331
|
+
let fileLength = 0, encFilePath = null, originalFilePath = null
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
for await (const data of finalStream) {
|
|
335
|
+
fileLength += data.length
|
|
336
|
+
if (type === 'remote' && opts?.maxContentLength && fileLength > opts.maxContentLength) throw new Boom('content length exceeded', { data: { media, type } })
|
|
337
|
+
plainChunks?.push(data)
|
|
338
|
+
sha256Plain.update(data)
|
|
339
|
+
const encrypted = aes.update(data)
|
|
340
|
+
sha256Enc.update(encrypted); hmac.update(encrypted); encChunks.push(encrypted)
|
|
341
|
+
}
|
|
342
|
+
const finalData = aes.final()
|
|
343
|
+
sha256Enc.update(finalData); hmac.update(finalData); encChunks.push(finalData)
|
|
344
|
+
const mac = hmac.digest().slice(0, 10)
|
|
345
|
+
sha256Enc.update(mac); encChunks.push(mac)
|
|
346
|
+
finalStream.destroy()
|
|
347
|
+
|
|
348
|
+
const encBuffer = Buffer.concat(encChunks)
|
|
349
|
+
const fileEncSha256 = sha256Enc.digest()
|
|
350
|
+
const fileSha256 = sha256Plain.digest()
|
|
351
|
+
|
|
352
|
+
if (plainChunks) {
|
|
353
|
+
try { originalFilePath = join(tmpdir(), mediaType + generateMessageIDV2() + '-original'); await fs.writeFile(originalFilePath, Buffer.concat(plainChunks)) }
|
|
354
|
+
catch { originalFilePath = null }
|
|
355
|
+
}
|
|
356
|
+
let useMemory = false
|
|
357
|
+
try { encFilePath = join(tmpdir(), mediaType + generateMessageIDV2() + '-enc'); await fs.writeFile(encFilePath, encBuffer) }
|
|
358
|
+
catch { encFilePath = null; useMemory = true }
|
|
359
|
+
|
|
360
|
+
const cleanup = async () => _cleanupFiles(encFilePath, originalFilePath)
|
|
361
|
+
return { mediaKey, bodyPath: originalFilePath, encFilePath, encBuffer: useMemory ? encBuffer : null, mac, fileEncSha256, fileSha256, fileLength, opusConverted, cleanup }
|
|
362
|
+
} catch (error) {
|
|
363
|
+
aes.destroy(); hmac.destroy(); sha256Plain.destroy(); sha256Enc.destroy(); finalStream.destroy()
|
|
364
|
+
await _cleanupFiles(encFilePath, originalFilePath)
|
|
365
|
+
throw error
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─── DOWNLOAD ─────────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
const DEF_HOST = 'mmg.whatsapp.net'
|
|
372
|
+
const AES_CHUNK_SIZE = 16
|
|
373
|
+
const toSmallestChunkSize = num => Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE
|
|
374
|
+
|
|
375
|
+
export const getUrlFromDirectPath = directPath => directPath ? (directPath.startsWith('http') ? directPath : `https://${DEF_HOST}${directPath.startsWith('/') ? '' : '/'}${directPath}`) : undefined
|
|
376
|
+
|
|
377
|
+
export const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
|
|
378
|
+
const downloadUrl = url?.startsWith('https://mmg.whatsapp.net/') ? url : getUrlFromDirectPath(directPath)
|
|
379
|
+
if (!downloadUrl) throw new Boom('No valid media URL or directPath present', { statusCode: 400 })
|
|
380
|
+
return downloadEncryptedContent(downloadUrl, await getMediaKeys(mediaKey, type), opts)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
|
|
384
|
+
let bytesFetched = 0, startChunk = 0, firstBlockIsIV = false
|
|
385
|
+
if (startByte) {
|
|
386
|
+
const chunk = toSmallestChunkSize(startByte || 0)
|
|
387
|
+
if (chunk) { startChunk = chunk - AES_CHUNK_SIZE; bytesFetched = chunk; firstBlockIsIV = true }
|
|
388
|
+
}
|
|
389
|
+
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined
|
|
390
|
+
const headers = { ...(options?.headers ? (Array.isArray(options.headers) ? Object.fromEntries(options.headers) : options.headers) : {}), Origin: DEFAULT_ORIGIN }
|
|
391
|
+
if (startChunk || endChunk) headers.Range = `bytes=${startChunk}-${endChunk || ''}`
|
|
392
|
+
const fetched = await getHttpStream(downloadUrl, { ...(options || {}), headers })
|
|
393
|
+
let remainingBytes = Buffer.from([]), aes
|
|
394
|
+
const pushBytes = (bytes, push) => {
|
|
395
|
+
if (startByte || endByte) {
|
|
396
|
+
const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0)
|
|
397
|
+
const end = bytesFetched + bytes.length < endByte ? undefined : Math.max(endByte - bytesFetched, 0)
|
|
398
|
+
push(bytes.slice(start, end)); bytesFetched += bytes.length
|
|
399
|
+
} else push(bytes)
|
|
400
|
+
}
|
|
401
|
+
return fetched.pipe(new Transform({
|
|
402
|
+
transform(chunk, _, callback) {
|
|
403
|
+
let data = Buffer.concat([remainingBytes, chunk])
|
|
404
|
+
const decryptLength = toSmallestChunkSize(data.length)
|
|
405
|
+
remainingBytes = data.slice(decryptLength); data = data.slice(0, decryptLength)
|
|
406
|
+
if (!aes) {
|
|
407
|
+
let ivValue = iv
|
|
408
|
+
if (firstBlockIsIV) { ivValue = data.slice(0, AES_CHUNK_SIZE); data = data.slice(AES_CHUNK_SIZE) }
|
|
409
|
+
aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue)
|
|
410
|
+
if (endByte) aes.setAutoPadding(false)
|
|
411
|
+
}
|
|
412
|
+
try { pushBytes(aes.update(data), b => this.push(b)); callback() } catch (error) { callback(error) }
|
|
413
|
+
},
|
|
414
|
+
final(callback) { try { pushBytes(aes.final(), b => this.push(b)); callback() } catch (error) { callback(error) } }
|
|
415
|
+
}), { end: true })
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ─── UPLOAD ───────────────────────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
export const encodeBase64EncodedStringForUpload = b64 => encodeURIComponent(b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''))
|
|
421
|
+
|
|
422
|
+
export const extensionForMediaMessage = message => {
|
|
423
|
+
const type = Object.keys(message)[0]
|
|
424
|
+
if (type === 'locationMessage' || type === 'liveLocationMessage' || type === 'productMessage') return '.jpeg'
|
|
425
|
+
return message[type].mimetype.split(';')[0]?.split('/')[1]
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const httpsPost = (url, body, headers, signal) => new Promise((resolve, reject) => {
|
|
429
|
+
const urlObj = new URL(url)
|
|
430
|
+
const isHttps = urlObj.protocol === 'https:'
|
|
431
|
+
const buf = Buffer.isBuffer(body) ? body : null
|
|
432
|
+
const req = (isHttps ? https : http).request({ hostname: urlObj.hostname, port: urlObj.port || (isHttps ? 443 : 80), path: urlObj.pathname + urlObj.search, method: 'POST', family: 4, headers: { ...headers, ...(buf ? { 'Content-Length': buf.length } : {}) } }, res => {
|
|
433
|
+
const chunks = []
|
|
434
|
+
res.on('data', d => chunks.push(d))
|
|
435
|
+
res.on('end', () => { let json = null; try { json = JSON.parse(Buffer.concat(chunks).toString()) } catch { }; resolve({ status: res.statusCode, ok: res.statusCode >= 200 && res.statusCode < 300, json: async () => json }) })
|
|
436
|
+
res.on('error', reject)
|
|
437
|
+
})
|
|
438
|
+
req.on('error', reject)
|
|
439
|
+
if (signal) {
|
|
440
|
+
if (signal.aborted) { req.destroy(); return reject(new Error('Upload aborted')) }
|
|
441
|
+
const onAbort = () => { req.destroy(); reject(new Error('Upload aborted')) }
|
|
442
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
443
|
+
req.on('close', () => signal.removeEventListener('abort', onAbort))
|
|
444
|
+
}
|
|
445
|
+
buf ? req.end(buf) : body.pipe(req)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
export const getWAUploadToServer = ({ customUploadHosts, logger, options }, refreshMediaConn) => async (stream, { mediaType, fileEncSha256B64, newsletter, timeoutMs }) => {
|
|
449
|
+
const toUploadBody = async input => {
|
|
450
|
+
if (!input) throw new Boom('Upload input is null or undefined', { statusCode: 400 })
|
|
451
|
+
if (Buffer.isBuffer(input)) return input
|
|
452
|
+
if (typeof input === 'string') return fs.readFile(input)
|
|
453
|
+
if (typeof ReadableStream !== 'undefined' && input instanceof ReadableStream) return Readable.fromWeb(input)
|
|
454
|
+
if (typeof input.pipe === 'function' || typeof input[Symbol.asyncIterator] === 'function') return input
|
|
455
|
+
throw new Boom(`Unsupported upload input type: ${Object.prototype.toString.call(input)}`, { statusCode: 400 })
|
|
456
|
+
}
|
|
457
|
+
let reqBody
|
|
458
|
+
try { reqBody = await toUploadBody(stream) } catch (err) { logger?.error({ err: err.message }, 'failed to prepare upload body'); throw err }
|
|
459
|
+
fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
|
|
460
|
+
let media = MEDIA_PATH_MAP[mediaType]
|
|
461
|
+
if (newsletter) media = media?.replace('/mms/', '/newsletter/newsletter-')
|
|
462
|
+
if (!media) throw new Boom(`No media path found for type: ${mediaType}`, { statusCode: 400 })
|
|
463
|
+
let uploadInfo = await refreshMediaConn(false)
|
|
464
|
+
const hosts = [...(customUploadHosts ?? []), ...(uploadInfo.hosts ?? [])]
|
|
465
|
+
if (!hosts.length) throw new Boom('No upload hosts available', { statusCode: 503 })
|
|
466
|
+
let urls, lastError
|
|
467
|
+
for (const { hostname, maxContentLengthBytes } of hosts) {
|
|
468
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
469
|
+
try {
|
|
470
|
+
if (attempt > 1) { uploadInfo = await refreshMediaConn(true); reqBody = await toUploadBody(stream) }
|
|
471
|
+
if (maxContentLengthBytes && Buffer.isBuffer(reqBody) && reqBody.length > maxContentLengthBytes) break
|
|
472
|
+
const auth = encodeURIComponent(uploadInfo.auth)
|
|
473
|
+
const url = `https://${hostname}${media}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
|
|
474
|
+
const controller = new AbortController()
|
|
475
|
+
const timer = timeoutMs ? setTimeout(() => controller.abort(), timeoutMs) : null
|
|
476
|
+
let response
|
|
477
|
+
try { response = await httpsPost(url, reqBody, { ...(Array.isArray(options?.headers) ? Object.fromEntries(options.headers) : (options?.headers ?? {})), 'Content-Type': 'application/octet-stream', Origin: DEFAULT_ORIGIN }, controller.signal) }
|
|
478
|
+
finally { if (timer) clearTimeout(timer) }
|
|
479
|
+
const result = await response.json().catch(() => null)
|
|
480
|
+
if (result?.url || result?.directPath) { urls = { mediaUrl: result.url, directPath: result.direct_path, handle: result.handle }; break }
|
|
481
|
+
lastError = new Error(`${hostname} rejected upload (HTTP ${response.status}): ${JSON.stringify(result)}`)
|
|
482
|
+
} catch (err) { lastError = err; if (attempt < 2) await new Promise(r => setTimeout(r, 500 * attempt)) }
|
|
483
|
+
}
|
|
484
|
+
if (urls) break
|
|
485
|
+
}
|
|
486
|
+
if (!urls) throw new Boom(`Media upload failed on all hosts. Last error: ${lastError?.message ?? 'unknown'}`, { statusCode: 500, data: { lastError: lastError?.message } })
|
|
487
|
+
return urls
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ─── MEDIA RETRY ──────────────────────────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
const getMediaRetryKey = mediaKey => hkdf(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' })
|
|
493
|
+
|
|
494
|
+
export const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
|
|
495
|
+
const iv = Crypto.randomBytes(12)
|
|
496
|
+
const ciphertext = aesEncryptGCM(proto.ServerErrorReceipt.encode({ stanzaId: key.id }).finish(), await getMediaRetryKey(mediaKey), iv, Buffer.from(key.id))
|
|
497
|
+
return { tag: 'receipt', attrs: { id: key.id, to: jidNormalizedUser(meId), type: 'server-error' }, content: [{ tag: 'encrypt', attrs: {}, content: [{ tag: 'enc_p', attrs: {}, content: ciphertext }, { tag: 'enc_iv', attrs: {}, content: iv }] }, { tag: 'rmr', attrs: { jid: key.remoteJid, from_me: (!!key.fromMe).toString(), participant: key.participant } }] }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export const decodeMediaRetryNode = node => {
|
|
501
|
+
const rmrNode = getBinaryNodeChild(node, 'rmr')
|
|
502
|
+
const event = { key: { id: node.attrs.id, remoteJid: rmrNode.attrs.jid, fromMe: rmrNode.attrs.from_me === 'true', participant: rmrNode.attrs.participant } }
|
|
503
|
+
const errorNode = getBinaryNodeChild(node, 'error')
|
|
504
|
+
if (errorNode) { event.error = new Boom(`Failed to re-upload media (${+errorNode.attrs.code})`, { data: errorNode.attrs, statusCode: getStatusCodeForMediaRetry(+errorNode.attrs.code) }); return event }
|
|
505
|
+
const encNode = getBinaryNodeChild(node, 'encrypt')
|
|
506
|
+
const ciphertext = getBinaryNodeChildBuffer(encNode, 'enc_p')
|
|
507
|
+
const iv = getBinaryNodeChildBuffer(encNode, 'enc_iv')
|
|
508
|
+
event[ciphertext && iv ? 'media' : 'error'] = ciphertext && iv ? { ciphertext, iv } : new Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 })
|
|
509
|
+
return event
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export const decryptMediaRetryData = async ({ ciphertext, iv }, mediaKey, msgId) => proto.MediaRetryNotification.decode(aesDecryptGCM(ciphertext, await getMediaRetryKey(mediaKey), iv, Buffer.from(msgId)))
|
|
513
|
+
|
|
514
|
+
export const getStatusCodeForMediaRetry = code => MEDIA_RETRY_STATUS_MAP[code]
|
|
515
|
+
const MEDIA_RETRY_STATUS_MAP = {
|
|
516
|
+
[proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
|
|
517
|
+
[proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
|
|
518
|
+
[proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
|
|
519
|
+
[proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
|
|
520
|
+
}
|