@ubedsockets/baileys 1.6.6 → 1.6.7
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/WAProto/GenerateStatics.sh +4 -3
- package/WAProto/WAProto.proto +511 -1215
- package/WAProto/index.js +131318 -59331
- package/lib/Defaults/index.js +92 -76
- package/lib/Signal/Group/ciphertext-message.js +5 -2
- package/lib/Signal/Group/group-session-builder.js +41 -7
- package/lib/Signal/Group/group_cipher.js +51 -37
- package/lib/Signal/Group/index.js +57 -12
- package/lib/Signal/Group/keyhelper.js +44 -7
- package/lib/Signal/Group/sender-chain-key.js +15 -7
- package/lib/Signal/Group/sender-key-distribution-message.js +11 -8
- package/lib/Signal/Group/sender-key-message.js +12 -9
- package/lib/Signal/Group/sender-key-name.js +5 -2
- package/lib/Signal/Group/sender-key-record.js +21 -9
- package/lib/Signal/Group/sender-key-state.js +42 -27
- package/lib/Signal/Group/sender-message-key.js +7 -4
- package/lib/Signal/libsignal.js +90 -347
- package/lib/Socket/Client/index.js +18 -3
- package/lib/Socket/Client/types.js +6 -4
- package/lib/Socket/Client/websocket.js +78 -21
- package/lib/Socket/business.js +43 -162
- package/lib/Socket/chats.js +249 -571
- package/lib/Socket/groups.js +99 -114
- package/lib/Socket/index.js +9 -11
- package/lib/Socket/messages-recv.js +559 -1277
- package/lib/Socket/messages-send.js +669 -674
- package/lib/Socket/newsletter.js +286 -149
- package/lib/Socket/socket.js +235 -548
- package/lib/Types/Auth.js +2 -2
- package/lib/Types/Call.js +2 -2
- package/lib/Types/Chat.js +4 -8
- package/lib/Types/Contact.js +2 -2
- package/lib/Types/Events.js +2 -2
- package/lib/Types/GroupMetadata.js +2 -2
- package/lib/Types/Label.js +5 -3
- package/lib/Types/LabelAssociation.js +5 -3
- package/lib/Types/Message.js +7 -11
- package/lib/Types/Product.js +2 -2
- package/lib/Types/Signal.js +2 -2
- package/lib/Types/Socket.js +2 -3
- package/lib/Types/State.js +2 -56
- package/lib/Types/USync.js +2 -2
- package/lib/Types/index.js +31 -15
- package/lib/Utils/auth-utils.js +136 -239
- package/lib/Utils/business.js +69 -66
- package/lib/Utils/chat-utils.js +238 -380
- package/lib/Utils/crypto.js +127 -56
- package/lib/Utils/decode-wa-message.js +63 -173
- package/lib/Utils/event-buffer.js +70 -174
- package/lib/Utils/generics.js +207 -159
- package/lib/Utils/history.js +53 -93
- package/lib/Utils/index.js +33 -22
- package/lib/Utils/link-preview.js +58 -17
- package/lib/Utils/logger.js +7 -3
- package/lib/Utils/lt-hash.js +46 -3
- package/lib/Utils/make-mutex.js +34 -24
- package/lib/Utils/messages-media.js +744 -483
- package/lib/Utils/messages.js +621 -411
- package/lib/Utils/noise-handler.js +99 -149
- package/lib/Utils/process-message.js +150 -459
- package/lib/Utils/signal.js +73 -112
- package/lib/Utils/use-multi-file-auth-state.js +27 -23
- package/lib/Utils/validate-connection.js +104 -74
- package/lib/WABinary/constants.js +11 -9
- package/lib/WABinary/decode.js +52 -49
- package/lib/WABinary/encode.js +49 -19
- package/lib/WABinary/generic-utils.js +43 -46
- package/lib/WABinary/index.js +21 -6
- package/lib/WABinary/jid-utils.js +45 -72
- package/lib/WABinary/types.js +2 -2
- package/lib/WAM/BinaryInfo.js +5 -2
- package/lib/WAM/constants.js +11958 -19461
- package/lib/WAM/encode.js +22 -17
- package/lib/WAM/index.js +19 -4
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +13 -33
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +14 -11
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +11 -8
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +13 -9
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +22 -20
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +8 -13
- package/lib/WAUSync/Protocols/index.js +20 -6
- package/lib/WAUSync/USyncQuery.js +35 -44
- package/lib/WAUSync/USyncUser.js +5 -10
- package/lib/WAUSync/index.js +19 -4
- package/lib/index.js +70 -11
- package/package.json +60 -100
- package/engine-requirements.js +0 -10
|
@@ -1,244 +1,549 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.getStatusCodeForMediaRetry =
|
|
40
|
+
exports.decryptMediaRetryData =
|
|
41
|
+
exports.decodeMediaRetryNode =
|
|
42
|
+
exports.encryptMediaRetryRequest =
|
|
43
|
+
exports.getWAUploadToServer =
|
|
44
|
+
exports.downloadEncryptedContent =
|
|
45
|
+
exports.downloadContentFromMessage =
|
|
46
|
+
exports.getUrlFromDirectPath =
|
|
47
|
+
exports.encryptedStream =
|
|
48
|
+
exports.prepareStream =
|
|
49
|
+
exports.getHttpStream =
|
|
50
|
+
exports.getStream =
|
|
51
|
+
exports.toBuffer =
|
|
52
|
+
exports.toReadable =
|
|
53
|
+
exports.mediaMessageSHA256B64 =
|
|
54
|
+
exports.generateProfilePicture =
|
|
55
|
+
exports.encodeBase64EncodedStringForUpload =
|
|
56
|
+
exports.extractImageThumb =
|
|
57
|
+
exports.extractVideoThumb =
|
|
58
|
+
exports.hkdfInfoKey = void 0;
|
|
59
|
+
exports.getMediaKeys = getMediaKeys;
|
|
60
|
+
exports.uploadFile = uploadFile;
|
|
61
|
+
exports.vid2jpg = vid2jpg;
|
|
62
|
+
exports.getAudioDuration = getAudioDuration;
|
|
63
|
+
exports.getAudioWaveform = getAudioWaveform;
|
|
64
|
+
exports.generateThumbnail = generateThumbnail;
|
|
65
|
+
exports.extensionForMediaMessage = extensionForMediaMessage;
|
|
66
|
+
// 🆅🆈🆉🅴🅽 🅱🅰🅸🅻
|
|
67
|
+
const boom_1 = require("@hapi/boom");
|
|
68
|
+
const axios_1 = __importDefault(require("axios"));
|
|
69
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
70
|
+
const cheerio = __importStar(require("cheerio"));
|
|
71
|
+
const Crypto = __importStar(require("crypto"));
|
|
72
|
+
const events_1 = require("events");
|
|
73
|
+
const fs_1 = require("fs");
|
|
74
|
+
const os_1 = require("os");
|
|
75
|
+
const path_1 = require("path");
|
|
76
|
+
const jimp_1 = __importDefault(require("jimp"));
|
|
77
|
+
const stream_1 = require("stream");
|
|
78
|
+
const child_process_1 = require("child_process");
|
|
79
|
+
const WAProto_1 = require("../../WAProto");
|
|
80
|
+
const Defaults_1 = require("../Defaults");
|
|
81
|
+
const WABinary_1 = require("../WABinary");
|
|
82
|
+
const crypto_1 = require("./crypto");
|
|
83
|
+
const generics_1 = require("./generics");
|
|
84
|
+
const getTmpFilesDirectory = () => (0, os_1.tmpdir)();
|
|
16
85
|
const getImageProcessingLibrary = async () => {
|
|
17
|
-
|
|
18
|
-
|
|
86
|
+
const [_jimp, sharp] = await Promise.all([
|
|
87
|
+
(async () => {
|
|
88
|
+
const jimp = await (Promise.resolve().then(() => __importStar(require('jimp'))).catch(() => { }));
|
|
89
|
+
return jimp;
|
|
90
|
+
})(),
|
|
91
|
+
(async () => {
|
|
92
|
+
const sharp = await (Promise.resolve().then(() => __importStar(require('sharp'))).catch(() => { }));
|
|
93
|
+
return sharp;
|
|
94
|
+
})()
|
|
95
|
+
]);
|
|
19
96
|
if (sharp) {
|
|
20
97
|
return { sharp };
|
|
21
98
|
}
|
|
99
|
+
const jimp = (_jimp === null || _jimp === void 0 ? void 0 : _jimp.default) || _jimp;
|
|
22
100
|
if (jimp) {
|
|
23
101
|
return { jimp };
|
|
24
102
|
}
|
|
25
|
-
throw new Boom('No image processing library available');
|
|
103
|
+
throw new boom_1.Boom('No image processing library available');
|
|
26
104
|
};
|
|
27
|
-
|
|
28
|
-
const hkdfInfo = MEDIA_HKDF_KEY_MAPPING[type];
|
|
105
|
+
const hkdfInfoKey = (type) => {
|
|
106
|
+
const hkdfInfo = Defaults_1.MEDIA_HKDF_KEY_MAPPING[type];
|
|
29
107
|
return `WhatsApp ${hkdfInfo} Keys`;
|
|
30
108
|
};
|
|
31
|
-
|
|
32
|
-
const { stream } = await getStream(media);
|
|
33
|
-
logger?.debug('got stream for raw upload');
|
|
34
|
-
const hasher = Crypto.createHash('sha256');
|
|
35
|
-
const filePath = join(tmpdir(), mediaType + generateMessageIDV2());
|
|
36
|
-
const fileWriteStream = createWriteStream(filePath);
|
|
37
|
-
let fileLength = 0;
|
|
38
|
-
try {
|
|
39
|
-
for await (const data of stream) {
|
|
40
|
-
fileLength += data.length;
|
|
41
|
-
hasher.update(data);
|
|
42
|
-
if (!fileWriteStream.write(data)) {
|
|
43
|
-
await once(fileWriteStream, 'drain');
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
fileWriteStream.end();
|
|
47
|
-
await once(fileWriteStream, 'finish');
|
|
48
|
-
stream.destroy();
|
|
49
|
-
const fileSha256 = hasher.digest();
|
|
50
|
-
logger?.debug('hashed data for raw upload');
|
|
51
|
-
return {
|
|
52
|
-
filePath: filePath,
|
|
53
|
-
fileSha256,
|
|
54
|
-
fileLength
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
fileWriteStream.destroy();
|
|
59
|
-
stream.destroy();
|
|
60
|
-
try {
|
|
61
|
-
await fs.unlink(filePath);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
//
|
|
65
|
-
}
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
68
|
-
};
|
|
109
|
+
exports.hkdfInfoKey = hkdfInfoKey;
|
|
69
110
|
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
|
70
|
-
|
|
111
|
+
async function getMediaKeys(buffer, mediaType) {
|
|
71
112
|
if (!buffer) {
|
|
72
|
-
throw new Boom('Cannot derive from empty media key');
|
|
113
|
+
throw new boom_1.Boom('Cannot derive from empty media key');
|
|
73
114
|
}
|
|
74
115
|
if (typeof buffer === 'string') {
|
|
75
116
|
buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64');
|
|
76
117
|
}
|
|
77
118
|
// expand using HKDF to 112 bytes, also pass in the relevant app info
|
|
78
|
-
const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) });
|
|
119
|
+
const expandedMediaKey = await (0, crypto_1.hkdf)(buffer, 112, { info: (0, exports.hkdfInfoKey)(mediaType) });
|
|
79
120
|
return {
|
|
80
121
|
iv: expandedMediaKey.slice(0, 16),
|
|
81
122
|
cipherKey: expandedMediaKey.slice(16, 48),
|
|
82
|
-
macKey: expandedMediaKey.slice(48, 80)
|
|
123
|
+
macKey: expandedMediaKey.slice(48, 80),
|
|
83
124
|
};
|
|
84
125
|
}
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
126
|
+
async function uploadFile(buffer, logger) {
|
|
127
|
+
const { fromBuffer } = await Promise.resolve().then(() => __importStar(require('file-type')));
|
|
128
|
+
const fileType = await fromBuffer(buffer);
|
|
129
|
+
if (!fileType)
|
|
130
|
+
throw new Error("Failed to detect file type.");
|
|
131
|
+
const { ext, mime } = fileType;
|
|
132
|
+
const services = [
|
|
133
|
+
{
|
|
134
|
+
name: "catbox",
|
|
135
|
+
url: "https://catbox.moe/user/api.php",
|
|
136
|
+
buildForm: () => {
|
|
137
|
+
const form = new form_data_1.default();
|
|
138
|
+
form.append("fileToUpload", buffer, {
|
|
139
|
+
filename: `file.${ext}`,
|
|
140
|
+
contentType: mime || "application/octet-stream"
|
|
141
|
+
});
|
|
142
|
+
form.append("reqtype", "fileupload");
|
|
143
|
+
return form;
|
|
144
|
+
},
|
|
145
|
+
parseResponse: res => res.data
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "pdi.moe",
|
|
149
|
+
url: "https://scdn.pdi.moe/upload",
|
|
150
|
+
buildForm: () => {
|
|
151
|
+
const form = new form_data_1.default();
|
|
152
|
+
form.append("file", buffer, {
|
|
153
|
+
filename: `file.${ext}`,
|
|
154
|
+
contentType: mime
|
|
155
|
+
});
|
|
156
|
+
return form;
|
|
157
|
+
},
|
|
158
|
+
parseResponse: res => res.data.result.url
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "qu.ax",
|
|
162
|
+
url: "https://qu.ax/upload.php",
|
|
163
|
+
buildForm: () => {
|
|
164
|
+
const form = new form_data_1.default();
|
|
165
|
+
form.append("files[]", buffer, {
|
|
166
|
+
filename: `file.${ext}`,
|
|
167
|
+
contentType: mime || "application/octet-stream"
|
|
168
|
+
});
|
|
169
|
+
return form;
|
|
170
|
+
},
|
|
171
|
+
parseResponse: res => {
|
|
172
|
+
var _a, _b, _c;
|
|
173
|
+
if (!((_c = (_b = (_a = res.data) === null || _a === void 0 ? void 0 : _a.files) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.url))
|
|
174
|
+
throw new Error("Failed to get URL from qu.ax");
|
|
175
|
+
return res.data.files[0].url;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "uguu.se",
|
|
180
|
+
url: "https://uguu.se/upload.php",
|
|
181
|
+
buildForm: () => {
|
|
182
|
+
const form = new form_data_1.default();
|
|
183
|
+
form.append("files[]", buffer, {
|
|
184
|
+
filename: `file.${ext}`,
|
|
185
|
+
contentType: mime || "application/octet-stream"
|
|
186
|
+
});
|
|
187
|
+
return form;
|
|
188
|
+
},
|
|
189
|
+
parseResponse: res => {
|
|
190
|
+
var _a, _b, _c;
|
|
191
|
+
if (!((_c = (_b = (_a = res.data) === null || _a === void 0 ? void 0 : _a.files) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.url))
|
|
192
|
+
throw new Error("Failed to get URL from uguu.se");
|
|
193
|
+
return res.data.files[0].url;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "tmpfiles",
|
|
198
|
+
url: "https://tmpfiles.org/api/v1/upload",
|
|
199
|
+
buildForm: () => {
|
|
200
|
+
const form = new form_data_1.default();
|
|
201
|
+
form.append("file", buffer, {
|
|
202
|
+
filename: `file.${ext}`,
|
|
203
|
+
contentType: mime
|
|
204
|
+
});
|
|
205
|
+
return form;
|
|
206
|
+
},
|
|
207
|
+
parseResponse: res => {
|
|
208
|
+
const match = res.data.data.url.match(/https:\/\/tmpfiles\.org\/(.*)/);
|
|
209
|
+
if (!match)
|
|
210
|
+
throw new Error("Failed to parse tmpfiles URL.");
|
|
211
|
+
return `https://tmpfiles.org/dl/${match[1]}`;
|
|
212
|
+
}
|
|
91
213
|
}
|
|
92
|
-
|
|
93
|
-
|
|
214
|
+
];
|
|
215
|
+
for (const service of services) {
|
|
216
|
+
try {
|
|
217
|
+
const form = service.buildForm();
|
|
218
|
+
const res = await axios_1.default.post(service.url, form, {
|
|
219
|
+
headers: form.getHeaders()
|
|
220
|
+
});
|
|
221
|
+
const url = service.parseResponse(res);
|
|
222
|
+
return url;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
logger === null || logger === void 0 ? void 0 : logger.debug(`[${service.name}] eror:`, (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
throw new Error("All upload services failed.");
|
|
229
|
+
}
|
|
230
|
+
async function vid2jpg(videoUrl) {
|
|
231
|
+
try {
|
|
232
|
+
const { data } = await axios_1.default.get(`https://ezgif.com/video-to-jpg?url=${encodeURIComponent(videoUrl)}`);
|
|
233
|
+
const $ = cheerio.load(data);
|
|
234
|
+
const fileToken = $('input[name="file"]').attr("value");
|
|
235
|
+
if (!fileToken) {
|
|
236
|
+
throw new Error("Failed to retrieve file token. The video URL may be invalid or inaccessible.");
|
|
237
|
+
}
|
|
238
|
+
const formData = new URLSearchParams();
|
|
239
|
+
formData.append("file", fileToken);
|
|
240
|
+
formData.append("end", "1");
|
|
241
|
+
formData.append("video-to-jpg", "Convert to JPG!");
|
|
242
|
+
const convert = await axios_1.default.post(`https://ezgif.com/video-to-jpg/${fileToken}`, formData);
|
|
243
|
+
const $2 = cheerio.load(convert.data);
|
|
244
|
+
let imageUrl = $2("#output img").first().attr("src");
|
|
245
|
+
if (!imageUrl) {
|
|
246
|
+
throw new Error("Could not locate the converted image output.");
|
|
247
|
+
}
|
|
248
|
+
if (imageUrl.startsWith("//")) {
|
|
249
|
+
imageUrl = "https:" + imageUrl;
|
|
250
|
+
}
|
|
251
|
+
else if (imageUrl.startsWith("/")) {
|
|
252
|
+
const cdnMatch = imageUrl.match(/\/(s\d+\..+?)\/.*/);
|
|
253
|
+
if (cdnMatch) {
|
|
254
|
+
imageUrl = "https://" + imageUrl.slice(2);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
imageUrl = "https://ezgif.com" + imageUrl;
|
|
258
|
+
}
|
|
94
259
|
}
|
|
260
|
+
return imageUrl;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
throw new Error("Failed to convert video to JPG: " + error.message);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Extracts video thumbnail using FFmpeg
|
|
268
|
+
*/
|
|
269
|
+
const extractVideoThumb = async (videoPath, time = '00:00:00', size = { width: 256 }) => {
|
|
270
|
+
return new Promise((resolve, reject) => {
|
|
271
|
+
const args = [
|
|
272
|
+
'-ss', time,
|
|
273
|
+
'-i', videoPath,
|
|
274
|
+
'-y',
|
|
275
|
+
'-vf', `scale=${size.width}:-1`,
|
|
276
|
+
'-vframes', '1',
|
|
277
|
+
'-f', 'image2',
|
|
278
|
+
'-vcodec', 'mjpeg',
|
|
279
|
+
'pipe:1'
|
|
280
|
+
];
|
|
281
|
+
const ffmpeg = (0, child_process_1.spawn)('ffmpeg', args);
|
|
282
|
+
const chunks = [];
|
|
283
|
+
let errorOutput = '';
|
|
284
|
+
ffmpeg.stdout.on('data', chunk => chunks.push(chunk));
|
|
285
|
+
ffmpeg.stderr.on('data', data => {
|
|
286
|
+
errorOutput += data.toString();
|
|
287
|
+
});
|
|
288
|
+
ffmpeg.on('error', reject);
|
|
289
|
+
ffmpeg.on('close', code => {
|
|
290
|
+
if (code === 0) return resolve(Buffer.concat(chunks));
|
|
291
|
+
reject(new Error(`ffmpeg exited with code ${code}\n${errorOutput}`));
|
|
292
|
+
});
|
|
95
293
|
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (bufferOrFilePath instanceof Readable) {
|
|
101
|
-
bufferOrFilePath = await toBuffer(bufferOrFilePath);
|
|
294
|
+
};
|
|
295
|
+
exports.extractVideoThumb = extractVideoThumb;
|
|
296
|
+
const extractImageThumb = async (bufferOrFilePath, width = 32) => {
|
|
297
|
+
var _a, _b;
|
|
298
|
+
if (bufferOrFilePath instanceof stream_1.Readable) {
|
|
299
|
+
bufferOrFilePath = await (0, exports.toBuffer)(bufferOrFilePath);
|
|
102
300
|
}
|
|
103
301
|
const lib = await getImageProcessingLibrary();
|
|
104
|
-
if ('sharp' in lib && typeof lib.sharp
|
|
302
|
+
if ('sharp' in lib && typeof ((_a = lib.sharp) === null || _a === void 0 ? void 0 : _a.default) === 'function') {
|
|
105
303
|
const img = lib.sharp.default(bufferOrFilePath);
|
|
106
304
|
const dimensions = await img.metadata();
|
|
107
|
-
const buffer = await img
|
|
305
|
+
const buffer = await img
|
|
306
|
+
.resize(width)
|
|
307
|
+
.jpeg({ quality: 50 })
|
|
308
|
+
.toBuffer();
|
|
108
309
|
return {
|
|
109
310
|
buffer,
|
|
110
311
|
original: {
|
|
111
312
|
width: dimensions.width,
|
|
112
|
-
height: dimensions.height
|
|
113
|
-
}
|
|
313
|
+
height: dimensions.height,
|
|
314
|
+
},
|
|
114
315
|
};
|
|
115
316
|
}
|
|
116
|
-
else if ('jimp' in lib && typeof lib.jimp
|
|
117
|
-
const
|
|
317
|
+
else if ('jimp' in lib && typeof ((_b = lib.jimp) === null || _b === void 0 ? void 0 : _b.read) === 'function') {
|
|
318
|
+
const { read, MIME_JPEG, RESIZE_BILINEAR, AUTO } = lib.jimp;
|
|
319
|
+
const jimp = await read(bufferOrFilePath);
|
|
118
320
|
const dimensions = {
|
|
119
|
-
width: jimp.
|
|
120
|
-
height: jimp.
|
|
321
|
+
width: jimp.getWidth(),
|
|
322
|
+
height: jimp.getHeight()
|
|
121
323
|
};
|
|
122
324
|
const buffer = await jimp
|
|
123
|
-
.
|
|
124
|
-
.
|
|
325
|
+
.quality(50)
|
|
326
|
+
.resize(width, AUTO, RESIZE_BILINEAR)
|
|
327
|
+
.getBufferAsync(MIME_JPEG);
|
|
125
328
|
return {
|
|
126
329
|
buffer,
|
|
127
330
|
original: dimensions
|
|
128
331
|
};
|
|
129
332
|
}
|
|
130
333
|
else {
|
|
131
|
-
throw new Boom('No image processing library available');
|
|
334
|
+
throw new boom_1.Boom('No image processing library available');
|
|
132
335
|
}
|
|
133
336
|
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// Use getStream to handle all WAMediaUpload types (Buffer, Stream, URL)
|
|
143
|
-
const { stream } = await getStream(mediaUpload);
|
|
144
|
-
// Convert the resulting stream to a buffer
|
|
145
|
-
buffer = await toBuffer(stream);
|
|
146
|
-
}
|
|
147
|
-
const lib = await getImageProcessingLibrary();
|
|
337
|
+
exports.extractImageThumb = extractImageThumb;
|
|
338
|
+
const encodeBase64EncodedStringForUpload = (b64) => (encodeURIComponent(b64
|
|
339
|
+
.replace(/\+/g, '-')
|
|
340
|
+
.replace(/\//g, '_')
|
|
341
|
+
.replace(/\=+$/, '')));
|
|
342
|
+
exports.encodeBase64EncodedStringForUpload = encodeBase64EncodedStringForUpload;
|
|
343
|
+
const generateProfilePicture = async (mediaUpload) => {
|
|
344
|
+
let bufferOrFilePath;
|
|
148
345
|
let img;
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
.default(buffer)
|
|
152
|
-
.resize(w, h)
|
|
153
|
-
.jpeg({
|
|
154
|
-
quality: 50
|
|
155
|
-
})
|
|
156
|
-
.toBuffer();
|
|
346
|
+
if (Buffer.isBuffer(mediaUpload)) {
|
|
347
|
+
bufferOrFilePath = mediaUpload;
|
|
157
348
|
}
|
|
158
|
-
else if ('
|
|
159
|
-
|
|
160
|
-
const min = Math.min(jimp.width, jimp.height);
|
|
161
|
-
const cropped = jimp.crop({ x: 0, y: 0, w: min, h: min });
|
|
162
|
-
img = cropped.resize({ w, h, mode: lib.jimp.ResizeStrategy.BILINEAR }).getBuffer('image/jpeg', { quality: 50 });
|
|
349
|
+
else if ('url' in mediaUpload) {
|
|
350
|
+
bufferOrFilePath = mediaUpload.url.toString();
|
|
163
351
|
}
|
|
164
352
|
else {
|
|
165
|
-
|
|
353
|
+
bufferOrFilePath = await (0, exports.toBuffer)(mediaUpload.stream);
|
|
166
354
|
}
|
|
355
|
+
const jimp = await jimp_1.default.read(bufferOrFilePath);
|
|
356
|
+
const cropped = jimp.getWidth() > jimp.getHeight() ? jimp.resize(550, -1) : jimp.resize(-1, 650);
|
|
357
|
+
img = cropped
|
|
358
|
+
.quality(100)
|
|
359
|
+
.getBufferAsync(jimp_1.default.MIME_JPEG);
|
|
167
360
|
return {
|
|
168
|
-
img: await img
|
|
361
|
+
img: await img,
|
|
169
362
|
};
|
|
170
363
|
};
|
|
364
|
+
exports.generateProfilePicture = generateProfilePicture;
|
|
171
365
|
/** gets the SHA256 of the given media message */
|
|
172
|
-
|
|
366
|
+
const mediaMessageSHA256B64 = (message) => {
|
|
173
367
|
const media = Object.values(message)[0];
|
|
174
|
-
return media
|
|
368
|
+
return (media === null || media === void 0 ? void 0 : media.fileSha256) && Buffer.from(media.fileSha256).toString('base64');
|
|
175
369
|
};
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
370
|
+
exports.mediaMessageSHA256B64 = mediaMessageSHA256B64;
|
|
371
|
+
async function getAudioDuration(buffer) {
|
|
372
|
+
try {
|
|
373
|
+
const { PassThrough } = require('stream');
|
|
374
|
+
const ff = require('fluent-ffmpeg');
|
|
375
|
+
|
|
376
|
+
return await new Promise((resolve, reject) => {
|
|
377
|
+
const inputStream = new PassThrough();
|
|
378
|
+
inputStream.end(buffer);
|
|
379
|
+
|
|
380
|
+
ff(inputStream)
|
|
381
|
+
.ffprobe((err, data) => {
|
|
382
|
+
if (err) reject(err);
|
|
383
|
+
else resolve(data.format.duration);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
} catch (error) {
|
|
387
|
+
const musicMetadata = await import('music-metadata');
|
|
388
|
+
let metadata;
|
|
389
|
+
if (Buffer.isBuffer(buffer)) {
|
|
390
|
+
metadata = await musicMetadata.parseBuffer(buffer, undefined, {
|
|
391
|
+
duration: true
|
|
392
|
+
});
|
|
393
|
+
} else if (typeof buffer === 'string') {
|
|
394
|
+
const rStream = (0, fs_1.createReadStream)(buffer);
|
|
395
|
+
try {
|
|
396
|
+
metadata = await musicMetadata.parseStream(rStream, undefined, {
|
|
397
|
+
duration: true
|
|
398
|
+
});
|
|
399
|
+
} finally {
|
|
400
|
+
rStream.destroy();
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
metadata = await musicMetadata.parseStream(buffer, undefined, {
|
|
404
|
+
duration: true
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return metadata.format.duration;
|
|
190
408
|
}
|
|
191
|
-
return metadata.format.duration;
|
|
192
409
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
*/
|
|
196
|
-
export async function getAudioWaveform(buffer, logger) {
|
|
410
|
+
exports.getAudioDuration = getAudioDuration;
|
|
411
|
+
async function getAudioWaveform(buffer, logger) {
|
|
197
412
|
try {
|
|
198
|
-
|
|
199
|
-
const
|
|
413
|
+
const { PassThrough } = require('stream');
|
|
414
|
+
const ff = require('fluent-ffmpeg');
|
|
415
|
+
|
|
200
416
|
let audioData;
|
|
201
417
|
if (Buffer.isBuffer(buffer)) {
|
|
202
418
|
audioData = buffer;
|
|
419
|
+
} else if (typeof buffer === 'string') {
|
|
420
|
+
const rStream = require('fs').createReadStream(buffer);
|
|
421
|
+
audioData = await exports.toBuffer(rStream);
|
|
422
|
+
} else {
|
|
423
|
+
audioData = await exports.toBuffer(buffer);
|
|
203
424
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
425
|
+
|
|
426
|
+
return await new Promise((resolve, reject) => {
|
|
427
|
+
const inputStream = new PassThrough();
|
|
428
|
+
inputStream.end(audioData);
|
|
429
|
+
const chunks = [];
|
|
430
|
+
const bars = 64;
|
|
431
|
+
|
|
432
|
+
ff(inputStream)
|
|
433
|
+
.audioChannels(1)
|
|
434
|
+
.audioFrequency(16000)
|
|
435
|
+
.format('s16le')
|
|
436
|
+
.on('error', reject)
|
|
437
|
+
.on('end', () => {
|
|
438
|
+
const rawData = Buffer.concat(chunks);
|
|
439
|
+
const samples = rawData.length / 2;
|
|
440
|
+
const amplitudes = [];
|
|
441
|
+
|
|
442
|
+
for (let i = 0; i < samples; i++) {
|
|
443
|
+
amplitudes.push(Math.abs(rawData.readInt16LE(i * 2)) / 32768);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const blockSize = Math.floor(amplitudes.length / bars);
|
|
447
|
+
const avg = [];
|
|
448
|
+
for (let i = 0; i < bars; i++) {
|
|
449
|
+
const block = amplitudes.slice(i * blockSize, (i + 1) * blockSize);
|
|
450
|
+
avg.push(block.reduce((a, b) => a + b, 0) / block.length);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const max = Math.max(...avg);
|
|
454
|
+
const normalized = avg.map(v => Math.floor((v / max) * 100));
|
|
455
|
+
resolve(new Uint8Array(normalized));
|
|
456
|
+
})
|
|
457
|
+
.pipe()
|
|
458
|
+
.on('data', chunk => chunks.push(chunk));
|
|
459
|
+
});
|
|
460
|
+
} catch (e) {
|
|
461
|
+
logger?.debug(e);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
exports.getAudioWaveform = getAudioWaveform;
|
|
465
|
+
async function convertToOpusBuffer(buffer, logger) {
|
|
466
|
+
try {
|
|
467
|
+
const { PassThrough } = require('stream');
|
|
468
|
+
const ff = require('fluent-ffmpeg');
|
|
469
|
+
|
|
470
|
+
return await new Promise((resolve, reject) => {
|
|
471
|
+
const inStream = new PassThrough();
|
|
472
|
+
const outStream = new PassThrough();
|
|
473
|
+
const chunks = [];
|
|
474
|
+
inStream.end(buffer);
|
|
475
|
+
|
|
476
|
+
ff(inStream)
|
|
477
|
+
.noVideo()
|
|
478
|
+
.audioCodec('libopus')
|
|
479
|
+
.format('ogg')
|
|
480
|
+
.audioBitrate('48k')
|
|
481
|
+
.audioChannels(1)
|
|
482
|
+
.audioFrequency(48000)
|
|
483
|
+
.outputOptions([
|
|
484
|
+
'-vn',
|
|
485
|
+
'-b:a 64k',
|
|
486
|
+
'-ac 2',
|
|
487
|
+
'-ar 48000',
|
|
488
|
+
'-map_metadata', '-1',
|
|
489
|
+
'-application', 'voip'
|
|
490
|
+
])
|
|
491
|
+
.on('error', reject)
|
|
492
|
+
.on('end', () => resolve(Buffer.concat(chunks)))
|
|
493
|
+
.pipe(outStream, {
|
|
494
|
+
end: true
|
|
495
|
+
});
|
|
496
|
+
outStream.on('data', c => chunks.push(c));
|
|
497
|
+
});
|
|
498
|
+
} catch (e) {
|
|
499
|
+
logger?.debug(e);
|
|
500
|
+
throw e;
|
|
230
501
|
}
|
|
231
|
-
|
|
232
|
-
|
|
502
|
+
}
|
|
503
|
+
exports.convertToOpusBuffer = convertToOpusBuffer;
|
|
504
|
+
async function convertToMp4Buffer(buffer, logger) {
|
|
505
|
+
try {
|
|
506
|
+
const { PassThrough } = require('stream');
|
|
507
|
+
const ff = require('fluent-ffmpeg');
|
|
508
|
+
|
|
509
|
+
return await new Promise((resolve, reject) => {
|
|
510
|
+
const inStream = new PassThrough();
|
|
511
|
+
const outStream = new PassThrough();
|
|
512
|
+
const chunks = [];
|
|
513
|
+
|
|
514
|
+
inStream.end(buffer);
|
|
515
|
+
|
|
516
|
+
ff(inStream)
|
|
517
|
+
.videoCodec('libx264')
|
|
518
|
+
.audioCodec('aac')
|
|
519
|
+
.format('mp4')
|
|
520
|
+
.outputOptions([
|
|
521
|
+
'-preset', 'veryfast',
|
|
522
|
+
'-crf', '23',
|
|
523
|
+
'-movflags', 'faststart',
|
|
524
|
+
'-map_metadata', '-1'
|
|
525
|
+
])
|
|
526
|
+
.on('error', reject)
|
|
527
|
+
.on('end', () => resolve(Buffer.concat(chunks)))
|
|
528
|
+
.pipe(outStream, { end: true });
|
|
529
|
+
|
|
530
|
+
outStream.on('data', c => chunks.push(c));
|
|
531
|
+
});
|
|
532
|
+
} catch (e) {
|
|
533
|
+
logger?.debug(e);
|
|
534
|
+
throw e;
|
|
233
535
|
}
|
|
234
536
|
}
|
|
235
|
-
|
|
236
|
-
|
|
537
|
+
|
|
538
|
+
exports.convertToMp4Buffer = convertToMp4Buffer;
|
|
539
|
+
const toReadable = (buffer) => {
|
|
540
|
+
const readable = new stream_1.Readable({ read: () => { } });
|
|
237
541
|
readable.push(buffer);
|
|
238
542
|
readable.push(null);
|
|
239
543
|
return readable;
|
|
240
544
|
};
|
|
241
|
-
|
|
545
|
+
exports.toReadable = toReadable;
|
|
546
|
+
const toBuffer = async (stream) => {
|
|
242
547
|
const chunks = [];
|
|
243
548
|
for await (const chunk of stream) {
|
|
244
549
|
chunks.push(chunk);
|
|
@@ -246,47 +551,61 @@ export const toBuffer = async (stream) => {
|
|
|
246
551
|
stream.destroy();
|
|
247
552
|
return Buffer.concat(chunks);
|
|
248
553
|
};
|
|
249
|
-
|
|
554
|
+
exports.toBuffer = toBuffer;
|
|
555
|
+
const getStream = async (item, opts) => {
|
|
250
556
|
if (Buffer.isBuffer(item)) {
|
|
251
|
-
return { stream: toReadable(item), type: 'buffer' };
|
|
557
|
+
return { stream: (0, exports.toReadable)(item), type: 'buffer' };
|
|
252
558
|
}
|
|
253
559
|
if ('stream' in item) {
|
|
254
560
|
return { stream: item.stream, type: 'readable' };
|
|
255
561
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const buffer = Buffer.from(urlStr.split(',')[1], 'base64');
|
|
259
|
-
return { stream: toReadable(buffer), type: 'buffer' };
|
|
562
|
+
if (item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
|
|
563
|
+
return { stream: await (0, exports.getHttpStream)(item.url, opts), type: 'remote' };
|
|
260
564
|
}
|
|
261
|
-
|
|
262
|
-
return { stream: await getHttpStream(item.url, opts), type: 'remote' };
|
|
263
|
-
}
|
|
264
|
-
return { stream: createReadStream(item.url), type: 'file' };
|
|
565
|
+
return { stream: (0, fs_1.createReadStream)(item.url), type: 'file' };
|
|
265
566
|
};
|
|
567
|
+
exports.getStream = getStream;
|
|
266
568
|
/** generates a thumbnail for a given media, if required */
|
|
267
|
-
|
|
569
|
+
async function generateThumbnail(file, mediaType, options) {
|
|
570
|
+
var _a;
|
|
268
571
|
let thumbnail;
|
|
269
572
|
let originalImageDimensions;
|
|
270
573
|
if (mediaType === 'image') {
|
|
271
|
-
const { buffer, original } = await extractImageThumb(file);
|
|
574
|
+
const { buffer, original } = await (0, exports.extractImageThumb)(file);
|
|
272
575
|
thumbnail = buffer.toString('base64');
|
|
273
576
|
if (original.width && original.height) {
|
|
274
577
|
originalImageDimensions = {
|
|
275
578
|
width: original.width,
|
|
276
|
-
height: original.height
|
|
579
|
+
height: original.height,
|
|
277
580
|
};
|
|
278
581
|
}
|
|
279
582
|
}
|
|
280
583
|
else if (mediaType === 'video') {
|
|
281
|
-
const imgFilename = join(getTmpFilesDirectory(), generateMessageIDV2() + '.jpg');
|
|
282
584
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
585
|
+
let videoPath = file;
|
|
586
|
+
if (Buffer.isBuffer(file) || file instanceof stream_1.Readable) {
|
|
587
|
+
videoPath = (0, path_1.join)(getTmpFilesDirectory(), (0, generics_1.generateMessageIDV2)() + '.mp4');
|
|
588
|
+
const buffer = Buffer.isBuffer(file) ? file : await (0, exports.toBuffer)(file);
|
|
589
|
+
await fs_1.promises.writeFile(videoPath, buffer);
|
|
590
|
+
}
|
|
591
|
+
const thumbnailBuffer = await (0, exports.extractVideoThumb)(videoPath);
|
|
592
|
+
const imgFilename = (0, path_1.join)(getTmpFilesDirectory(), (0, generics_1.generateMessageIDV2)() + '.jpg');
|
|
593
|
+
await fs_1.promises.writeFile(imgFilename, thumbnailBuffer);
|
|
594
|
+
const { buffer: processedThumbnailBuffer, original } = await (0, exports.extractImageThumb)(imgFilename);
|
|
595
|
+
thumbnail = processedThumbnailBuffer.toString('base64');
|
|
596
|
+
if (original.width && original.height) {
|
|
597
|
+
originalImageDimensions = {
|
|
598
|
+
width: original.width,
|
|
599
|
+
height: original.height,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
await fs_1.promises.unlink(imgFilename);
|
|
603
|
+
if (videoPath !== file) {
|
|
604
|
+
await fs_1.promises.unlink(videoPath);
|
|
605
|
+
}
|
|
287
606
|
}
|
|
288
607
|
catch (err) {
|
|
289
|
-
options.logger
|
|
608
|
+
(_a = options.logger) === null || _a === void 0 ? void 0 : _a.debug('could not generate video thumb: ' + err);
|
|
290
609
|
}
|
|
291
610
|
}
|
|
292
611
|
return {
|
|
@@ -294,145 +613,196 @@ export async function generateThumbnail(file, mediaType, options) {
|
|
|
294
613
|
originalImageDimensions
|
|
295
614
|
};
|
|
296
615
|
}
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
616
|
+
const getHttpStream = async (url, options = {}) => {
|
|
617
|
+
const fetched = await axios_1.default.get(url.toString(), { ...options, responseType: 'stream' });
|
|
618
|
+
return fetched.data;
|
|
619
|
+
};
|
|
620
|
+
exports.getHttpStream = getHttpStream;
|
|
621
|
+
const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
|
|
622
|
+
const { stream, type } = await (0, exports.getStream)(media, opts);
|
|
623
|
+
logger === null || logger === void 0 ? void 0 : logger.debug('fetched media stream');
|
|
624
|
+
let bodyPath;
|
|
625
|
+
let didSaveToTmpPath = false;
|
|
626
|
+
try {
|
|
627
|
+
const buffer = await (0, exports.toBuffer)(stream);
|
|
628
|
+
if (type === 'file') {
|
|
629
|
+
bodyPath = media.url;
|
|
630
|
+
}
|
|
631
|
+
else if (saveOriginalFileIfRequired) {
|
|
632
|
+
bodyPath = (0, path_1.join)(getTmpFilesDirectory(), mediaType + (0, generics_1.generateMessageIDV2)());
|
|
633
|
+
(0, fs_1.writeFileSync)(bodyPath, buffer);
|
|
634
|
+
didSaveToTmpPath = true;
|
|
635
|
+
}
|
|
636
|
+
const fileLength = buffer.length;
|
|
637
|
+
const fileSha256 = Crypto.createHash('sha256').update(buffer).digest();
|
|
638
|
+
stream === null || stream === void 0 ? void 0 : stream.destroy();
|
|
639
|
+
logger === null || logger === void 0 ? void 0 : logger.debug('prepare stream data successfully');
|
|
640
|
+
return {
|
|
641
|
+
mediaKey: undefined,
|
|
642
|
+
encWriteStream: buffer,
|
|
643
|
+
fileLength,
|
|
644
|
+
fileSha256,
|
|
645
|
+
fileEncSha256: undefined,
|
|
646
|
+
bodyPath,
|
|
647
|
+
didSaveToTmpPath
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
stream.destroy();
|
|
652
|
+
if (didSaveToTmpPath) {
|
|
653
|
+
try {
|
|
654
|
+
await fs_1.promises.unlink(bodyPath);
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
logger === null || logger === void 0 ? void 0 : logger.error({ err }, 'failed to save to tmp path');
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
throw error;
|
|
305
661
|
}
|
|
306
|
-
// @ts-ignore Node18+ Readable.fromWeb exists
|
|
307
|
-
return response.body instanceof Readable ? response.body : Readable.fromWeb(response.body);
|
|
308
662
|
};
|
|
309
|
-
|
|
310
|
-
|
|
663
|
+
exports.prepareStream = prepareStream;
|
|
664
|
+
const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus, convertVideo } = {}) => {
|
|
665
|
+
const { stream, type } = await (0, exports.getStream)(media, opts);
|
|
311
666
|
logger?.debug('fetched media stream');
|
|
667
|
+
|
|
668
|
+
let finalStream = stream;
|
|
669
|
+
|
|
670
|
+
if (mediaType === 'audio' && (isPtt === true || forceOpus === true)) {
|
|
671
|
+
try {
|
|
672
|
+
const buffer = await (0, exports.toBuffer)(stream);
|
|
673
|
+
const opusBuffer = await exports.convertToOpusBuffer(buffer, logger);
|
|
674
|
+
finalStream = (0, exports.toReadable)(opusBuffer);
|
|
675
|
+
logger?.debug('converted audio to Opus');
|
|
676
|
+
} catch (error) {
|
|
677
|
+
logger?.error('failed to convert audio to Opus, fallback to original stream');
|
|
678
|
+
const { stream: newStream } = await (0, exports.getStream)(media, opts);
|
|
679
|
+
finalStream = newStream;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (mediaType === 'video' && convertVideo === true) {
|
|
684
|
+
try {
|
|
685
|
+
const buffer = await (0, exports.toBuffer)(stream);
|
|
686
|
+
const mp4Buffer = await exports.convertToMp4Buffer(buffer, logger);
|
|
687
|
+
finalStream = (0, exports.toReadable)(mp4Buffer);
|
|
688
|
+
logger?.debug('converted video to mp4');
|
|
689
|
+
} catch (error) {
|
|
690
|
+
logger?.error('failed to convert video to mp4, fallback to original stream');
|
|
691
|
+
const { stream: newStream } = await (0, exports.getStream)(media, opts);
|
|
692
|
+
finalStream = newStream;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
312
696
|
const mediaKey = Crypto.randomBytes(32);
|
|
313
697
|
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType);
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
let
|
|
317
|
-
let
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
698
|
+
const encWriteStream = new stream_1.Readable({ read: () => {} });
|
|
699
|
+
let bodyPath;
|
|
700
|
+
let writeStream;
|
|
701
|
+
let didSaveToTmpPath = false;
|
|
702
|
+
|
|
703
|
+
if (type === 'file') {
|
|
704
|
+
bodyPath = media.url;
|
|
705
|
+
} else if (saveOriginalFileIfRequired) {
|
|
706
|
+
bodyPath = (0, path_1.join)(getTmpFilesDirectory(), mediaType + (0, generics_1.generateMessageIDV2)());
|
|
707
|
+
writeStream = (0, fs_1.createWriteStream)(bodyPath);
|
|
708
|
+
didSaveToTmpPath = true;
|
|
321
709
|
}
|
|
710
|
+
|
|
322
711
|
let fileLength = 0;
|
|
323
712
|
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv);
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
sha256Enc.update(buff);
|
|
329
|
-
hmac.update(buff);
|
|
330
|
-
// Handle backpressure: if write returns false, wait for drain
|
|
331
|
-
if (!encFileWriteStream.write(buff)) {
|
|
332
|
-
await once(encFileWriteStream, 'drain');
|
|
333
|
-
}
|
|
334
|
-
};
|
|
713
|
+
let hmac = Crypto.createHmac('sha256', macKey).update(iv);
|
|
714
|
+
let sha256Plain = Crypto.createHash('sha256');
|
|
715
|
+
let sha256Enc = Crypto.createHash('sha256');
|
|
716
|
+
|
|
335
717
|
try {
|
|
336
|
-
for await (const data of
|
|
718
|
+
for await (const data of finalStream) {
|
|
337
719
|
fileLength += data.length;
|
|
338
|
-
if (type === 'remote' &&
|
|
339
|
-
|
|
340
|
-
fileLength + data.length > opts.maxContentLength) {
|
|
341
|
-
throw new Boom(`content length exceeded when encrypting "${type}"`, {
|
|
342
|
-
data: { media, type }
|
|
343
|
-
});
|
|
720
|
+
if (type === 'remote' && (opts?.maxContentLength) && fileLength + data.length > opts.maxContentLength) {
|
|
721
|
+
throw new boom_1.Boom(`content length exceeded when encrypting "${type}"`, { data: { media, type } });
|
|
344
722
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
723
|
+
|
|
724
|
+
sha256Plain = sha256Plain.update(data);
|
|
725
|
+
|
|
726
|
+
if (writeStream) {
|
|
727
|
+
if (!writeStream.write(data)) {
|
|
728
|
+
await (0, events_1.once)(writeStream, 'drain');
|
|
348
729
|
}
|
|
349
730
|
}
|
|
350
|
-
|
|
351
|
-
|
|
731
|
+
|
|
732
|
+
onChunk(aes.update(data));
|
|
352
733
|
}
|
|
353
|
-
|
|
734
|
+
|
|
735
|
+
onChunk(aes.final());
|
|
354
736
|
const mac = hmac.digest().slice(0, 10);
|
|
355
|
-
sha256Enc.update(mac);
|
|
737
|
+
sha256Enc = sha256Enc.update(mac);
|
|
356
738
|
const fileSha256 = sha256Plain.digest();
|
|
357
739
|
const fileEncSha256 = sha256Enc.digest();
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
// Wait for write streams to fully flush to disk
|
|
365
|
-
// This helps reduce memory pressure by allowing OS to release buffers
|
|
366
|
-
await encFinishPromise;
|
|
367
|
-
await originalFinishPromise;
|
|
740
|
+
|
|
741
|
+
encWriteStream.push(mac);
|
|
742
|
+
encWriteStream.push(null);
|
|
743
|
+
writeStream?.end();
|
|
744
|
+
finalStream.destroy();
|
|
745
|
+
|
|
368
746
|
logger?.debug('encrypted data successfully');
|
|
747
|
+
|
|
369
748
|
return {
|
|
370
749
|
mediaKey,
|
|
371
|
-
|
|
372
|
-
|
|
750
|
+
encWriteStream,
|
|
751
|
+
bodyPath,
|
|
373
752
|
mac,
|
|
374
753
|
fileEncSha256,
|
|
375
754
|
fileSha256,
|
|
376
|
-
fileLength
|
|
755
|
+
fileLength,
|
|
756
|
+
didSaveToTmpPath
|
|
377
757
|
};
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
encFileWriteStream.destroy();
|
|
382
|
-
originalFileStream?.destroy?.();
|
|
758
|
+
} catch (error) {
|
|
759
|
+
encWriteStream.destroy();
|
|
760
|
+
writeStream?.destroy();
|
|
383
761
|
aes.destroy();
|
|
384
762
|
hmac.destroy();
|
|
385
763
|
sha256Plain.destroy();
|
|
386
764
|
sha256Enc.destroy();
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
await
|
|
765
|
+
finalStream.destroy();
|
|
766
|
+
|
|
767
|
+
if (didSaveToTmpPath) {
|
|
768
|
+
try {
|
|
769
|
+
await fs_1.promises.unlink(bodyPath);
|
|
770
|
+
} catch (err) {
|
|
771
|
+
logger?.error({ err }, 'failed to save to tmp path');
|
|
392
772
|
}
|
|
393
773
|
}
|
|
394
|
-
catch (err) {
|
|
395
|
-
logger?.error({ err }, 'failed deleting tmp files');
|
|
396
|
-
}
|
|
397
774
|
throw error;
|
|
398
775
|
}
|
|
776
|
+
|
|
777
|
+
function onChunk(buff) {
|
|
778
|
+
sha256Enc = sha256Enc.update(buff);
|
|
779
|
+
hmac = hmac.update(buff);
|
|
780
|
+
encWriteStream.push(buff);
|
|
781
|
+
}
|
|
399
782
|
};
|
|
400
|
-
|
|
783
|
+
|
|
784
|
+
exports.encryptedStream = encryptedStream;
|
|
785
|
+
const DEF_HOST = 'mmg.whatsapp.net';
|
|
401
786
|
const AES_CHUNK_SIZE = 16;
|
|
402
787
|
const toSmallestChunkSize = (num) => {
|
|
403
788
|
return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE;
|
|
404
789
|
};
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return new URL(url).host;
|
|
411
|
-
}
|
|
412
|
-
catch {
|
|
413
|
-
return undefined;
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
export const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
|
|
417
|
-
// Fallback host: explicit opt > host parsed from `url` > DEF_MEDIA_HOST.
|
|
418
|
-
// Lets us honor a non-default host carried by the proto without forcing callers to thread it through.
|
|
419
|
-
const fallbackHost = opts.host ?? extractHost(url);
|
|
420
|
-
const downloadUrl = directPath ? getUrlFromDirectPath(directPath, fallbackHost) : url;
|
|
790
|
+
const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`;
|
|
791
|
+
exports.getUrlFromDirectPath = getUrlFromDirectPath;
|
|
792
|
+
const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
|
|
793
|
+
const isValidMediaUrl = url === null || url === void 0 ? void 0 : url.startsWith('https://mmg.whatsapp.net/');
|
|
794
|
+
const downloadUrl = isValidMediaUrl ? url : (0, exports.getUrlFromDirectPath)(directPath);
|
|
421
795
|
if (!downloadUrl) {
|
|
422
|
-
throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 });
|
|
796
|
+
throw new boom_1.Boom('No valid media URL or directPath present in message', { statusCode: 400 });
|
|
423
797
|
}
|
|
424
798
|
const keys = await getMediaKeys(mediaKey, type);
|
|
425
|
-
return downloadEncryptedContent(downloadUrl, keys, opts);
|
|
799
|
+
return (0, exports.downloadEncryptedContent)(downloadUrl, keys, opts);
|
|
426
800
|
};
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
* Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
|
|
430
|
-
* */
|
|
431
|
-
export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
|
|
801
|
+
exports.downloadContentFromMessage = downloadContentFromMessage;
|
|
802
|
+
const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
|
|
432
803
|
let bytesFetched = 0;
|
|
433
804
|
let startChunk = 0;
|
|
434
805
|
let firstBlockIsIV = false;
|
|
435
|
-
// if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
|
|
436
806
|
if (startByte) {
|
|
437
807
|
const chunk = toSmallestChunkSize(startByte || 0);
|
|
438
808
|
if (chunk) {
|
|
@@ -442,14 +812,9 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
|
|
|
442
812
|
}
|
|
443
813
|
}
|
|
444
814
|
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined;
|
|
445
|
-
const headersInit = options?.headers ? options.headers : undefined;
|
|
446
815
|
const headers = {
|
|
447
|
-
...(
|
|
448
|
-
|
|
449
|
-
? Object.fromEntries(headersInit)
|
|
450
|
-
: headersInit
|
|
451
|
-
: {}),
|
|
452
|
-
Origin: DEFAULT_ORIGIN
|
|
816
|
+
...(options === null || options === void 0 ? void 0 : options.headers) || {},
|
|
817
|
+
Origin: Defaults_1.DEFAULT_ORIGIN,
|
|
453
818
|
};
|
|
454
819
|
if (startChunk || endChunk) {
|
|
455
820
|
headers.Range = `bytes=${startChunk}-`;
|
|
@@ -457,10 +822,11 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
|
|
|
457
822
|
headers.Range += endChunk;
|
|
458
823
|
}
|
|
459
824
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
825
|
+
const fetched = await (0, exports.getHttpStream)(downloadUrl, {
|
|
826
|
+
...options || {},
|
|
827
|
+
headers,
|
|
828
|
+
maxBodyLength: Infinity,
|
|
829
|
+
maxContentLength: Infinity,
|
|
464
830
|
});
|
|
465
831
|
let remainingBytes = Buffer.from([]);
|
|
466
832
|
let aes;
|
|
@@ -475,9 +841,9 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
|
|
|
475
841
|
push(bytes);
|
|
476
842
|
}
|
|
477
843
|
};
|
|
478
|
-
const output = new Transform({
|
|
844
|
+
const output = new stream_1.Transform({
|
|
479
845
|
transform(chunk, _, callback) {
|
|
480
|
-
let data =
|
|
846
|
+
let data = Buffer.concat([remainingBytes, chunk]);
|
|
481
847
|
const decryptLength = toSmallestChunkSize(data.length);
|
|
482
848
|
remainingBytes = data.slice(decryptLength);
|
|
483
849
|
data = data.slice(0, decryptLength);
|
|
@@ -488,8 +854,6 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
|
|
|
488
854
|
data = data.slice(AES_CHUNK_SIZE);
|
|
489
855
|
}
|
|
490
856
|
aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue);
|
|
491
|
-
// if an end byte that is not EOF is specified
|
|
492
|
-
// stop auto padding (PKCS7) -- otherwise throws an error for decryption
|
|
493
857
|
if (endByte) {
|
|
494
858
|
aes.setAutoPadding(false);
|
|
495
859
|
}
|
|
@@ -510,15 +874,18 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
|
|
|
510
874
|
catch (error) {
|
|
511
875
|
callback(error);
|
|
512
876
|
}
|
|
513
|
-
}
|
|
877
|
+
},
|
|
514
878
|
});
|
|
515
879
|
return fetched.pipe(output, { end: true });
|
|
516
880
|
};
|
|
517
|
-
|
|
518
|
-
|
|
881
|
+
exports.downloadEncryptedContent = downloadEncryptedContent;
|
|
882
|
+
function extensionForMediaMessage(message) {
|
|
883
|
+
const getExtension = (mimetype) => mimetype.split(';')[0].split('/')[1];
|
|
519
884
|
const type = Object.keys(message)[0];
|
|
520
885
|
let extension;
|
|
521
|
-
if (type === 'locationMessage' ||
|
|
886
|
+
if (type === 'locationMessage' ||
|
|
887
|
+
type === 'liveLocationMessage' ||
|
|
888
|
+
type === 'productMessage') {
|
|
522
889
|
extension = '.jpeg';
|
|
523
890
|
}
|
|
524
891
|
else {
|
|
@@ -527,158 +894,52 @@ export function extensionForMediaMessage(message) {
|
|
|
527
894
|
}
|
|
528
895
|
return extension;
|
|
529
896
|
}
|
|
530
|
-
const
|
|
531
|
-
return (
|
|
532
|
-
|
|
533
|
-
typeof process.versions.bun === 'undefined' &&
|
|
534
|
-
typeof globalThis.Deno === 'undefined');
|
|
535
|
-
};
|
|
536
|
-
export const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent }, redirectCount = 0) => {
|
|
537
|
-
if (redirectCount > 5) {
|
|
538
|
-
throw new Error('Too many redirects');
|
|
539
|
-
}
|
|
540
|
-
const parsedUrl = new URL(url);
|
|
541
|
-
const httpModule = parsedUrl.protocol === 'https:' ? await import('https') : await import('http');
|
|
542
|
-
// Get file size for Content-Length header (required for Node.js streaming)
|
|
543
|
-
const fileStats = await fs.stat(filePath);
|
|
544
|
-
const fileSize = fileStats.size;
|
|
545
|
-
return new Promise((resolve, reject) => {
|
|
546
|
-
const req = httpModule.request({
|
|
547
|
-
hostname: parsedUrl.hostname,
|
|
548
|
-
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
549
|
-
path: parsedUrl.pathname + parsedUrl.search,
|
|
550
|
-
method: 'POST',
|
|
551
|
-
headers: {
|
|
552
|
-
...headers,
|
|
553
|
-
'Content-Length': fileSize
|
|
554
|
-
},
|
|
555
|
-
agent,
|
|
556
|
-
timeout: timeoutMs
|
|
557
|
-
}, res => {
|
|
558
|
-
// Handle redirects (3xx)
|
|
559
|
-
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
560
|
-
res.resume(); // Consume response to free resources
|
|
561
|
-
const newUrl = new URL(res.headers.location, url).toString();
|
|
562
|
-
resolve(uploadWithNodeHttp({
|
|
563
|
-
url: newUrl,
|
|
564
|
-
filePath,
|
|
565
|
-
headers,
|
|
566
|
-
timeoutMs,
|
|
567
|
-
agent
|
|
568
|
-
}, redirectCount + 1));
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
let body = '';
|
|
572
|
-
res.on('data', chunk => (body += chunk));
|
|
573
|
-
res.on('end', () => {
|
|
574
|
-
try {
|
|
575
|
-
resolve(JSON.parse(body));
|
|
576
|
-
}
|
|
577
|
-
catch {
|
|
578
|
-
resolve(undefined);
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
req.on('error', reject);
|
|
583
|
-
req.on('timeout', () => {
|
|
584
|
-
req.destroy();
|
|
585
|
-
reject(new Error('Upload timeout'));
|
|
586
|
-
});
|
|
587
|
-
const stream = createReadStream(filePath);
|
|
588
|
-
stream.pipe(req);
|
|
589
|
-
stream.on('error', err => {
|
|
590
|
-
req.destroy();
|
|
591
|
-
reject(err);
|
|
592
|
-
});
|
|
593
|
-
});
|
|
594
|
-
};
|
|
595
|
-
const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {
|
|
596
|
-
// Convert Node.js Readable to Web ReadableStream
|
|
597
|
-
const nodeStream = createReadStream(filePath);
|
|
598
|
-
const webStream = Readable.toWeb(nodeStream);
|
|
599
|
-
// Native fetch only accepts Undici-style dispatchers, not generic https Agents.
|
|
600
|
-
const dispatcher = typeof agent?.dispatch === 'function' ? agent : undefined;
|
|
601
|
-
const response = await fetch(url, {
|
|
602
|
-
...(dispatcher ? { dispatcher } : {}),
|
|
603
|
-
method: 'POST',
|
|
604
|
-
body: webStream,
|
|
605
|
-
headers,
|
|
606
|
-
duplex: 'half',
|
|
607
|
-
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
|
|
608
|
-
});
|
|
609
|
-
try {
|
|
610
|
-
return (await response.json());
|
|
611
|
-
}
|
|
612
|
-
catch {
|
|
613
|
-
return undefined;
|
|
614
|
-
}
|
|
615
|
-
};
|
|
616
|
-
/**
|
|
617
|
-
* Uploads media to WhatsApp servers.
|
|
618
|
-
*
|
|
619
|
-
* ## Why we have two upload implementations:
|
|
620
|
-
*
|
|
621
|
-
* Node.js's native `fetch` (powered by undici) has a known bug where it buffers
|
|
622
|
-
* the entire request body in memory before sending, even when using streams.
|
|
623
|
-
* This causes memory issues with large files (e.g., 1GB file = 1GB+ memory usage).
|
|
624
|
-
* See: https://github.com/nodejs/undici/issues/4058
|
|
625
|
-
*
|
|
626
|
-
* Other runtimes (Bun, Deno, browsers) correctly stream the request body without
|
|
627
|
-
* buffering, so we can use the web-standard Fetch API there.
|
|
628
|
-
*
|
|
629
|
-
* ## Future considerations:
|
|
630
|
-
* Once the undici bug is fixed, we can simplify this to use only the Fetch API
|
|
631
|
-
* across all runtimes. Monitor the GitHub issue for updates.
|
|
632
|
-
*/
|
|
633
|
-
const uploadMedia = async (params, logger) => {
|
|
634
|
-
if (isNodeRuntime()) {
|
|
635
|
-
logger?.debug('Using Node.js https module for upload (avoids undici buffering bug)');
|
|
636
|
-
return uploadWithNodeHttp(params);
|
|
637
|
-
}
|
|
638
|
-
else {
|
|
639
|
-
logger?.debug('Using web-standard Fetch API for upload');
|
|
640
|
-
return uploadWithFetch(params);
|
|
641
|
-
}
|
|
642
|
-
};
|
|
643
|
-
export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
|
|
644
|
-
return async (filePath, { mediaType, fileEncSha256B64, timeoutMs }) => {
|
|
645
|
-
// send a query JSON to obtain the url & auth token to upload our media
|
|
897
|
+
const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
|
|
898
|
+
return async (stream, { mediaType, fileEncSha256B64, newsletter, timeoutMs }) => {
|
|
899
|
+
var _a, _b;
|
|
646
900
|
let uploadInfo = await refreshMediaConn(false);
|
|
647
901
|
let urls;
|
|
648
902
|
const hosts = [...customUploadHosts, ...uploadInfo.hosts];
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
'
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
for (const { hostname } of hosts) {
|
|
903
|
+
const chunks = [];
|
|
904
|
+
if (!Buffer.isBuffer(stream)) {
|
|
905
|
+
for await (const chunk of stream) {
|
|
906
|
+
chunks.push(chunk);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
const reqBody = Buffer.isBuffer(stream) ? stream : Buffer.concat(chunks);
|
|
910
|
+
fileEncSha256B64 = (0, exports.encodeBase64EncodedStringForUpload)(fileEncSha256B64);
|
|
911
|
+
let media = Defaults_1.MEDIA_PATH_MAP[mediaType];
|
|
912
|
+
if (newsletter) {
|
|
913
|
+
media = media === null || media === void 0 ? void 0 : media.replace('/mms/', '/newsletter/newsletter-');
|
|
914
|
+
}
|
|
915
|
+
for (const { hostname, maxContentLengthBytes } of hosts) {
|
|
663
916
|
logger.debug(`uploading to "${hostname}"`);
|
|
664
917
|
const auth = encodeURIComponent(uploadInfo.auth);
|
|
665
|
-
const url = `https://${hostname}${
|
|
918
|
+
const url = `https://${hostname}${media}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
|
|
666
919
|
let result;
|
|
667
920
|
try {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
921
|
+
if (maxContentLengthBytes && reqBody.length > maxContentLengthBytes) {
|
|
922
|
+
throw new boom_1.Boom(`Body too large for "${hostname}"`, { statusCode: 413 });
|
|
923
|
+
}
|
|
924
|
+
const body = await axios_1.default.post(url, reqBody, {
|
|
925
|
+
...options,
|
|
926
|
+
headers: {
|
|
927
|
+
...options.headers || {},
|
|
928
|
+
'Content-Type': 'application/octet-stream',
|
|
929
|
+
'Origin': Defaults_1.DEFAULT_ORIGIN
|
|
930
|
+
},
|
|
931
|
+
httpsAgent: fetchAgent,
|
|
932
|
+
timeout: timeoutMs,
|
|
933
|
+
responseType: 'json',
|
|
934
|
+
maxBodyLength: Infinity,
|
|
935
|
+
maxContentLength: Infinity,
|
|
936
|
+
});
|
|
937
|
+
result = body.data;
|
|
938
|
+
if ((result === null || result === void 0 ? void 0 : result.url) || (result === null || result === void 0 ? void 0 : result.directPath)) {
|
|
676
939
|
urls = {
|
|
677
940
|
mediaUrl: result.url,
|
|
678
941
|
directPath: result.direct_path,
|
|
679
|
-
|
|
680
|
-
fbid: result.fbid,
|
|
681
|
-
ts: result.ts
|
|
942
|
+
handle: result.handle
|
|
682
943
|
};
|
|
683
944
|
break;
|
|
684
945
|
}
|
|
@@ -688,39 +949,37 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, opt
|
|
|
688
949
|
}
|
|
689
950
|
}
|
|
690
951
|
catch (error) {
|
|
691
|
-
|
|
692
|
-
|
|
952
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
953
|
+
result = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data;
|
|
954
|
+
}
|
|
955
|
+
const isLast = hostname === ((_b = hosts[uploadInfo.hosts.length - 1]) === null || _b === void 0 ? void 0 : _b.hostname);
|
|
956
|
+
logger.warn({ trace: error.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`);
|
|
693
957
|
}
|
|
694
958
|
}
|
|
695
959
|
if (!urls) {
|
|
696
|
-
throw new Boom('Media upload failed on all hosts', { statusCode: 500 });
|
|
960
|
+
throw new boom_1.Boom('Media upload failed on all hosts', { statusCode: 500 });
|
|
697
961
|
}
|
|
698
962
|
return urls;
|
|
699
963
|
};
|
|
700
964
|
};
|
|
965
|
+
exports.getWAUploadToServer = getWAUploadToServer;
|
|
701
966
|
const getMediaRetryKey = (mediaKey) => {
|
|
702
|
-
return hkdf(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' });
|
|
967
|
+
return (0, crypto_1.hkdf)(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' });
|
|
703
968
|
};
|
|
704
|
-
|
|
705
|
-
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
|
|
706
|
-
*/
|
|
707
|
-
export const encryptMediaRetryRequest = (key, mediaKey, meId) => {
|
|
969
|
+
const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
|
|
708
970
|
const recp = { stanzaId: key.id };
|
|
709
|
-
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish();
|
|
971
|
+
const recpBuffer = WAProto_1.proto.ServerErrorReceipt.encode(recp).finish();
|
|
710
972
|
const iv = Crypto.randomBytes(12);
|
|
711
|
-
const retryKey = getMediaRetryKey(mediaKey);
|
|
712
|
-
const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id));
|
|
973
|
+
const retryKey = await getMediaRetryKey(mediaKey);
|
|
974
|
+
const ciphertext = (0, crypto_1.aesEncryptGCM)(recpBuffer, retryKey, iv, Buffer.from(key.id));
|
|
713
975
|
const req = {
|
|
714
976
|
tag: 'receipt',
|
|
715
977
|
attrs: {
|
|
716
978
|
id: key.id,
|
|
717
|
-
to: jidNormalizedUser(meId),
|
|
979
|
+
to: (0, WABinary_1.jidNormalizedUser)(meId),
|
|
718
980
|
type: 'server-error'
|
|
719
981
|
},
|
|
720
982
|
content: [
|
|
721
|
-
// this encrypt node is actually pretty useless
|
|
722
|
-
// the media is returned even without this node
|
|
723
|
-
// keeping it here to maintain parity with WA Web
|
|
724
983
|
{
|
|
725
984
|
tag: 'encrypt',
|
|
726
985
|
attrs: {},
|
|
@@ -733,8 +992,7 @@ export const encryptMediaRetryRequest = (key, mediaKey, meId) => {
|
|
|
733
992
|
tag: 'rmr',
|
|
734
993
|
attrs: {
|
|
735
994
|
jid: key.remoteJid,
|
|
736
|
-
from_me: (!!key.fromMe).toString(),
|
|
737
|
-
// @ts-ignore
|
|
995
|
+
'from_me': (!!key.fromMe).toString(),
|
|
738
996
|
participant: key.participant || undefined
|
|
739
997
|
}
|
|
740
998
|
}
|
|
@@ -742,8 +1000,9 @@ export const encryptMediaRetryRequest = (key, mediaKey, meId) => {
|
|
|
742
1000
|
};
|
|
743
1001
|
return req;
|
|
744
1002
|
};
|
|
745
|
-
|
|
746
|
-
|
|
1003
|
+
exports.encryptMediaRetryRequest = encryptMediaRetryRequest;
|
|
1004
|
+
const decodeMediaRetryNode = (node) => {
|
|
1005
|
+
const rmrNode = (0, WABinary_1.getBinaryNodeChild)(node, 'rmr');
|
|
747
1006
|
const event = {
|
|
748
1007
|
key: {
|
|
749
1008
|
id: node.attrs.id,
|
|
@@ -752,37 +1011,39 @@ export const decodeMediaRetryNode = (node) => {
|
|
|
752
1011
|
participant: rmrNode.attrs.participant
|
|
753
1012
|
}
|
|
754
1013
|
};
|
|
755
|
-
const errorNode = getBinaryNodeChild(node, 'error');
|
|
1014
|
+
const errorNode = (0, WABinary_1.getBinaryNodeChild)(node, 'error');
|
|
756
1015
|
if (errorNode) {
|
|
757
1016
|
const errorCode = +errorNode.attrs.code;
|
|
758
|
-
event.error = new Boom(`Failed to re-upload media (${errorCode})`, {
|
|
759
|
-
data: errorNode.attrs,
|
|
760
|
-
statusCode: getStatusCodeForMediaRetry(errorCode)
|
|
761
|
-
});
|
|
1017
|
+
event.error = new boom_1.Boom(`Failed to re-upload media (${errorCode})`, { data: errorNode.attrs, statusCode: (0, exports.getStatusCodeForMediaRetry)(errorCode) });
|
|
762
1018
|
}
|
|
763
1019
|
else {
|
|
764
|
-
const encryptedInfoNode = getBinaryNodeChild(node, 'encrypt');
|
|
765
|
-
const ciphertext = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_p');
|
|
766
|
-
const iv = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_iv');
|
|
1020
|
+
const encryptedInfoNode = (0, WABinary_1.getBinaryNodeChild)(node, 'encrypt');
|
|
1021
|
+
const ciphertext = (0, WABinary_1.getBinaryNodeChildBuffer)(encryptedInfoNode, 'enc_p');
|
|
1022
|
+
const iv = (0, WABinary_1.getBinaryNodeChildBuffer)(encryptedInfoNode, 'enc_iv');
|
|
767
1023
|
if (ciphertext && iv) {
|
|
768
1024
|
event.media = { ciphertext, iv };
|
|
769
1025
|
}
|
|
770
1026
|
else {
|
|
771
|
-
event.error = new Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 });
|
|
1027
|
+
event.error = new boom_1.Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 });
|
|
772
1028
|
}
|
|
773
1029
|
}
|
|
774
1030
|
return event;
|
|
775
1031
|
};
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const
|
|
779
|
-
|
|
1032
|
+
exports.decodeMediaRetryNode = decodeMediaRetryNode;
|
|
1033
|
+
const decryptMediaRetryData = async ({ ciphertext, iv }, mediaKey, msgId) => {
|
|
1034
|
+
const retryKey = await getMediaRetryKey(mediaKey);
|
|
1035
|
+
const plaintext = (0, crypto_1.aesDecryptGCM)(ciphertext, retryKey, iv, Buffer.from(msgId));
|
|
1036
|
+
return WAProto_1.proto.MediaRetryNotification.decode(plaintext);
|
|
780
1037
|
};
|
|
781
|
-
|
|
1038
|
+
exports.decryptMediaRetryData = decryptMediaRetryData;
|
|
1039
|
+
const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code];
|
|
1040
|
+
exports.getStatusCodeForMediaRetry = getStatusCodeForMediaRetry;
|
|
782
1041
|
const MEDIA_RETRY_STATUS_MAP = {
|
|
783
|
-
[proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
|
|
784
|
-
[proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
|
|
785
|
-
[proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
|
|
786
|
-
[proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
|
|
1042
|
+
[WAProto_1.proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
|
|
1043
|
+
[WAProto_1.proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
|
|
1044
|
+
[WAProto_1.proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
|
|
1045
|
+
[WAProto_1.proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418,
|
|
787
1046
|
};
|
|
788
|
-
|
|
1047
|
+
function __importStar(arg0) {
|
|
1048
|
+
throw new Error('Function not implemented.');
|
|
1049
|
+
}
|