@pontalabs/baileys 1.0.6 → 1.0.8
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 +1 -1
- package/README.md +839 -267
- package/WAProto/GenerateStatics.sh +4 -3
- package/WAProto/WAProto.proto +511 -1215
- package/WAProto/index.d.ts +55 -14017
- package/WAProto/index.js +29 -97689
- package/lib/Defaults/index.d.ts +74 -72
- package/lib/Defaults/index.js +108 -94
- package/lib/Signal/Group/ciphertext-message.d.ts +7 -8
- package/lib/Signal/Group/ciphertext-message.js +16 -9
- package/lib/Signal/Group/group-session-builder.d.ts +13 -11
- package/lib/Signal/Group/group-session-builder.js +61 -19
- package/lib/Signal/Group/group_cipher.d.ts +16 -14
- package/lib/Signal/Group/group_cipher.js +70 -41
- package/lib/Signal/Group/index.d.ts +11 -12
- package/lib/Signal/Group/index.js +61 -12
- package/lib/Signal/Group/keyhelper.d.ts +14 -9
- package/lib/Signal/Group/keyhelper.js +58 -10
- package/lib/Signal/Group/sender-chain-key.d.ts +13 -13
- package/lib/Signal/Group/sender-chain-key.js +34 -13
- package/lib/Signal/Group/sender-key-distribution-message.d.ts +16 -16
- package/lib/Signal/Group/sender-key-distribution-message.js +36 -28
- package/lib/Signal/Group/sender-key-message.d.ts +18 -18
- package/lib/Signal/Group/sender-key-message.js +45 -38
- package/lib/Signal/Group/sender-key-name.d.ts +15 -14
- package/lib/Signal/Group/sender-key-name.js +31 -20
- package/lib/Signal/Group/sender-key-record.d.ts +25 -24
- package/lib/Signal/Group/sender-key-record.js +35 -18
- package/lib/Signal/Group/sender-key-state.d.ts +34 -29
- package/lib/Signal/Group/sender-key-state.js +100 -37
- package/lib/Signal/Group/sender-message-key.d.ts +10 -11
- package/lib/Signal/Group/sender-message-key.js +22 -15
- package/lib/Signal/libsignal.d.ts +8 -5
- package/lib/Signal/libsignal.js +142 -288
- package/lib/Signal/lid-mapping.d.ts +28 -23
- package/lib/Signal/lid-mapping.js +184 -277
- package/lib/Socket/Client/index.d.ts +2 -3
- package/lib/Socket/Client/index.js +22 -3
- package/lib/Socket/Client/types.d.ts +15 -15
- package/lib/Socket/Client/types.js +15 -8
- package/lib/Socket/Client/websocket.d.ts +12 -12
- package/lib/Socket/Client/websocket.js +37 -29
- package/lib/Socket/business.d.ts +178 -208
- package/lib/Socket/business.js +127 -91
- package/lib/Socket/chats.d.ts +93 -120
- package/lib/Socket/chats.js +652 -727
- package/lib/Socket/community.js +10 -2
- package/lib/Socket/groups.d.ts +111 -143
- package/lib/Socket/groups.js +186 -165
- package/lib/Socket/index.d.ts +183 -252
- package/lib/Socket/index.js +18 -38
- package/lib/Socket/messages-recv.d.ts +167 -196
- package/lib/Socket/messages-recv.js +1567 -1443
- package/lib/Socket/messages-send.d.ts +158 -192
- package/lib/Socket/messages-send.js +1208 -1102
- package/lib/Socket/newsletter.d.ts +138 -163
- package/lib/Socket/newsletter.js +249 -153
- package/lib/Socket/socket.d.ts +36 -50
- package/lib/Socket/socket.js +820 -749
- package/lib/Store/index.d.ts +4 -4
- package/lib/Store/index.js +24 -4
- package/lib/Store/make-in-memory-store.d.ts +104 -44
- package/lib/Store/make-in-memory-store.js +213 -199
- package/lib/Store/make-ordered-dictionary.d.ts +12 -13
- package/lib/Store/make-ordered-dictionary.js +45 -38
- package/lib/Store/object-repository.d.ts +10 -11
- package/lib/Store/object-repository.js +16 -9
- package/lib/Types/Auth.d.ts +97 -93
- package/lib/Types/Auth.js +3 -2
- package/lib/Types/Bussines.js +3 -2
- package/lib/Types/Call.d.ts +13 -14
- package/lib/Types/Call.js +3 -2
- package/lib/Types/Chat.d.ts +97 -78
- package/lib/Types/Chat.js +9 -8
- package/lib/Types/Contact.d.ts +9 -12
- package/lib/Types/Contact.js +3 -2
- package/lib/Types/Events.d.ts +176 -206
- package/lib/Types/Events.js +3 -2
- package/lib/Types/GroupMetadata.d.ts +48 -53
- package/lib/Types/GroupMetadata.js +3 -2
- package/lib/Types/Label.d.ts +14 -13
- package/lib/Types/Label.js +30 -24
- package/lib/Types/LabelAssociation.d.ts +20 -15
- package/lib/Types/LabelAssociation.js +12 -6
- package/lib/Types/Message.d.ts +412 -248
- package/lib/Types/Message.js +19 -17
- package/lib/Types/Product.d.ts +71 -58
- package/lib/Types/Product.js +3 -2
- package/lib/Types/Signal.d.ts +82 -71
- package/lib/Types/Signal.js +3 -2
- package/lib/Types/Socket.d.ts +81 -76
- package/lib/Types/Socket.js +3 -3
- package/lib/Types/State.d.ts +19 -75
- package/lib/Types/State.js +14 -56
- package/lib/Types/USync.d.ts +8 -8
- package/lib/Types/USync.js +3 -2
- package/lib/Types/index.d.ts +62 -47
- package/lib/Types/index.js +50 -26
- package/lib/Utils/auth-utils.d.ts +10 -13
- package/lib/Utils/auth-utils.js +472 -213
- package/lib/Utils/business.d.ts +20 -14
- package/lib/Utils/business.js +134 -110
- package/lib/Utils/chat-utils.d.ts +70 -88
- package/lib/Utils/chat-utils.js +403 -466
- package/lib/Utils/crypto.d.ts +46 -27
- package/lib/Utils/crypto.js +188 -117
- package/lib/Utils/decode-wa-message.d.ts +45 -58
- package/lib/Utils/decode-wa-message.js +311 -212
- package/lib/Utils/event-buffer.d.ts +17 -14
- package/lib/Utils/event-buffer.js +296 -321
- package/lib/Utils/generics.d.ts +92 -66
- package/lib/Utils/generics.js +436 -270
- package/lib/Utils/history.d.ts +23 -24
- package/lib/Utils/history.js +76 -106
- package/lib/Utils/index.d.ts +21 -24
- package/lib/Utils/index.js +41 -24
- package/lib/Utils/link-preview.d.ts +14 -12
- package/lib/Utils/link-preview.js +75 -40
- package/lib/Utils/logger.d.ts +11 -10
- package/lib/Utils/logger.js +7 -3
- package/lib/Utils/lt-hash.d.ts +14 -8
- package/lib/Utils/lt-hash.js +53 -3
- package/lib/Utils/make-mutex.d.ts +7 -7
- package/lib/Utils/make-mutex.js +44 -28
- package/lib/Utils/message-retry-manager.d.ts +88 -115
- package/lib/Utils/message-retry-manager.js +160 -265
- package/lib/Utils/messages-media.d.ts +107 -105
- package/lib/Utils/messages-media.js +619 -548
- package/lib/Utils/messages.d.ts +66 -52
- package/lib/Utils/messages.js +1558 -1730
- package/lib/Utils/noise-handler.d.ts +18 -18
- package/lib/Utils/noise-handler.js +130 -176
- package/lib/Utils/process-message.d.ts +32 -43
- package/lib/Utils/process-message.js +282 -484
- package/lib/Utils/rich-message-utils.js +724 -1220
- package/lib/Utils/signal.d.ts +32 -37
- package/lib/Utils/signal.js +105 -140
- package/lib/Utils/use-multi-file-auth-state.d.ts +5 -5
- package/lib/Utils/use-multi-file-auth-state.js +120 -70
- package/lib/Utils/use-single-file-auth-state.d.ts +13 -11
- package/lib/Utils/use-single-file-auth-state.js +66 -92
- package/lib/Utils/validate-connection.d.ts +13 -11
- package/lib/Utils/validate-connection.js +113 -128
- package/lib/WABinary/constants.d.ts +27 -25
- package/lib/WABinary/constants.js +1292 -1451
- package/lib/WABinary/decode.d.ts +9 -7
- package/lib/WABinary/decode.js +189 -163
- package/lib/WABinary/encode.d.ts +3 -3
- package/lib/WABinary/encode.js +155 -110
- package/lib/WABinary/generic-utils.d.ts +27 -14
- package/lib/WABinary/generic-utils.js +105 -177
- package/lib/WABinary/index.d.ts +5 -6
- package/lib/WABinary/index.js +25 -6
- package/lib/WABinary/jid-utils.d.ts +54 -41
- package/lib/WABinary/jid-utils.js +152 -83
- package/lib/WABinary/types.d.ts +13 -10
- package/lib/WABinary/types.js +3 -2
- package/lib/WAM/BinaryInfo.d.ts +15 -8
- package/lib/WAM/BinaryInfo.js +14 -7
- package/lib/WAM/constants.d.ts +37 -30
- package/lib/WAM/constants.js +11983 -19465
- package/lib/WAM/encode.d.ts +3 -3
- package/lib/WAM/encode.js +109 -95
- package/lib/WAM/index.d.ts +3 -4
- package/lib/WAM/index.js +23 -4
- package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts +9 -9
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +21 -37
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.d.ts +22 -19
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +26 -18
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +12 -11
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +20 -12
- package/lib/WAUSync/Protocols/USyncStatusProtocol.d.ts +12 -11
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +24 -16
- package/lib/WAUSync/Protocols/index.d.ts +6 -6
- package/lib/WAUSync/Protocols/index.js +26 -6
- package/lib/WAUSync/USyncQuery.d.ts +28 -27
- package/lib/WAUSync/USyncQuery.js +62 -68
- package/lib/WAUSync/USyncUser.d.ts +10 -15
- package/lib/WAUSync/USyncUser.js +19 -20
- package/lib/WAUSync/index.d.ts +3 -4
- package/lib/WAUSync/index.js +23 -4
- package/lib/index.d.ts +12 -11
- package/lib/index.js +39 -12
- package/package.json +43 -87
- package/engine-requirements.js +0 -10
|
@@ -1,386 +1,536 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k)
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k] } }
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc)
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k
|
|
12
|
+
o[k2] = m[k]
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v })
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
22
|
+
if (mod && mod.__esModule) return mod
|
|
23
|
+
var result = {}
|
|
24
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k)
|
|
25
|
+
__setModuleDefault(result, mod)
|
|
26
|
+
return result
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
30
|
+
return (mod && mod.__esModule) ? mod : { "default": mod }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true })
|
|
34
|
+
|
|
35
|
+
const boom_1 = require("@hapi/boom")
|
|
36
|
+
const axios_1 = __importDefault(require("axios"))
|
|
37
|
+
const child_process_1 = require("child_process")
|
|
38
|
+
const Crypto = __importStar(require("crypto"))
|
|
39
|
+
const events_1 = require("events")
|
|
40
|
+
const fs_1 = require("fs")
|
|
41
|
+
const os_1 = require("os")
|
|
42
|
+
const path_1 = require("path")
|
|
43
|
+
const stream_1 = require("stream")
|
|
44
|
+
const WAProto_1 = require("../../WAProto")
|
|
45
|
+
const Defaults_1 = require("../Defaults")
|
|
46
|
+
const WABinary_1 = require("../WABinary")
|
|
47
|
+
const crypto_1 = require("./crypto")
|
|
48
|
+
const generics_1 = require("./generics")
|
|
49
|
+
|
|
50
|
+
const getImageProcessingLibrary = async () => {
|
|
51
|
+
const [_jimp, sharp] = await Promise.all([
|
|
52
|
+
(async () => {
|
|
53
|
+
const jimp = await (Promise.resolve().then(() => __importStar(require('jimp'))).catch(() => { }))
|
|
54
|
+
return jimp
|
|
55
|
+
})(),
|
|
56
|
+
(async () => {
|
|
57
|
+
const sharp = await (Promise.resolve().then(() => __importStar(require('sharp'))).catch(() => { }))
|
|
58
|
+
return sharp
|
|
59
|
+
})()
|
|
60
|
+
])
|
|
19
61
|
if (sharp) {
|
|
20
|
-
return { sharp }
|
|
62
|
+
return { sharp }
|
|
21
63
|
}
|
|
64
|
+
// Jimp v1 pakai named export (export { Jimp }), beda sama v0 yang default
|
|
65
|
+
// export. Coba named export dulu, fallback ke .default buat jaga-jaga versi lama.
|
|
66
|
+
const jimp = _jimp?.Jimp || _jimp?.default || _jimp
|
|
22
67
|
if (jimp) {
|
|
23
|
-
return { jimp }
|
|
24
|
-
}
|
|
25
|
-
throw new Boom('No image processing library available')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
68
|
+
return { jimp, jimpMime: _jimp?.JimpMime }
|
|
69
|
+
}
|
|
70
|
+
throw new boom_1.Boom('No image processing library available')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hkdfInfoKey = (type) => {
|
|
74
|
+
const hkdfInfo = Defaults_1.MEDIA_HKDF_KEY_MAPPING[type]
|
|
75
|
+
return `WhatsApp ${hkdfInfo} Keys`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const getRawMediaUploadData = async (media, mediaType, logger) => {
|
|
79
|
+
const { stream } = await getStream(media)
|
|
80
|
+
|
|
81
|
+
logger?.debug('got stream for raw upload')
|
|
82
|
+
|
|
83
|
+
const hasher = Crypto.createHash('sha256')
|
|
84
|
+
const filePath = path_1.join(os_1.tmpdir(), mediaType + generics_1.generateMessageID())
|
|
85
|
+
const fileWriteStream = fs_1.createWriteStream(filePath)
|
|
86
|
+
|
|
87
|
+
let fileLength = 0
|
|
88
|
+
|
|
38
89
|
try {
|
|
39
90
|
for await (const data of stream) {
|
|
40
|
-
fileLength += data.length
|
|
41
|
-
hasher.update(data)
|
|
91
|
+
fileLength += data.length
|
|
92
|
+
hasher.update(data)
|
|
93
|
+
|
|
42
94
|
if (!fileWriteStream.write(data)) {
|
|
43
|
-
await once(fileWriteStream, 'drain')
|
|
95
|
+
await events_1.once(fileWriteStream, 'drain')
|
|
44
96
|
}
|
|
45
97
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
98
|
+
|
|
99
|
+
fileWriteStream.end()
|
|
100
|
+
await events_1.once(fileWriteStream, 'finish')
|
|
101
|
+
stream.destroy()
|
|
102
|
+
|
|
103
|
+
const fileSha256 = hasher.digest()
|
|
104
|
+
|
|
105
|
+
logger?.debug('hashed data for raw upload')
|
|
106
|
+
|
|
51
107
|
return {
|
|
52
108
|
filePath: filePath,
|
|
53
109
|
fileSha256,
|
|
54
110
|
fileLength
|
|
55
|
-
}
|
|
111
|
+
}
|
|
56
112
|
}
|
|
57
113
|
catch (error) {
|
|
58
|
-
fileWriteStream.destroy()
|
|
59
|
-
stream.destroy()
|
|
114
|
+
fileWriteStream.destroy()
|
|
115
|
+
stream.destroy()
|
|
116
|
+
|
|
60
117
|
try {
|
|
61
|
-
await
|
|
118
|
+
await fs_1.promises.unlink(filePath)
|
|
62
119
|
}
|
|
63
120
|
catch {
|
|
64
121
|
//
|
|
65
122
|
}
|
|
66
|
-
throw error
|
|
123
|
+
throw error
|
|
67
124
|
}
|
|
68
|
-
}
|
|
125
|
+
}
|
|
126
|
+
|
|
69
127
|
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
|
70
|
-
|
|
128
|
+
async function getMediaKeys(buffer, mediaType) {
|
|
71
129
|
if (!buffer) {
|
|
72
|
-
throw new Boom('Cannot derive from empty media key')
|
|
130
|
+
throw new boom_1.Boom('Cannot derive from empty media key')
|
|
73
131
|
}
|
|
74
132
|
if (typeof buffer === 'string') {
|
|
75
|
-
buffer = Buffer.from(buffer.replace('data
|
|
133
|
+
buffer = Buffer.from(buffer.replace('data:base64,', ''), 'base64')
|
|
76
134
|
}
|
|
77
135
|
// expand using HKDF to 112 bytes, also pass in the relevant app info
|
|
78
|
-
const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
|
|
136
|
+
const expandedMediaKey = await crypto_1.hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
|
|
79
137
|
return {
|
|
80
138
|
iv: expandedMediaKey.slice(0, 16),
|
|
81
139
|
cipherKey: expandedMediaKey.slice(16, 48),
|
|
82
|
-
macKey: expandedMediaKey.slice(48, 80)
|
|
83
|
-
}
|
|
140
|
+
macKey: expandedMediaKey.slice(48, 80),
|
|
141
|
+
}
|
|
84
142
|
}
|
|
143
|
+
|
|
85
144
|
/** Extracts video thumb using FFMPEG */
|
|
86
|
-
const extractVideoThumb =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
145
|
+
const extractVideoThumb = (videoPath, time = '00:00:00', size = { width: 320 }) => {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const args = [
|
|
148
|
+
'-ss', time,
|
|
149
|
+
'-i', videoPath,
|
|
150
|
+
'-y',
|
|
151
|
+
'-vf', `scale=${size.width}:-1`,
|
|
152
|
+
'-vframes', '1',
|
|
153
|
+
'-f', 'image2',
|
|
154
|
+
'-vcodec', 'mjpeg',
|
|
155
|
+
'pipe:1'
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
const ffmpeg = child_process_1.spawn('ffmpeg', args)
|
|
159
|
+
const chunks = []
|
|
160
|
+
let errorOutput = ''
|
|
161
|
+
|
|
162
|
+
ffmpeg.stdout.on('data', chunk => chunks.push(chunk))
|
|
163
|
+
ffmpeg.stderr.on('data', data => {
|
|
164
|
+
errorOutput += data.toString()
|
|
165
|
+
})
|
|
166
|
+
ffmpeg.on('error', reject)
|
|
167
|
+
ffmpeg.on('close', code => {
|
|
168
|
+
if (code === 0) return resolve(Buffer.concat(chunks))
|
|
169
|
+
reject(new Error(`ffmpeg exited with code ${code}\n${errorOutput}`))
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const extractImageThumb = async (bufferOrFilePath, width = 32, quality = 50) => {
|
|
175
|
+
if (typeof bufferOrFilePath === "string" && bufferOrFilePath.startsWith("http")) {
|
|
176
|
+
const response = await axios_1.default.get(bufferOrFilePath, { responseType: "arraybuffer" })
|
|
177
|
+
bufferOrFilePath = Buffer.from(response.data)
|
|
178
|
+
}
|
|
179
|
+
if (bufferOrFilePath instanceof stream_1.Readable) {
|
|
180
|
+
bufferOrFilePath = await toBuffer(bufferOrFilePath)
|
|
181
|
+
}
|
|
182
|
+
const lib = await getImageProcessingLibrary()
|
|
104
183
|
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
|
|
105
|
-
const img = lib.sharp.default(bufferOrFilePath)
|
|
106
|
-
const dimensions = await img.metadata()
|
|
107
|
-
const buffer = await img
|
|
184
|
+
const img = lib.sharp.default(bufferOrFilePath)
|
|
185
|
+
const dimensions = await img.metadata()
|
|
186
|
+
const buffer = await img
|
|
187
|
+
.resize({
|
|
188
|
+
width,
|
|
189
|
+
height: width,
|
|
190
|
+
fit: 'contain',
|
|
191
|
+
background: { r: 255, g: 255, b: 255, alpha: 0 }
|
|
192
|
+
})
|
|
193
|
+
.jpeg({ quality })
|
|
194
|
+
.toBuffer()
|
|
108
195
|
return {
|
|
109
196
|
buffer,
|
|
110
197
|
original: {
|
|
111
198
|
width: dimensions.width,
|
|
112
|
-
height: dimensions.height
|
|
113
|
-
}
|
|
114
|
-
}
|
|
199
|
+
height: dimensions.height,
|
|
200
|
+
},
|
|
201
|
+
}
|
|
115
202
|
}
|
|
116
|
-
else if ('jimp' in lib && typeof lib.jimp?.
|
|
117
|
-
const
|
|
203
|
+
else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
|
|
204
|
+
const Jimp = lib.jimp
|
|
205
|
+
const JimpMime = lib.jimpMime
|
|
206
|
+
const jimp = await Jimp.read(bufferOrFilePath)
|
|
118
207
|
const dimensions = {
|
|
119
208
|
width: jimp.width,
|
|
120
209
|
height: jimp.height
|
|
121
|
-
}
|
|
210
|
+
}
|
|
122
211
|
const buffer = await jimp
|
|
123
|
-
|
|
124
|
-
|
|
212
|
+
.resize({ w: width })
|
|
213
|
+
.getBuffer(JimpMime?.jpeg || 'image/jpeg', { quality })
|
|
125
214
|
return {
|
|
126
215
|
buffer,
|
|
127
216
|
original: dimensions
|
|
128
|
-
}
|
|
217
|
+
}
|
|
129
218
|
}
|
|
130
219
|
else {
|
|
131
|
-
throw new Boom('No image processing library available')
|
|
220
|
+
throw new boom_1.Boom('No image processing library available')
|
|
132
221
|
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const encodeBase64EncodedStringForUpload = (b64) => (encodeURIComponent(b64
|
|
225
|
+
.replace(/\+/g, '-')
|
|
226
|
+
.replace(/\//g, '_')
|
|
227
|
+
.replace(/\=+$/, '')))
|
|
228
|
+
|
|
229
|
+
const generateProfilePicture = async (mediaUpload) => {
|
|
230
|
+
let bufferOrFilePath
|
|
138
231
|
if (Buffer.isBuffer(mediaUpload)) {
|
|
139
|
-
|
|
232
|
+
bufferOrFilePath = mediaUpload
|
|
233
|
+
}
|
|
234
|
+
else if ('url' in mediaUpload) {
|
|
235
|
+
bufferOrFilePath = mediaUpload.url.toString()
|
|
140
236
|
}
|
|
141
237
|
else {
|
|
142
|
-
|
|
143
|
-
const { stream } = await getStream(mediaUpload);
|
|
144
|
-
// Convert the resulting stream to a buffer
|
|
145
|
-
buffer = await toBuffer(stream);
|
|
238
|
+
bufferOrFilePath = await toBuffer(mediaUpload.stream)
|
|
146
239
|
}
|
|
147
|
-
const lib = await getImageProcessingLibrary()
|
|
148
|
-
let img
|
|
240
|
+
const lib = await getImageProcessingLibrary()
|
|
241
|
+
let img
|
|
149
242
|
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
|
|
150
|
-
img = lib.sharp
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
.jpeg({
|
|
154
|
-
quality: 50
|
|
243
|
+
img = await lib.sharp.default(bufferOrFilePath)
|
|
244
|
+
.resize(720, 720, {
|
|
245
|
+
fit: 'inside',
|
|
155
246
|
})
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
247
|
+
.jpeg({ quality: 50 })
|
|
248
|
+
.toBuffer()
|
|
249
|
+
}
|
|
250
|
+
else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
|
|
251
|
+
const Jimp = lib.jimp
|
|
252
|
+
const JimpMime = lib.jimpMime
|
|
253
|
+
const image = await Jimp.read(bufferOrFilePath)
|
|
254
|
+
const min = image.width
|
|
255
|
+
const max = image.height
|
|
256
|
+
const cropped = image.crop({ x: 0, y: 0, w: min, h: max })
|
|
257
|
+
img = await cropped.scaleToFit({ w: 720, h: 720 }).getBuffer(JimpMime?.jpeg || 'image/jpeg')
|
|
163
258
|
}
|
|
164
259
|
else {
|
|
165
|
-
throw new Boom('No image processing library available')
|
|
260
|
+
throw new boom_1.Boom('No image processing library available')
|
|
166
261
|
}
|
|
167
262
|
return {
|
|
168
|
-
img: await img
|
|
169
|
-
}
|
|
170
|
-
}
|
|
263
|
+
img: await img,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
171
268
|
/** gets the SHA256 of the given media message */
|
|
172
|
-
|
|
173
|
-
const media = Object.values(message)[0]
|
|
174
|
-
return media
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
269
|
+
const mediaMessageSHA256B64 = (message) => {
|
|
270
|
+
const media = Object.values(message)[0]
|
|
271
|
+
return (media === null || media === void 0 ? void 0 : media.fileSha256) && Buffer.from(media.fileSha256).toString('base64')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function getAudioDuration(buffer) {
|
|
275
|
+
const musicMetadata = await Promise.resolve().then(() => __importStar(require('music-metadata')))
|
|
179
276
|
const options = {
|
|
180
|
-
|
|
181
|
-
}
|
|
277
|
+
duration: true
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let metadata
|
|
281
|
+
|
|
182
282
|
if (Buffer.isBuffer(buffer)) {
|
|
183
|
-
metadata = await musicMetadata.parseBuffer(buffer, undefined, options)
|
|
283
|
+
metadata = await musicMetadata.parseBuffer(buffer, undefined, options)
|
|
184
284
|
}
|
|
185
285
|
else if (typeof buffer === 'string') {
|
|
186
|
-
metadata = await musicMetadata.parseFile(buffer, options)
|
|
286
|
+
metadata = await musicMetadata.parseFile(buffer, options)
|
|
187
287
|
}
|
|
188
288
|
else {
|
|
189
|
-
metadata = await musicMetadata.parseStream(buffer, undefined, options)
|
|
289
|
+
metadata = await musicMetadata.parseStream(buffer, undefined, options)
|
|
190
290
|
}
|
|
191
|
-
return metadata.format.duration
|
|
291
|
+
return metadata.format.duration
|
|
192
292
|
}
|
|
293
|
+
|
|
193
294
|
/**
|
|
194
295
|
referenced from and modifying https://github.com/wppconnect-team/wa-js/blob/main/src/chat/functions/prepareAudioWaveform.ts
|
|
195
296
|
*/
|
|
196
|
-
|
|
297
|
+
async function getAudioWaveform(buffer, logger) {
|
|
197
298
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
let audioData;
|
|
299
|
+
const { default: decoder } = await eval('import(\'audio-decode\')')
|
|
300
|
+
let audioData
|
|
201
301
|
if (Buffer.isBuffer(buffer)) {
|
|
202
|
-
audioData = buffer
|
|
302
|
+
audioData = buffer
|
|
203
303
|
}
|
|
204
304
|
else if (typeof buffer === 'string') {
|
|
205
|
-
const rStream = createReadStream(buffer)
|
|
206
|
-
audioData = await toBuffer(rStream)
|
|
305
|
+
const rStream = fs_1.createReadStream(buffer)
|
|
306
|
+
audioData = await toBuffer(rStream)
|
|
207
307
|
}
|
|
208
308
|
else {
|
|
209
|
-
audioData = await toBuffer(buffer)
|
|
309
|
+
audioData = await toBuffer(buffer)
|
|
210
310
|
}
|
|
211
|
-
const audioBuffer = await decoder(audioData)
|
|
212
|
-
const rawData = audioBuffer.getChannelData(0)
|
|
213
|
-
const samples = 64
|
|
214
|
-
const blockSize = Math.floor(rawData.length / samples)
|
|
215
|
-
const filteredData = []
|
|
311
|
+
const audioBuffer = await decoder(audioData)
|
|
312
|
+
const rawData = audioBuffer.getChannelData(0) // We only need to work with one channel of data
|
|
313
|
+
const samples = 64 // Number of samples we want to have in our final data set
|
|
314
|
+
const blockSize = Math.floor(rawData.length / samples) // the number of samples in each subdivision
|
|
315
|
+
const filteredData = []
|
|
216
316
|
for (let i = 0; i < samples; i++) {
|
|
217
|
-
const blockStart = blockSize * i
|
|
218
|
-
let sum = 0
|
|
317
|
+
const blockStart = blockSize * i // the location of the first sample in the block
|
|
318
|
+
let sum = 0
|
|
219
319
|
for (let j = 0; j < blockSize; j++) {
|
|
220
|
-
sum = sum + Math.abs(rawData[blockStart + j])
|
|
320
|
+
sum = sum + Math.abs(rawData[blockStart + j]) // find the sum of all the samples in the block
|
|
221
321
|
}
|
|
222
|
-
filteredData.push(sum / blockSize)
|
|
322
|
+
filteredData.push(sum / blockSize) // divide the sum by the block size to get the average
|
|
223
323
|
}
|
|
224
324
|
// This guarantees that the largest data point will be set to 1, and the rest of the data will scale proportionally.
|
|
225
|
-
const multiplier = Math.pow(Math.max(...filteredData), -1)
|
|
226
|
-
const normalizedData = filteredData.map(n => n * multiplier)
|
|
325
|
+
const multiplier = Math.pow(Math.max(...filteredData), -1)
|
|
326
|
+
const normalizedData = filteredData.map((n) => n * multiplier)
|
|
227
327
|
// Generate waveform like WhatsApp
|
|
228
|
-
const waveform = new Uint8Array(normalizedData.map(n => Math.floor(100 * n)))
|
|
229
|
-
return waveform
|
|
328
|
+
const waveform = new Uint8Array(normalizedData.map((n) => Math.floor(100 * n)))
|
|
329
|
+
return waveform
|
|
230
330
|
}
|
|
231
331
|
catch (e) {
|
|
232
|
-
logger?.debug('Failed to generate waveform: ' + e)
|
|
332
|
+
logger?.debug('Failed to generate waveform: ' + e)
|
|
233
333
|
}
|
|
234
334
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
readable.
|
|
238
|
-
readable.push(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
335
|
+
|
|
336
|
+
const toReadable = (buffer) => {
|
|
337
|
+
const readable = new stream_1.Readable({ read: () => { } })
|
|
338
|
+
readable.push(buffer)
|
|
339
|
+
readable.push(null)
|
|
340
|
+
return readable
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const toBuffer = async (stream) => {
|
|
344
|
+
const chunks = []
|
|
243
345
|
for await (const chunk of stream) {
|
|
244
|
-
chunks.push(chunk)
|
|
346
|
+
chunks.push(chunk)
|
|
245
347
|
}
|
|
246
|
-
stream.destroy()
|
|
247
|
-
return Buffer.concat(chunks)
|
|
248
|
-
}
|
|
249
|
-
|
|
348
|
+
stream.destroy()
|
|
349
|
+
return Buffer.concat(chunks)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const getStream = async (item, opts) => {
|
|
250
353
|
if (Buffer.isBuffer(item)) {
|
|
251
|
-
return { stream: toReadable(item), type: 'buffer' }
|
|
354
|
+
return { stream: toReadable(item), type: 'buffer' }
|
|
252
355
|
}
|
|
356
|
+
|
|
253
357
|
if ('stream' in item) {
|
|
254
|
-
return { stream: item.stream, type: 'readable' }
|
|
358
|
+
return { stream: item.stream, type: 'readable' }
|
|
255
359
|
}
|
|
256
|
-
|
|
360
|
+
|
|
361
|
+
const urlStr = item.url.toString()
|
|
362
|
+
|
|
257
363
|
if (urlStr.startsWith('data:')) {
|
|
258
|
-
const buffer = Buffer.from(urlStr.split(',')[1], 'base64')
|
|
259
|
-
return { stream: toReadable(buffer), type: 'buffer' }
|
|
364
|
+
const buffer = Buffer.from(urlStr.split(',')[1], 'base64')
|
|
365
|
+
return { stream: await toReadable(buffer), type: 'buffer' }
|
|
260
366
|
}
|
|
367
|
+
|
|
261
368
|
if (urlStr.startsWith('http://') || urlStr.startsWith('https://')) {
|
|
262
|
-
return { stream: await getHttpStream(item.url, opts), type: 'remote' }
|
|
369
|
+
return { stream: await getHttpStream(item.url, opts), type: 'remote' }
|
|
263
370
|
}
|
|
264
|
-
|
|
265
|
-
}
|
|
371
|
+
|
|
372
|
+
return { stream: fs_1.createReadStream(item.url), type: 'file' }
|
|
373
|
+
}
|
|
374
|
+
|
|
266
375
|
/** generates a thumbnail for a given media, if required */
|
|
267
|
-
|
|
268
|
-
let thumbnail
|
|
269
|
-
let originalImageDimensions
|
|
376
|
+
async function generateThumbnail(file, mediaType, options) {
|
|
377
|
+
let thumbnail
|
|
378
|
+
let originalImageDimensions
|
|
270
379
|
if (mediaType === 'image') {
|
|
271
|
-
const { buffer, original } = await extractImageThumb(file)
|
|
272
|
-
thumbnail = buffer.toString('base64')
|
|
380
|
+
const { buffer, original } = await extractImageThumb(file, 256, 95)
|
|
381
|
+
thumbnail = buffer.toString('base64')
|
|
273
382
|
if (original.width && original.height) {
|
|
274
383
|
originalImageDimensions = {
|
|
275
384
|
width: original.width,
|
|
276
|
-
height: original.height
|
|
277
|
-
}
|
|
385
|
+
height: original.height,
|
|
386
|
+
}
|
|
278
387
|
}
|
|
279
388
|
}
|
|
280
389
|
else if (mediaType === 'video') {
|
|
281
|
-
const imgFilename = join(getTmpFilesDirectory(), generateMessageIDV2() + '.jpg');
|
|
282
390
|
try {
|
|
283
|
-
await extractVideoThumb(file,
|
|
284
|
-
|
|
285
|
-
thumbnail = buff.toString('base64');
|
|
286
|
-
await fs.unlink(imgFilename);
|
|
391
|
+
const buff = await extractVideoThumb(file, '00:00:00', { width: 32, height: 32 })
|
|
392
|
+
thumbnail = buff.toString('base64')
|
|
287
393
|
}
|
|
288
394
|
catch (err) {
|
|
289
|
-
options
|
|
395
|
+
options?.logger?.debug('could not generate video thumb: ' + err)
|
|
290
396
|
}
|
|
291
397
|
}
|
|
292
398
|
return {
|
|
293
399
|
thumbnail,
|
|
294
400
|
originalImageDimensions
|
|
295
|
-
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const getHttpStream = async (url, options = {}) => {
|
|
405
|
+
const fetched = await axios_1.default.get(url.toString(), { ...options, responseType: 'stream' })
|
|
406
|
+
return fetched.data
|
|
296
407
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
408
|
+
|
|
409
|
+
const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
|
|
410
|
+
const { stream, type } = await getStream(media, opts)
|
|
411
|
+
logger?.debug('fetched media stream')
|
|
412
|
+
|
|
413
|
+
const encFilePath = path_1.join(os_1.tmpdir(), mediaType + generics_1.generateMessageID() + '-plain')
|
|
414
|
+
const encFileWriteStream = fs_1.createWriteStream(encFilePath)
|
|
415
|
+
|
|
416
|
+
let originalFilePath
|
|
417
|
+
let originalFileStream
|
|
418
|
+
|
|
419
|
+
if (type === 'file') {
|
|
420
|
+
originalFilePath = media.url.toString()
|
|
421
|
+
} else if (saveOriginalFileIfRequired) {
|
|
422
|
+
originalFilePath = path_1.join(os_1.tmpdir(), mediaType + generics_1.generateMessageID() + '-original')
|
|
423
|
+
originalFileStream = fs_1.createWriteStream(originalFilePath)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let fileLength = 0
|
|
427
|
+
const sha256 = Crypto.createHash('sha256')
|
|
428
|
+
|
|
301
429
|
try {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
let fileLength = 0;
|
|
338
|
-
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv);
|
|
339
|
-
const hmac = Crypto.createHmac('sha256', macKey).update(iv);
|
|
340
|
-
const sha256Plain = Crypto.createHash('sha256');
|
|
341
|
-
const sha256Enc = Crypto.createHash('sha256');
|
|
342
|
-
const onChunk = async (buff) => {
|
|
343
|
-
sha256Enc.update(buff);
|
|
344
|
-
hmac.update(buff);
|
|
345
|
-
// Handle backpressure: if write returns false, wait for drain
|
|
346
|
-
if (!encFileWriteStream.write(buff)) {
|
|
347
|
-
await once(encFileWriteStream, 'drain');
|
|
430
|
+
for await (const data of stream) {
|
|
431
|
+
fileLength += data.length
|
|
432
|
+
|
|
433
|
+
if (type === 'remote'
|
|
434
|
+
&& opts?.maxContentLength
|
|
435
|
+
&& fileLength + data.length > opts.maxContentLength) {
|
|
436
|
+
throw new boom_1.Boom(`content length exceeded when preparing "${type}"`, {
|
|
437
|
+
data: { media, type }
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
sha256.update(data)
|
|
442
|
+
encFileWriteStream.write(data)
|
|
443
|
+
|
|
444
|
+
if (originalFileStream && !originalFileStream.write(data)) {
|
|
445
|
+
await events_1.once(originalFileStream, 'drain')
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const fileSha256 = sha256.digest()
|
|
450
|
+
encFileWriteStream.end()
|
|
451
|
+
originalFileStream?.end?.call(originalFileStream)
|
|
452
|
+
stream.destroy()
|
|
453
|
+
|
|
454
|
+
logger?.debug('prepared plain stream successfully')
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
mediaKey: undefined,
|
|
458
|
+
originalFilePath,
|
|
459
|
+
encFilePath,
|
|
460
|
+
mac: undefined,
|
|
461
|
+
fileEncSha256: undefined,
|
|
462
|
+
fileSha256,
|
|
463
|
+
fileLength
|
|
348
464
|
}
|
|
349
|
-
}
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
encFileWriteStream.destroy()
|
|
468
|
+
originalFileStream?.destroy?.call(originalFileStream)
|
|
469
|
+
sha256.destroy()
|
|
470
|
+
stream.destroy()
|
|
471
|
+
try {
|
|
472
|
+
await fs_1.promises.unlink(encFilePath)
|
|
473
|
+
if (originalFilePath && didSaveToTmpPath) {
|
|
474
|
+
await fs_1.promises.unlink(originalFilePath)
|
|
475
|
+
}
|
|
476
|
+
} catch (err) {
|
|
477
|
+
logger?.error({ err }, 'failed deleting tmp files')
|
|
478
|
+
}
|
|
479
|
+
throw error
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
|
|
484
|
+
const { stream, type } = await getStream(media, opts)
|
|
485
|
+
logger?.debug('fetched media stream')
|
|
486
|
+
const mediaKey = Crypto.randomBytes(32)
|
|
487
|
+
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType)
|
|
488
|
+
const encFilePath = path_1.join(os_1.tmpdir(), mediaType + generics_1.generateMessageID() + '-enc')
|
|
489
|
+
const encFileWriteStream = fs_1.createWriteStream(encFilePath)
|
|
490
|
+
let originalFileStream
|
|
491
|
+
let originalFilePath
|
|
492
|
+
if (saveOriginalFileIfRequired) {
|
|
493
|
+
originalFilePath = path_1.join(os_1.tmpdir(), mediaType + generics_1.generateMessageID() + '-original')
|
|
494
|
+
originalFileStream = fs_1.createWriteStream(originalFilePath)
|
|
495
|
+
}
|
|
496
|
+
let fileLength = 0
|
|
497
|
+
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv)
|
|
498
|
+
const hmac = Crypto.createHmac('sha256', macKey).update(iv)
|
|
499
|
+
const sha256Plain = Crypto.createHash('sha256')
|
|
500
|
+
const sha256Enc = Crypto.createHash('sha256')
|
|
501
|
+
const onChunk = (buff) => {
|
|
502
|
+
sha256Enc.update(buff)
|
|
503
|
+
hmac.update(buff)
|
|
504
|
+
encFileWriteStream.write(buff)
|
|
505
|
+
}
|
|
350
506
|
try {
|
|
351
507
|
for await (const data of stream) {
|
|
352
|
-
fileLength += data.length
|
|
353
|
-
if (type === 'remote'
|
|
354
|
-
opts?.maxContentLength
|
|
355
|
-
fileLength + data.length > opts.maxContentLength) {
|
|
356
|
-
throw new Boom(`content length exceeded when encrypting "${type}"`, {
|
|
508
|
+
fileLength += data.length
|
|
509
|
+
if (type === 'remote'
|
|
510
|
+
&& opts?.maxContentLength
|
|
511
|
+
&& fileLength + data.length > opts.maxContentLength) {
|
|
512
|
+
throw new boom_1.Boom(`content length exceeded when encrypting "${type}"`, {
|
|
357
513
|
data: { media, type }
|
|
358
|
-
})
|
|
514
|
+
})
|
|
359
515
|
}
|
|
360
516
|
if (originalFileStream) {
|
|
361
517
|
if (!originalFileStream.write(data)) {
|
|
362
|
-
await once(originalFileStream, 'drain')
|
|
518
|
+
await events_1.once(originalFileStream, 'drain')
|
|
363
519
|
}
|
|
364
520
|
}
|
|
365
|
-
sha256Plain.update(data)
|
|
366
|
-
|
|
521
|
+
sha256Plain.update(data)
|
|
522
|
+
onChunk(aes.update(data))
|
|
367
523
|
}
|
|
368
|
-
|
|
369
|
-
const mac = hmac.digest().slice(0, 10)
|
|
370
|
-
sha256Enc.update(mac)
|
|
371
|
-
const fileSha256 = sha256Plain.digest()
|
|
372
|
-
const fileEncSha256 = sha256Enc.digest()
|
|
373
|
-
encFileWriteStream.write(mac)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
stream.destroy();
|
|
379
|
-
// Wait for write streams to fully flush to disk
|
|
380
|
-
// This helps reduce memory pressure by allowing OS to release buffers
|
|
381
|
-
await encFinishPromise;
|
|
382
|
-
await originalFinishPromise;
|
|
383
|
-
logger?.debug('encrypted data successfully');
|
|
524
|
+
onChunk(aes.final())
|
|
525
|
+
const mac = hmac.digest().slice(0, 10)
|
|
526
|
+
sha256Enc.update(mac)
|
|
527
|
+
const fileSha256 = sha256Plain.digest()
|
|
528
|
+
const fileEncSha256 = sha256Enc.digest()
|
|
529
|
+
encFileWriteStream.write(mac)
|
|
530
|
+
encFileWriteStream.end()
|
|
531
|
+
originalFileStream?.end?.call(originalFileStream)
|
|
532
|
+
stream.destroy()
|
|
533
|
+
logger?.debug('encrypted data successfully')
|
|
384
534
|
return {
|
|
385
535
|
mediaKey,
|
|
386
536
|
originalFilePath,
|
|
@@ -389,345 +539,235 @@ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFi
|
|
|
389
539
|
fileEncSha256,
|
|
390
540
|
fileSha256,
|
|
391
541
|
fileLength
|
|
392
|
-
}
|
|
542
|
+
}
|
|
393
543
|
}
|
|
394
544
|
catch (error) {
|
|
395
545
|
// destroy all streams with error
|
|
396
|
-
encFileWriteStream.destroy()
|
|
397
|
-
originalFileStream?.destroy?.()
|
|
398
|
-
aes.destroy()
|
|
399
|
-
hmac.destroy()
|
|
400
|
-
sha256Plain.destroy()
|
|
401
|
-
sha256Enc.destroy()
|
|
402
|
-
stream.destroy()
|
|
546
|
+
encFileWriteStream.destroy()
|
|
547
|
+
originalFileStream?.destroy?.call(originalFileStream)
|
|
548
|
+
aes.destroy()
|
|
549
|
+
hmac.destroy()
|
|
550
|
+
sha256Plain.destroy()
|
|
551
|
+
sha256Enc.destroy()
|
|
552
|
+
stream.destroy()
|
|
403
553
|
try {
|
|
404
|
-
await
|
|
554
|
+
await fs_1.promises.unlink(encFilePath)
|
|
405
555
|
if (originalFilePath) {
|
|
406
|
-
await
|
|
556
|
+
await fs_1.promises.unlink(originalFilePath)
|
|
407
557
|
}
|
|
408
558
|
}
|
|
409
559
|
catch (err) {
|
|
410
|
-
logger?.error({ err }, 'failed deleting tmp files')
|
|
560
|
+
logger?.error({ err }, 'failed deleting tmp files')
|
|
411
561
|
}
|
|
412
|
-
throw error
|
|
562
|
+
throw error
|
|
413
563
|
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const DEF_HOST = 'mmg.whatsapp.net'
|
|
567
|
+
|
|
568
|
+
const AES_CHUNK_SIZE = 16
|
|
569
|
+
|
|
417
570
|
const toSmallestChunkSize = (num) => {
|
|
418
|
-
return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
return undefined;
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
export const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
|
|
432
|
-
// Fallback host: explicit opt > host parsed from `url` > DEF_MEDIA_HOST.
|
|
433
|
-
// Lets us honor a non-default host carried by the proto without forcing callers to thread it through.
|
|
434
|
-
const fallbackHost = opts.host ?? extractHost(url);
|
|
435
|
-
const downloadUrl = directPath ? getUrlFromDirectPath(directPath, fallbackHost) : url;
|
|
571
|
+
return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE
|
|
572
|
+
}
|
|
573
|
+
const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`
|
|
574
|
+
|
|
575
|
+
const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
|
|
576
|
+
const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/')
|
|
577
|
+
const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath)
|
|
578
|
+
|
|
436
579
|
if (!downloadUrl) {
|
|
437
|
-
|
|
580
|
+
throw new boom_1.Boom('No valid media URL or directPath present in message', { statusCode: 400 })
|
|
438
581
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
582
|
+
|
|
583
|
+
const keys = await getMediaKeys(mediaKey, type)
|
|
584
|
+
return downloadEncryptedContent(downloadUrl, keys, opts)
|
|
585
|
+
}
|
|
586
|
+
|
|
442
587
|
/**
|
|
443
588
|
* Decrypts and downloads an AES256-CBC encrypted file given the keys.
|
|
444
589
|
* Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
|
|
445
590
|
* */
|
|
446
|
-
|
|
447
|
-
let bytesFetched = 0
|
|
448
|
-
let startChunk = 0
|
|
449
|
-
let firstBlockIsIV = false
|
|
591
|
+
const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
|
|
592
|
+
let bytesFetched = 0
|
|
593
|
+
let startChunk = 0
|
|
594
|
+
let firstBlockIsIV = false
|
|
450
595
|
// if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
|
|
451
596
|
if (startByte) {
|
|
452
|
-
const chunk = toSmallestChunkSize(startByte || 0)
|
|
597
|
+
const chunk = toSmallestChunkSize(startByte || 0)
|
|
453
598
|
if (chunk) {
|
|
454
|
-
startChunk = chunk - AES_CHUNK_SIZE
|
|
455
|
-
bytesFetched = chunk
|
|
456
|
-
firstBlockIsIV = true
|
|
599
|
+
startChunk = chunk - AES_CHUNK_SIZE
|
|
600
|
+
bytesFetched = chunk
|
|
601
|
+
firstBlockIsIV = true
|
|
457
602
|
}
|
|
458
603
|
}
|
|
459
|
-
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined
|
|
460
|
-
const headersInit = options?.headers ? options.headers : undefined;
|
|
604
|
+
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined
|
|
461
605
|
const headers = {
|
|
462
|
-
...(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
: headersInit
|
|
466
|
-
: {}),
|
|
467
|
-
Origin: DEFAULT_ORIGIN
|
|
468
|
-
};
|
|
606
|
+
...(options?.headers) || {},
|
|
607
|
+
Origin: Defaults_1.DEFAULT_ORIGIN,
|
|
608
|
+
}
|
|
469
609
|
if (startChunk || endChunk) {
|
|
470
|
-
headers.Range = `bytes=${startChunk}
|
|
610
|
+
headers.Range = `bytes=${startChunk}-`
|
|
471
611
|
if (endChunk) {
|
|
472
|
-
headers.Range += endChunk
|
|
612
|
+
headers.Range += endChunk
|
|
473
613
|
}
|
|
474
614
|
}
|
|
475
615
|
// download the message
|
|
476
616
|
const fetched = await getHttpStream(downloadUrl, {
|
|
477
|
-
...
|
|
478
|
-
headers
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
617
|
+
...options || {},
|
|
618
|
+
headers,
|
|
619
|
+
maxBodyLength: Infinity,
|
|
620
|
+
maxContentLength: Infinity,
|
|
621
|
+
})
|
|
622
|
+
let remainingBytes = Buffer.from([])
|
|
623
|
+
let aes
|
|
482
624
|
const pushBytes = (bytes, push) => {
|
|
483
625
|
if (startByte || endByte) {
|
|
484
|
-
const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0)
|
|
485
|
-
const end = bytesFetched + bytes.length < endByte ? undefined : Math.max(endByte - bytesFetched, 0)
|
|
486
|
-
push(bytes.slice(start, end))
|
|
487
|
-
bytesFetched += bytes.length
|
|
626
|
+
const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0)
|
|
627
|
+
const end = bytesFetched + bytes.length < endByte ? undefined : Math.max(endByte - bytesFetched, 0)
|
|
628
|
+
push(bytes.slice(start, end))
|
|
629
|
+
bytesFetched += bytes.length
|
|
488
630
|
}
|
|
489
631
|
else {
|
|
490
|
-
push(bytes)
|
|
632
|
+
push(bytes)
|
|
491
633
|
}
|
|
492
|
-
}
|
|
493
|
-
const output = new Transform({
|
|
634
|
+
}
|
|
635
|
+
const output = new stream_1.Transform({
|
|
494
636
|
transform(chunk, _, callback) {
|
|
495
|
-
let data =
|
|
496
|
-
const decryptLength = toSmallestChunkSize(data.length)
|
|
497
|
-
remainingBytes = data.slice(decryptLength)
|
|
498
|
-
data = data.slice(0, decryptLength)
|
|
637
|
+
let data = Buffer.concat([remainingBytes, chunk])
|
|
638
|
+
const decryptLength = toSmallestChunkSize(data.length)
|
|
639
|
+
remainingBytes = data.slice(decryptLength)
|
|
640
|
+
data = data.slice(0, decryptLength)
|
|
499
641
|
if (!aes) {
|
|
500
|
-
let ivValue = iv
|
|
642
|
+
let ivValue = iv
|
|
501
643
|
if (firstBlockIsIV) {
|
|
502
|
-
ivValue = data.slice(0, AES_CHUNK_SIZE)
|
|
503
|
-
data = data.slice(AES_CHUNK_SIZE)
|
|
644
|
+
ivValue = data.slice(0, AES_CHUNK_SIZE)
|
|
645
|
+
data = data.slice(AES_CHUNK_SIZE)
|
|
504
646
|
}
|
|
505
|
-
aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue)
|
|
647
|
+
aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue)
|
|
506
648
|
// if an end byte that is not EOF is specified
|
|
507
649
|
// stop auto padding (PKCS7) -- otherwise throws an error for decryption
|
|
508
650
|
if (endByte) {
|
|
509
|
-
aes.setAutoPadding(false)
|
|
651
|
+
aes.setAutoPadding(false)
|
|
510
652
|
}
|
|
511
653
|
}
|
|
512
654
|
try {
|
|
513
|
-
pushBytes(aes.update(data), b => this.push(b))
|
|
514
|
-
callback()
|
|
655
|
+
pushBytes(aes.update(data), b => this.push(b))
|
|
656
|
+
callback()
|
|
515
657
|
}
|
|
516
658
|
catch (error) {
|
|
517
|
-
callback(error)
|
|
659
|
+
callback(error)
|
|
518
660
|
}
|
|
519
661
|
},
|
|
520
662
|
final(callback) {
|
|
521
663
|
try {
|
|
522
|
-
pushBytes(aes.final(), b => this.push(b))
|
|
523
|
-
callback()
|
|
664
|
+
pushBytes(aes.final(), b => this.push(b))
|
|
665
|
+
callback()
|
|
524
666
|
}
|
|
525
667
|
catch (error) {
|
|
526
|
-
callback(error)
|
|
668
|
+
callback(error)
|
|
527
669
|
}
|
|
528
|
-
}
|
|
529
|
-
})
|
|
530
|
-
return fetched.pipe(output, { end: true })
|
|
531
|
-
};
|
|
532
|
-
export function extensionForMediaMessage(message) {
|
|
533
|
-
const getExtension = (mimetype) => mimetype.split(';')[0]?.split('/')[1];
|
|
534
|
-
const type = Object.keys(message)[0];
|
|
535
|
-
let extension;
|
|
536
|
-
if (type === 'locationMessage' || type === 'liveLocationMessage' || type === 'productMessage') {
|
|
537
|
-
extension = '.jpeg';
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
const messageContent = message[type];
|
|
541
|
-
extension = getExtension(messageContent.mimetype);
|
|
542
|
-
}
|
|
543
|
-
return extension;
|
|
670
|
+
},
|
|
671
|
+
})
|
|
672
|
+
return fetched.pipe(output, { end: true })
|
|
544
673
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
const parsedUrl = new URL(url);
|
|
556
|
-
const httpModule = parsedUrl.protocol === 'https:' ? await import('https') : await import('http');
|
|
557
|
-
// Get file size for Content-Length header (required for Node.js streaming)
|
|
558
|
-
const fileStats = await fs.stat(filePath);
|
|
559
|
-
const fileSize = fileStats.size;
|
|
560
|
-
return new Promise((resolve, reject) => {
|
|
561
|
-
const req = httpModule.request({
|
|
562
|
-
hostname: parsedUrl.hostname,
|
|
563
|
-
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
564
|
-
path: parsedUrl.pathname + parsedUrl.search,
|
|
565
|
-
method: 'POST',
|
|
566
|
-
headers: {
|
|
567
|
-
...headers,
|
|
568
|
-
'Content-Length': fileSize
|
|
569
|
-
},
|
|
570
|
-
agent,
|
|
571
|
-
timeout: timeoutMs
|
|
572
|
-
}, res => {
|
|
573
|
-
// Handle redirects (3xx)
|
|
574
|
-
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
575
|
-
res.resume(); // Consume response to free resources
|
|
576
|
-
const newUrl = new URL(res.headers.location, url).toString();
|
|
577
|
-
resolve(uploadWithNodeHttp({
|
|
578
|
-
url: newUrl,
|
|
579
|
-
filePath,
|
|
580
|
-
headers,
|
|
581
|
-
timeoutMs,
|
|
582
|
-
agent
|
|
583
|
-
}, redirectCount + 1));
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
let body = '';
|
|
587
|
-
res.on('data', chunk => (body += chunk));
|
|
588
|
-
res.on('end', () => {
|
|
589
|
-
try {
|
|
590
|
-
resolve(JSON.parse(body));
|
|
591
|
-
}
|
|
592
|
-
catch {
|
|
593
|
-
resolve(undefined);
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
});
|
|
597
|
-
req.on('error', reject);
|
|
598
|
-
req.on('timeout', () => {
|
|
599
|
-
req.destroy();
|
|
600
|
-
reject(new Error('Upload timeout'));
|
|
601
|
-
});
|
|
602
|
-
const stream = createReadStream(filePath);
|
|
603
|
-
stream.pipe(req);
|
|
604
|
-
stream.on('error', err => {
|
|
605
|
-
req.destroy();
|
|
606
|
-
reject(err);
|
|
607
|
-
});
|
|
608
|
-
});
|
|
609
|
-
};
|
|
610
|
-
const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {
|
|
611
|
-
// Convert Node.js Readable to Web ReadableStream
|
|
612
|
-
const nodeStream = createReadStream(filePath);
|
|
613
|
-
const webStream = Readable.toWeb(nodeStream);
|
|
614
|
-
const response = await fetch(url, {
|
|
615
|
-
dispatcher: agent,
|
|
616
|
-
method: 'POST',
|
|
617
|
-
body: webStream,
|
|
618
|
-
headers,
|
|
619
|
-
duplex: 'half',
|
|
620
|
-
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
|
|
621
|
-
});
|
|
622
|
-
try {
|
|
623
|
-
return (await response.json());
|
|
624
|
-
}
|
|
625
|
-
catch {
|
|
626
|
-
return undefined;
|
|
627
|
-
}
|
|
628
|
-
};
|
|
629
|
-
/**
|
|
630
|
-
* Uploads media to WhatsApp servers.
|
|
631
|
-
*
|
|
632
|
-
* ## Why we have two upload implementations:
|
|
633
|
-
*
|
|
634
|
-
* Node.js's native `fetch` (powered by undici) has a known bug where it buffers
|
|
635
|
-
* the entire request body in memory before sending, even when using streams.
|
|
636
|
-
* This causes memory issues with large files (e.g., 1GB file = 1GB+ memory usage).
|
|
637
|
-
* See: https://github.com/nodejs/undici/issues/4058
|
|
638
|
-
*
|
|
639
|
-
* Other runtimes (Bun, Deno, browsers) correctly stream the request body without
|
|
640
|
-
* buffering, so we can use the web-standard Fetch API there.
|
|
641
|
-
*
|
|
642
|
-
* ## Future considerations:
|
|
643
|
-
* Once the undici bug is fixed, we can simplify this to use only the Fetch API
|
|
644
|
-
* across all runtimes. Monitor the GitHub issue for updates.
|
|
645
|
-
*/
|
|
646
|
-
const uploadMedia = async (params, logger) => {
|
|
647
|
-
if (isNodeRuntime()) {
|
|
648
|
-
logger?.debug('Using Node.js https module for upload (avoids undici buffering bug)');
|
|
649
|
-
return uploadWithNodeHttp(params);
|
|
674
|
+
|
|
675
|
+
function extensionForMediaMessage(message) {
|
|
676
|
+
const getExtension = (mimetype) => mimetype.split('')[0].split('/')[1]
|
|
677
|
+
const type = Object.keys(message)[0]
|
|
678
|
+
let extension
|
|
679
|
+
if (type === 'locationMessage' ||
|
|
680
|
+
type === 'liveLocationMessage' ||
|
|
681
|
+
type === 'productMessage') {
|
|
682
|
+
extension = '.jpeg'
|
|
650
683
|
}
|
|
651
684
|
else {
|
|
652
|
-
|
|
653
|
-
|
|
685
|
+
const messageContent = message[type]
|
|
686
|
+
extension = getExtension(messageContent.mimetype)
|
|
654
687
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
688
|
+
return extension
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
|
|
692
|
+
return async (filePath, { mediaType, fileEncSha256B64, newsletter, timeoutMs }) => {
|
|
658
693
|
// send a query JSON to obtain the url & auth token to upload our media
|
|
659
|
-
let uploadInfo = await refreshMediaConn(false)
|
|
660
|
-
let urls
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
return {};
|
|
668
|
-
return Array.isArray(hdrs) ? Object.fromEntries(hdrs) : hdrs;
|
|
669
|
-
})();
|
|
670
|
-
const headers = {
|
|
671
|
-
...customHeaders,
|
|
672
|
-
'Content-Type': 'application/octet-stream',
|
|
673
|
-
Origin: DEFAULT_ORIGIN
|
|
674
|
-
};
|
|
694
|
+
let uploadInfo = await refreshMediaConn(false)
|
|
695
|
+
let urls
|
|
696
|
+
let media = Defaults_1.MEDIA_PATH_MAP[mediaType]
|
|
697
|
+
const hosts = [...customUploadHosts, ...uploadInfo.hosts]
|
|
698
|
+
fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
|
|
699
|
+
if (newsletter) {
|
|
700
|
+
media = media?.replace('/mms/', '/newsletter/newsletter-')
|
|
701
|
+
}
|
|
675
702
|
for (const { hostname } of hosts) {
|
|
676
|
-
logger.debug(`uploading to "${hostname}"`)
|
|
677
|
-
const auth = encodeURIComponent(uploadInfo.auth)
|
|
678
|
-
const url = `https://${hostname}${
|
|
679
|
-
|
|
703
|
+
logger.debug(`uploading to "${hostname}"`)
|
|
704
|
+
const auth = encodeURIComponent(uploadInfo.auth) // the auth token
|
|
705
|
+
const url = `https://${hostname}${media}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
|
|
706
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
707
|
+
let result
|
|
680
708
|
try {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
headers
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
709
|
+
const body = await axios_1.default.post(url, fs_1.createReadStream(filePath), {
|
|
710
|
+
...options,
|
|
711
|
+
maxRedirects: 0,
|
|
712
|
+
headers: {
|
|
713
|
+
...options.headers || {},
|
|
714
|
+
'Content-Type': 'application/octet-stream',
|
|
715
|
+
'Origin': Defaults_1.DEFAULT_ORIGIN
|
|
716
|
+
},
|
|
717
|
+
httpsAgent: fetchAgent,
|
|
718
|
+
timeout: timeoutMs,
|
|
719
|
+
responseType: 'json',
|
|
720
|
+
maxBodyLength: Infinity,
|
|
721
|
+
maxContentLength: Infinity,
|
|
722
|
+
})
|
|
723
|
+
result = body.data
|
|
724
|
+
if (result?.url || result?.directPath) {
|
|
689
725
|
urls = {
|
|
690
726
|
mediaUrl: result.url,
|
|
691
|
-
directPath: result.direct_path,
|
|
727
|
+
directPath: result.direct_path,
|
|
692
728
|
meta_hmac: result.meta_hmac,
|
|
693
729
|
fbid: result.fbid,
|
|
694
730
|
ts: result.ts
|
|
695
|
-
}
|
|
696
|
-
break
|
|
731
|
+
}
|
|
732
|
+
break
|
|
697
733
|
}
|
|
698
734
|
else {
|
|
699
|
-
uploadInfo = await refreshMediaConn(true)
|
|
700
|
-
throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
|
|
735
|
+
uploadInfo = await refreshMediaConn(true)
|
|
736
|
+
throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
|
|
701
737
|
}
|
|
702
738
|
}
|
|
703
739
|
catch (error) {
|
|
704
|
-
|
|
705
|
-
|
|
740
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
741
|
+
result = error.response?.data
|
|
742
|
+
}
|
|
743
|
+
const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname
|
|
744
|
+
logger.warn({ trace: error.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`)
|
|
706
745
|
}
|
|
707
746
|
}
|
|
708
747
|
if (!urls) {
|
|
709
|
-
throw new Boom('Media upload failed on all hosts', { statusCode: 500 })
|
|
748
|
+
throw new boom_1.Boom('Media upload failed on all hosts', { statusCode: 500 })
|
|
710
749
|
}
|
|
711
|
-
return urls
|
|
712
|
-
}
|
|
713
|
-
}
|
|
750
|
+
return urls
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
714
754
|
const getMediaRetryKey = (mediaKey) => {
|
|
715
|
-
return hkdf(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' })
|
|
716
|
-
}
|
|
755
|
+
return crypto_1.hkdf(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' })
|
|
756
|
+
}
|
|
717
757
|
/**
|
|
718
758
|
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
|
|
719
759
|
*/
|
|
720
|
-
|
|
721
|
-
const recp = { stanzaId: key.id }
|
|
722
|
-
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
|
|
723
|
-
const iv = Crypto.randomBytes(12)
|
|
724
|
-
const retryKey = getMediaRetryKey(mediaKey)
|
|
725
|
-
const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id))
|
|
760
|
+
const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
|
|
761
|
+
const recp = { stanzaId: key.id }
|
|
762
|
+
const recpBuffer = WAProto_1.proto.ServerErrorReceipt.encode(recp).finish()
|
|
763
|
+
const iv = Crypto.randomBytes(12)
|
|
764
|
+
const retryKey = await getMediaRetryKey(mediaKey)
|
|
765
|
+
const ciphertext = crypto_1.aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id))
|
|
726
766
|
const req = {
|
|
727
767
|
tag: 'receipt',
|
|
728
768
|
attrs: {
|
|
729
769
|
id: key.id,
|
|
730
|
-
to: jidNormalizedUser(meId),
|
|
770
|
+
to: WABinary_1.jidNormalizedUser(meId),
|
|
731
771
|
type: 'server-error'
|
|
732
772
|
},
|
|
733
773
|
content: [
|
|
@@ -746,17 +786,18 @@ export const encryptMediaRetryRequest = (key, mediaKey, meId) => {
|
|
|
746
786
|
tag: 'rmr',
|
|
747
787
|
attrs: {
|
|
748
788
|
jid: key.remoteJid,
|
|
749
|
-
from_me: (!!key.fromMe).toString(),
|
|
789
|
+
'from_me': (!!key.fromMe).toString(),
|
|
750
790
|
// @ts-ignore
|
|
751
791
|
participant: key.participant || undefined
|
|
752
792
|
}
|
|
753
793
|
}
|
|
754
794
|
]
|
|
755
|
-
}
|
|
756
|
-
return req
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
|
|
795
|
+
}
|
|
796
|
+
return req
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const decodeMediaRetryNode = (node) => {
|
|
800
|
+
const rmrNode = WABinary_1.getBinaryNodeChild(node, 'rmr')
|
|
760
801
|
const event = {
|
|
761
802
|
key: {
|
|
762
803
|
id: node.attrs.id,
|
|
@@ -764,38 +805,68 @@ export const decodeMediaRetryNode = (node) => {
|
|
|
764
805
|
fromMe: rmrNode.attrs.from_me === 'true',
|
|
765
806
|
participant: rmrNode.attrs.participant
|
|
766
807
|
}
|
|
767
|
-
}
|
|
768
|
-
const errorNode = getBinaryNodeChild(node, 'error')
|
|
808
|
+
}
|
|
809
|
+
const errorNode = WABinary_1.getBinaryNodeChild(node, 'error')
|
|
769
810
|
if (errorNode) {
|
|
770
|
-
const errorCode = +errorNode.attrs.code
|
|
771
|
-
event.error = new Boom(`Failed to re-upload media (${errorCode})`, {
|
|
772
|
-
data: errorNode.attrs,
|
|
773
|
-
statusCode: getStatusCodeForMediaRetry(errorCode)
|
|
774
|
-
});
|
|
811
|
+
const errorCode = +errorNode.attrs.code
|
|
812
|
+
event.error = new boom_1.Boom(`Failed to re-upload media (${errorCode})`, { data: errorNode.attrs, statusCode: getStatusCodeForMediaRetry(errorCode) })
|
|
775
813
|
}
|
|
776
814
|
else {
|
|
777
|
-
const encryptedInfoNode = getBinaryNodeChild(node, 'encrypt')
|
|
778
|
-
const ciphertext = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_p')
|
|
779
|
-
const iv = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_iv')
|
|
815
|
+
const encryptedInfoNode = WABinary_1.getBinaryNodeChild(node, 'encrypt')
|
|
816
|
+
const ciphertext = WABinary_1.getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_p')
|
|
817
|
+
const iv = WABinary_1.getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_iv')
|
|
780
818
|
if (ciphertext && iv) {
|
|
781
|
-
event.media = { ciphertext, iv }
|
|
819
|
+
event.media = { ciphertext, iv }
|
|
782
820
|
}
|
|
783
821
|
else {
|
|
784
|
-
event.error = new Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 })
|
|
822
|
+
event.error = new boom_1.Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 })
|
|
785
823
|
}
|
|
786
824
|
}
|
|
787
|
-
return event
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
825
|
+
return event
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const decryptMediaRetryData = async ({ ciphertext, iv }, mediaKey, msgId) => {
|
|
829
|
+
const retryKey = await getMediaRetryKey(mediaKey)
|
|
830
|
+
const plaintext = crypto_1.aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId))
|
|
831
|
+
return WAProto_1.proto.MediaRetryNotification.decode(plaintext)
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code]
|
|
835
|
+
|
|
795
836
|
const MEDIA_RETRY_STATUS_MAP = {
|
|
796
|
-
[proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
|
|
797
|
-
[proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
|
|
798
|
-
[proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
|
|
799
|
-
[proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
|
|
800
|
-
}
|
|
801
|
-
|
|
837
|
+
[WAProto_1.proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
|
|
838
|
+
[WAProto_1.proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
|
|
839
|
+
[WAProto_1.proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
|
|
840
|
+
[WAProto_1.proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418,
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
module.exports = {
|
|
844
|
+
hkdfInfoKey,
|
|
845
|
+
getMediaKeys,
|
|
846
|
+
extractVideoThumb,
|
|
847
|
+
extractImageThumb,
|
|
848
|
+
encodeBase64EncodedStringForUpload,
|
|
849
|
+
generateProfilePicture,
|
|
850
|
+
mediaMessageSHA256B64,
|
|
851
|
+
getAudioDuration,
|
|
852
|
+
getAudioWaveform,
|
|
853
|
+
toReadable,
|
|
854
|
+
toBuffer,
|
|
855
|
+
getStream,
|
|
856
|
+
generateThumbnail,
|
|
857
|
+
getHttpStream,
|
|
858
|
+
prepareStream,
|
|
859
|
+
encryptedStream,
|
|
860
|
+
getUrlFromDirectPath,
|
|
861
|
+
downloadContentFromMessage,
|
|
862
|
+
downloadEncryptedContent,
|
|
863
|
+
extensionForMediaMessage,
|
|
864
|
+
getRawMediaUploadData,
|
|
865
|
+
getWAUploadToServer,
|
|
866
|
+
getMediaRetryKey,
|
|
867
|
+
encryptMediaRetryRequest,
|
|
868
|
+
decodeMediaRetryNode,
|
|
869
|
+
decryptMediaRetryData,
|
|
870
|
+
getStatusCodeForMediaRetry,
|
|
871
|
+
MEDIA_RETRY_STATUS_MAP
|
|
872
|
+
}
|