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