jagproject 26.3.23 → 26.3.26
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 +3 -4
- package/WAProto/WAProto.proto +1215 -511
- package/WAProto/fix-imports.js +73 -0
- package/WAProto/index.d.ts +14017 -0
- package/WAProto/index.js +64857 -145167
- package/engine-requirements.js +4 -7
- package/lib/Defaults/index.d.ts +74 -0
- package/lib/Defaults/index.js +49 -35
- package/lib/Defaults/phonenumber-mcc.json +223 -0
- package/lib/Defaults/wileys-version.json +2 -2
- package/lib/Signal/Group/ciphertext-message.d.ts +10 -0
- package/lib/Signal/Group/group-session-builder.d.ts +15 -0
- package/lib/Signal/Group/group-session-builder.js +5 -3
- package/lib/Signal/Group/group_cipher.d.ts +17 -0
- package/lib/Signal/Group/group_cipher.js +35 -46
- package/lib/Signal/Group/index.d.ts +12 -0
- package/lib/Signal/Group/index.js +21 -21
- package/lib/Signal/Group/keyhelper.d.ts +11 -0
- package/lib/Signal/Group/keyhelper.js +2 -2
- package/lib/Signal/Group/sender-chain-key.d.ts +14 -0
- package/lib/Signal/Group/sender-chain-key.js +5 -10
- package/lib/Signal/Group/sender-key-distribution-message.d.ts +17 -0
- package/lib/Signal/Group/sender-key-distribution-message.js +7 -7
- package/lib/Signal/Group/sender-key-message.d.ts +19 -0
- package/lib/Signal/Group/sender-key-message.js +8 -8
- package/lib/Signal/Group/sender-key-name.d.ts +18 -0
- package/lib/Signal/Group/sender-key-record.d.ts +31 -0
- package/lib/Signal/Group/sender-key-record.js +7 -16
- package/lib/Signal/Group/sender-key-state.d.ts +39 -0
- package/lib/Signal/Group/sender-key-state.js +25 -37
- package/lib/Signal/Group/sender-message-key.d.ts +12 -0
- package/lib/Signal/Group/sender-message-key.js +2 -2
- package/lib/Signal/libsignal.d.ts +5 -0
- package/lib/Signal/libsignal.js +358 -54
- package/lib/Signal/lid-mapping.d.ts +19 -0
- package/lib/Signal/lid-mapping.js +274 -0
- package/lib/Socket/Client/index.d.ts +3 -0
- package/lib/Socket/Client/index.js +2 -2
- package/lib/Socket/Client/types.d.ts +16 -0
- package/lib/Socket/Client/types.js +1 -0
- package/lib/Socket/Client/websocket.d.ts +13 -0
- package/lib/Socket/Client/websocket.js +18 -30
- package/lib/Socket/business.d.ts +202 -0
- package/lib/Socket/business.js +160 -38
- package/lib/Socket/chats.d.ts +111 -0
- package/lib/Socket/chats.js +497 -314
- package/lib/Socket/communities.d.ts +258 -0
- package/lib/Socket/communities.js +438 -0
- package/lib/Socket/community.js +333 -0
- package/lib/Socket/groups.d.ts +150 -0
- package/lib/Socket/groups.js +229 -91
- package/lib/Socket/index.d.ts +245 -0
- package/lib/Socket/index.js +9 -6
- package/lib/Socket/messages-recv.d.ts +187 -0
- package/lib/Socket/messages-recv.js +1105 -501
- package/lib/Socket/messages-send.d.ts +183 -0
- package/lib/Socket/messages-send.js +1181 -501
- package/lib/Socket/mex.d.ts +3 -0
- package/lib/Socket/mex.js +45 -0
- package/lib/Socket/newsletter.d.ts +160 -0
- package/lib/Socket/newsletter.js +227 -200
- package/lib/Socket/socket.d.ts +55 -0
- package/lib/Socket/socket.js +507 -206
- package/lib/Socket/usync.js +6 -6
- package/lib/Store/index.js +17 -5
- package/lib/Store/make-cache-manager-store.js +83 -0
- package/lib/Store/make-in-memory-store.js +48 -89
- package/lib/Store/make-ordered-dictionary.js +1 -1
- package/lib/Types/Auth.d.ts +116 -0
- package/lib/Types/Bussines.d.ts +25 -0
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Call.d.ts +15 -0
- package/lib/Types/Chat.d.ts +123 -0
- package/lib/Types/Chat.js +7 -1
- package/lib/Types/Contact.d.ts +24 -0
- package/lib/Types/Events.d.ts +237 -0
- package/lib/Types/Events.js +1 -0
- package/lib/Types/GroupMetadata.d.ts +67 -0
- package/lib/Types/Label.d.ts +47 -0
- package/lib/Types/Label.js +1 -3
- package/lib/Types/LabelAssociation.d.ts +30 -0
- package/lib/Types/LabelAssociation.js +1 -3
- package/lib/Types/Message.d.ts +305 -0
- package/lib/Types/Message.js +9 -5
- package/lib/Types/MexUpdates.js +11 -0
- package/lib/Types/Newsletter.d.ts +135 -0
- package/lib/Types/Newsletter.js +36 -11
- package/lib/Types/Product.d.ts +79 -0
- package/lib/Types/Signal.d.ts +76 -0
- package/lib/Types/Signal.js +1 -0
- package/lib/Types/Socket.d.ts +133 -0
- package/lib/Types/Socket.js +1 -0
- package/lib/Types/State.d.ts +39 -0
- package/lib/Types/State.js +12 -0
- package/lib/Types/USync.d.ts +26 -0
- package/lib/Types/USync.js +1 -0
- package/lib/Types/index.d.ts +65 -0
- package/lib/Types/index.js +14 -14
- package/lib/Utils/audioToBuffer.js +31 -0
- package/lib/Utils/auth-utils.d.ts +19 -0
- package/lib/Utils/auth-utils.js +222 -123
- package/lib/Utils/baileys-event-stream.js +60 -0
- package/lib/Utils/bridge-runtime.d.ts +1 -0
- package/lib/Utils/bridge-runtime.js +14 -0
- package/lib/Utils/browser-utils.d.ts +4 -0
- package/lib/Utils/browser-utils.js +38 -29
- package/lib/Utils/business.d.ts +23 -0
- package/lib/Utils/business.js +54 -48
- package/lib/Utils/chat-utils.d.ts +70 -0
- package/lib/Utils/chat-utils.js +284 -189
- package/lib/Utils/crypto.d.ts +37 -0
- package/lib/Utils/crypto.js +16 -41
- package/lib/Utils/decode-wa-message.d.ts +48 -0
- package/lib/Utils/decode-wa-message.js +128 -48
- package/lib/Utils/event-buffer.d.ts +34 -0
- package/lib/Utils/event-buffer.js +124 -62
- package/lib/Utils/generics.d.ts +91 -0
- package/lib/Utils/generics.js +154 -138
- package/lib/Utils/history.d.ts +22 -0
- package/lib/Utils/history.js +77 -34
- package/lib/Utils/identity-change-handler.d.ts +37 -0
- package/lib/Utils/identity-change-handler.js +54 -0
- package/lib/Utils/index.d.ts +22 -0
- package/lib/Utils/index.js +32 -19
- package/lib/Utils/link-preview.d.ts +21 -0
- package/lib/Utils/link-preview.js +12 -17
- package/lib/Utils/logger.d.ts +13 -0
- package/lib/Utils/lt-hash.d.ts +8 -0
- package/lib/Utils/lt-hash.js +2 -43
- package/lib/Utils/make-mutex.d.ts +9 -0
- package/lib/Utils/make-mutex.js +21 -27
- package/lib/Utils/message-retry-manager.d.ts +110 -0
- package/lib/Utils/message-retry-manager.js +143 -45
- package/lib/Utils/messages-media.d.ts +130 -0
- package/lib/Utils/messages-media.js +429 -502
- package/lib/Utils/messages-newsletter.d.ts +84 -0
- package/lib/Utils/messages-newsletter.js +295 -0
- package/lib/Utils/messages.d.ts +92 -0
- package/lib/Utils/messages.js +1025 -674
- package/lib/Utils/noise-handler.d.ts +20 -0
- package/lib/Utils/noise-handler.js +145 -91
- package/lib/Utils/pre-key-manager.d.ts +28 -0
- package/lib/Utils/pre-key-manager.js +112 -0
- package/lib/Utils/process-message.d.ts +60 -0
- package/lib/Utils/process-message.js +316 -184
- package/lib/Utils/reporting-utils.d.ts +11 -0
- package/lib/Utils/reporting-utils.js +262 -0
- package/lib/Utils/resolve-jid.d.ts +43 -0
- package/lib/Utils/resolve-jid.js +95 -0
- package/lib/Utils/rust-bridge-shim.d.ts +22 -0
- package/lib/Utils/rust-bridge-shim.js +70 -0
- package/lib/Utils/serial-task-queue.js +29 -0
- package/lib/Utils/signal.d.ts +34 -0
- package/lib/Utils/signal.js +56 -39
- package/lib/Utils/streamToBuffer.js +17 -0
- package/lib/Utils/sync-action-utils.d.ts +19 -0
- package/lib/Utils/sync-action-utils.js +52 -0
- package/lib/Utils/tc-token-utils.d.ts +12 -0
- package/lib/Utils/tc-token-utils.js +20 -0
- package/lib/Utils/use-mongo-file-auth-state.js +71 -0
- package/lib/Utils/use-multi-file-auth-state.d.ts +13 -0
- package/lib/Utils/use-multi-file-auth-state.js +11 -12
- package/lib/Utils/use-single-file-auth-state.js +73 -0
- package/lib/Utils/validate-connection.d.ts +11 -0
- package/lib/Utils/validate-connection.js +59 -82
- package/lib/Utils/wileys-event-stream.js +1 -61
- package/lib/WABinary/constants.d.ts +28 -0
- package/lib/WABinary/decode.d.ts +7 -0
- package/lib/WABinary/decode.js +39 -4
- package/lib/WABinary/encode.d.ts +3 -0
- package/lib/WABinary/encode.js +17 -11
- package/lib/WABinary/generic-utils.d.ts +15 -0
- package/lib/WABinary/generic-utils.js +46 -18
- package/lib/WABinary/index.d.ts +6 -0
- package/lib/WABinary/index.js +9 -5
- package/lib/WABinary/jid-utils.d.ts +48 -0
- package/lib/WABinary/jid-utils.js +67 -37
- package/lib/WABinary/types.d.ts +19 -0
- package/lib/WABinary/types.js +34 -0
- package/lib/WAM/BinaryInfo.d.ts +9 -0
- package/lib/WAM/constants.d.ts +40 -0
- package/lib/WAM/constants.js +19183 -11678
- package/lib/WAM/encode.d.ts +3 -0
- package/lib/WAM/encode.js +15 -17
- package/lib/WAM/index.d.ts +4 -0
- package/lib/WAM/index.js +3 -3
- package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts +10 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +6 -6
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.d.ts +23 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +9 -9
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +13 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +6 -6
- package/lib/WAUSync/Protocols/USyncStatusProtocol.d.ts +13 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +7 -8
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.d.ts +26 -0
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +18 -17
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.d.ts +10 -0
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +11 -3
- package/lib/WAUSync/Protocols/index.d.ts +5 -0
- package/lib/WAUSync/Protocols/index.js +6 -4
- package/lib/WAUSync/USyncQuery.d.ts +29 -0
- package/lib/WAUSync/USyncQuery.js +38 -30
- package/lib/WAUSync/USyncUser.d.ts +13 -0
- package/lib/WAUSync/index.d.ts +4 -0
- package/lib/WAUSync/index.js +3 -3
- package/lib/index.d.ts +12 -0
- package/lib/index.js +3 -5
- package/package.json +7 -4
- package/LICENSE +0 -21
|
@@ -32,61 +32,80 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.getStatusCodeForMediaRetry = exports.decryptMediaRetryData = exports.decodeMediaRetryNode = exports.encryptMediaRetryRequest = exports.getWAUploadToServer = exports.
|
|
36
|
+
exports.prepareStream = exports.getStatusCodeForMediaRetry = exports.decryptMediaRetryData = exports.decodeMediaRetryNode = exports.encryptMediaRetryRequest = exports.getWAUploadToServer = exports.uploadWithNodeHttp = exports.downloadEncryptedContent = exports.downloadContentFromMessage = exports.getUrlFromDirectPath = exports.encryptedStream = exports.getHttpStream = exports.getStream = exports.toBuffer = exports.toReadable = exports.mediaMessageSHA256B64 = exports.generateProfilePicture = exports.encodeBase64EncodedStringForUpload = exports.extractImageThumb = exports.getRawMediaUploadData = exports.hkdfInfoKey = void 0;
|
|
40
37
|
exports.getMediaKeys = getMediaKeys;
|
|
41
|
-
exports.uploadFile = uploadFile;
|
|
42
|
-
exports.vid2jpg = vid2jpg;
|
|
43
38
|
exports.getAudioDuration = getAudioDuration;
|
|
44
39
|
exports.getAudioWaveform = getAudioWaveform;
|
|
45
40
|
exports.generateThumbnail = generateThumbnail;
|
|
46
41
|
exports.extensionForMediaMessage = extensionForMediaMessage;
|
|
42
|
+
const events_1 = require("events");
|
|
47
43
|
const boom_1 = require("@hapi/boom");
|
|
48
|
-
const axios_1 = __importDefault(require("axios"));
|
|
49
|
-
const form_data_1 = __importDefault(require("form-data"));
|
|
50
|
-
const cheerio = __importStar(require("cheerio"));
|
|
51
44
|
const Crypto = __importStar(require("crypto"));
|
|
52
|
-
const events_1 = require("events");
|
|
53
45
|
const fs_1 = require("fs");
|
|
54
46
|
const os_1 = require("os");
|
|
55
47
|
const path_1 = require("path");
|
|
56
|
-
const jimp_1 = __importDefault(require("jimp"));
|
|
57
48
|
const stream_1 = require("stream");
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
49
|
+
const url_1 = require("url");
|
|
50
|
+
const index_js_1 = require("../../WAProto/index.js");
|
|
51
|
+
const index_js_2 = require("../Defaults/index.js");
|
|
52
|
+
const index_js_3 = require("../WABinary/index.js");
|
|
53
|
+
const crypto_js_1 = require("./crypto.js");
|
|
54
|
+
const generics_js_1 = require("./generics.js");
|
|
64
55
|
const getTmpFilesDirectory = () => (0, os_1.tmpdir)();
|
|
65
56
|
const getImageProcessingLibrary = async () => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const jimp = await (Promise.resolve().then(() => __importStar(require('jimp'))).catch(() => { }));
|
|
69
|
-
return jimp;
|
|
70
|
-
})(),
|
|
71
|
-
(async () => {
|
|
72
|
-
const sharp = await (Promise.resolve().then(() => __importStar(require('sharp'))).catch(() => { }));
|
|
73
|
-
return sharp;
|
|
74
|
-
})()
|
|
75
|
-
]);
|
|
57
|
+
//@ts-ignore
|
|
58
|
+
const [jimp, sharp] = await Promise.all([Promise.resolve().then(() => __importStar(require('jimp'))).catch(() => { }), Promise.resolve().then(() => __importStar(require('sharp'))).catch(() => { })]);
|
|
76
59
|
if (sharp) {
|
|
77
60
|
return { sharp };
|
|
78
61
|
}
|
|
79
|
-
const jimp = (_jimp === null || _jimp === void 0 ? void 0 : _jimp.default) || _jimp;
|
|
80
62
|
if (jimp) {
|
|
81
63
|
return { jimp };
|
|
82
64
|
}
|
|
83
65
|
throw new boom_1.Boom('No image processing library available');
|
|
84
66
|
};
|
|
85
67
|
const hkdfInfoKey = (type) => {
|
|
86
|
-
const hkdfInfo =
|
|
68
|
+
const hkdfInfo = index_js_2.MEDIA_HKDF_KEY_MAPPING[type];
|
|
87
69
|
return `WhatsApp ${hkdfInfo} Keys`;
|
|
88
70
|
};
|
|
89
71
|
exports.hkdfInfoKey = hkdfInfoKey;
|
|
72
|
+
const getRawMediaUploadData = async (media, mediaType, logger) => {
|
|
73
|
+
const { stream } = await (0, exports.getStream)(media);
|
|
74
|
+
const hasher = Crypto.createHash('sha256');
|
|
75
|
+
const filePath = (0, path_1.join)((0, os_1.tmpdir)(), mediaType + (0, generics_js_1.generateMessageIDV2)());
|
|
76
|
+
const fileWriteStream = (0, fs_1.createWriteStream)(filePath);
|
|
77
|
+
let fileLength = 0;
|
|
78
|
+
try {
|
|
79
|
+
for await (const data of stream) {
|
|
80
|
+
fileLength += data.length;
|
|
81
|
+
hasher.update(data);
|
|
82
|
+
if (!fileWriteStream.write(data)) {
|
|
83
|
+
await (0, events_1.once)(fileWriteStream, 'drain');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
fileWriteStream.end();
|
|
87
|
+
await (0, events_1.once)(fileWriteStream, 'finish');
|
|
88
|
+
stream.destroy();
|
|
89
|
+
const fileSha256 = hasher.digest();
|
|
90
|
+
return {
|
|
91
|
+
filePath: filePath,
|
|
92
|
+
fileSha256,
|
|
93
|
+
fileLength
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
fileWriteStream.destroy();
|
|
98
|
+
stream.destroy();
|
|
99
|
+
try {
|
|
100
|
+
await fs_1.promises.unlink(filePath);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
//
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
exports.getRawMediaUploadData = getRawMediaUploadData;
|
|
90
109
|
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
|
91
110
|
async function getMediaKeys(buffer, mediaType) {
|
|
92
111
|
if (!buffer) {
|
|
@@ -96,215 +115,41 @@ async function getMediaKeys(buffer, mediaType) {
|
|
|
96
115
|
buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64');
|
|
97
116
|
}
|
|
98
117
|
// expand using HKDF to 112 bytes, also pass in the relevant app info
|
|
99
|
-
const expandedMediaKey =
|
|
118
|
+
const expandedMediaKey = (0, crypto_js_1.hkdf)(buffer, 112, { info: (0, exports.hkdfInfoKey)(mediaType) });
|
|
100
119
|
return {
|
|
101
120
|
iv: expandedMediaKey.slice(0, 16),
|
|
102
121
|
cipherKey: expandedMediaKey.slice(16, 48),
|
|
103
|
-
macKey: expandedMediaKey.slice(48, 80)
|
|
122
|
+
macKey: expandedMediaKey.slice(48, 80)
|
|
104
123
|
};
|
|
105
124
|
}
|
|
106
|
-
async function uploadFile(buffer, logger) {
|
|
107
|
-
const { fromBuffer } = await Promise.resolve().then(() => __importStar(require('file-type')));
|
|
108
|
-
const fileType = await fromBuffer(buffer);
|
|
109
|
-
if (!fileType)
|
|
110
|
-
throw new Error("Failed to detect file type.");
|
|
111
|
-
const { ext, mime } = fileType;
|
|
112
|
-
const services = [
|
|
113
|
-
{
|
|
114
|
-
name: "catbox",
|
|
115
|
-
url: "https://catbox.moe/user/api.php",
|
|
116
|
-
buildForm: () => {
|
|
117
|
-
const form = new form_data_1.default();
|
|
118
|
-
form.append("fileToUpload", buffer, {
|
|
119
|
-
filename: `file.${ext}`,
|
|
120
|
-
contentType: mime || "application/octet-stream"
|
|
121
|
-
});
|
|
122
|
-
form.append("reqtype", "fileupload");
|
|
123
|
-
return form;
|
|
124
|
-
},
|
|
125
|
-
parseResponse: res => res.data
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: "pdi.moe",
|
|
129
|
-
url: "https://scdn.pdi.moe/upload",
|
|
130
|
-
buildForm: () => {
|
|
131
|
-
const form = new form_data_1.default();
|
|
132
|
-
form.append("file", buffer, {
|
|
133
|
-
filename: `file.${ext}`,
|
|
134
|
-
contentType: mime
|
|
135
|
-
});
|
|
136
|
-
return form;
|
|
137
|
-
},
|
|
138
|
-
parseResponse: res => res.data.result.url
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: "qu.ax",
|
|
142
|
-
url: "https://qu.ax/upload.php",
|
|
143
|
-
buildForm: () => {
|
|
144
|
-
const form = new form_data_1.default();
|
|
145
|
-
form.append("files[]", buffer, {
|
|
146
|
-
filename: `file.${ext}`,
|
|
147
|
-
contentType: mime || "application/octet-stream"
|
|
148
|
-
});
|
|
149
|
-
return form;
|
|
150
|
-
},
|
|
151
|
-
parseResponse: res => {
|
|
152
|
-
var _a, _b, _c;
|
|
153
|
-
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))
|
|
154
|
-
throw new Error("Failed to get URL from qu.ax");
|
|
155
|
-
return res.data.files[0].url;
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
name: "uguu.se",
|
|
160
|
-
url: "https://uguu.se/upload.php",
|
|
161
|
-
buildForm: () => {
|
|
162
|
-
const form = new form_data_1.default();
|
|
163
|
-
form.append("files[]", buffer, {
|
|
164
|
-
filename: `file.${ext}`,
|
|
165
|
-
contentType: mime || "application/octet-stream"
|
|
166
|
-
});
|
|
167
|
-
return form;
|
|
168
|
-
},
|
|
169
|
-
parseResponse: res => {
|
|
170
|
-
var _a, _b, _c;
|
|
171
|
-
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))
|
|
172
|
-
throw new Error("Failed to get URL from uguu.se");
|
|
173
|
-
return res.data.files[0].url;
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
name: "tmpfiles",
|
|
178
|
-
url: "https://tmpfiles.org/api/v1/upload",
|
|
179
|
-
buildForm: () => {
|
|
180
|
-
const form = new form_data_1.default();
|
|
181
|
-
form.append("file", buffer, {
|
|
182
|
-
filename: `file.${ext}`,
|
|
183
|
-
contentType: mime
|
|
184
|
-
});
|
|
185
|
-
return form;
|
|
186
|
-
},
|
|
187
|
-
parseResponse: res => {
|
|
188
|
-
const match = res.data.data.url.match(/https:\/\/tmpfiles\.org\/(.*)/);
|
|
189
|
-
if (!match)
|
|
190
|
-
throw new Error("Failed to parse tmpfiles URL.");
|
|
191
|
-
return `https://tmpfiles.org/dl/${match[1]}`;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
];
|
|
195
|
-
for (const service of services) {
|
|
196
|
-
try {
|
|
197
|
-
const form = service.buildForm();
|
|
198
|
-
const res = await axios_1.default.post(service.url, form, {
|
|
199
|
-
headers: form.getHeaders()
|
|
200
|
-
});
|
|
201
|
-
const url = service.parseResponse(res);
|
|
202
|
-
return url;
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
logger === null || logger === void 0 ? void 0 : logger.debug(`[${service.name}] eror:`, (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
throw new Error("All upload services failed.");
|
|
209
|
-
}
|
|
210
|
-
async function vid2jpg(videoUrl) {
|
|
211
|
-
try {
|
|
212
|
-
const { data } = await axios_1.default.get(`https://ezgif.com/video-to-jpg?url=${encodeURIComponent(videoUrl)}`);
|
|
213
|
-
const $ = cheerio.load(data);
|
|
214
|
-
const fileToken = $('input[name="file"]').attr("value");
|
|
215
|
-
if (!fileToken) {
|
|
216
|
-
throw new Error("Failed to retrieve file token. The video URL may be invalid or inaccessible.");
|
|
217
|
-
}
|
|
218
|
-
const formData = new URLSearchParams();
|
|
219
|
-
formData.append("file", fileToken);
|
|
220
|
-
formData.append("end", "1");
|
|
221
|
-
formData.append("video-to-jpg", "Convert to JPG!");
|
|
222
|
-
const convert = await axios_1.default.post(`https://ezgif.com/video-to-jpg/${fileToken}`, formData);
|
|
223
|
-
const $2 = cheerio.load(convert.data);
|
|
224
|
-
let imageUrl = $2("#output img").first().attr("src");
|
|
225
|
-
if (!imageUrl) {
|
|
226
|
-
throw new Error("Could not locate the converted image output.");
|
|
227
|
-
}
|
|
228
|
-
if (imageUrl.startsWith("//")) {
|
|
229
|
-
imageUrl = "https:" + imageUrl;
|
|
230
|
-
}
|
|
231
|
-
else if (imageUrl.startsWith("/")) {
|
|
232
|
-
const cdnMatch = imageUrl.match(/\/(s\d+\..+?)\/.*/);
|
|
233
|
-
if (cdnMatch) {
|
|
234
|
-
imageUrl = "https://" + imageUrl.slice(2);
|
|
235
|
-
}
|
|
236
|
-
else {
|
|
237
|
-
imageUrl = "https://ezgif.com" + imageUrl;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return imageUrl;
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
243
|
-
throw new Error("Failed to convert video to JPG: " + error.message);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Extracts video thumbnail using FFmpeg
|
|
248
|
-
*/
|
|
249
|
-
const extractVideoThumb = async (videoPath, time = '00:00:00', size = { width: 256 }) => {
|
|
250
|
-
return new Promise((resolve, reject) => {
|
|
251
|
-
const args = [
|
|
252
|
-
'-ss', time,
|
|
253
|
-
'-i', videoPath,
|
|
254
|
-
'-y',
|
|
255
|
-
'-vf', `scale=${size.width}:-1`,
|
|
256
|
-
'-vframes', '1',
|
|
257
|
-
'-f', 'image2',
|
|
258
|
-
'-vcodec', 'mjpeg',
|
|
259
|
-
'pipe:1'
|
|
260
|
-
];
|
|
261
|
-
const ffmpeg = (0, child_process_1.spawn)('ffmpeg', args);
|
|
262
|
-
const chunks = [];
|
|
263
|
-
let errorOutput = '';
|
|
264
|
-
ffmpeg.stdout.on('data', chunk => chunks.push(chunk));
|
|
265
|
-
ffmpeg.stderr.on('data', data => {
|
|
266
|
-
errorOutput += data.toString();
|
|
267
|
-
});
|
|
268
|
-
ffmpeg.on('error', reject);
|
|
269
|
-
ffmpeg.on('close', code => {
|
|
270
|
-
if (code === 0) return resolve(Buffer.concat(chunks));
|
|
271
|
-
reject(new Error(`ffmpeg exited with code ${code}\n${errorOutput}`));
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
};
|
|
275
|
-
exports.extractVideoThumb = extractVideoThumb;
|
|
276
125
|
const extractImageThumb = async (bufferOrFilePath, width = 32) => {
|
|
277
|
-
|
|
126
|
+
// TODO: Move entirely to sharp, removing jimp as it supports readable streams
|
|
127
|
+
// This will have positive speed and performance impacts as well as minimizing RAM usage.
|
|
278
128
|
if (bufferOrFilePath instanceof stream_1.Readable) {
|
|
279
129
|
bufferOrFilePath = await (0, exports.toBuffer)(bufferOrFilePath);
|
|
280
130
|
}
|
|
281
131
|
const lib = await getImageProcessingLibrary();
|
|
282
|
-
if ('sharp' in lib && typeof
|
|
132
|
+
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
|
|
283
133
|
const img = lib.sharp.default(bufferOrFilePath);
|
|
284
134
|
const dimensions = await img.metadata();
|
|
285
|
-
const buffer = await img
|
|
286
|
-
.resize(width)
|
|
287
|
-
.jpeg({ quality: 50 })
|
|
288
|
-
.toBuffer();
|
|
135
|
+
const buffer = await img.resize(width).jpeg({ quality: 50 }).toBuffer();
|
|
289
136
|
return {
|
|
290
137
|
buffer,
|
|
291
138
|
original: {
|
|
292
139
|
width: dimensions.width,
|
|
293
|
-
height: dimensions.height
|
|
294
|
-
}
|
|
140
|
+
height: dimensions.height
|
|
141
|
+
}
|
|
295
142
|
};
|
|
296
143
|
}
|
|
297
|
-
else if ('jimp' in lib && typeof
|
|
298
|
-
const
|
|
299
|
-
const jimp = await read(bufferOrFilePath);
|
|
144
|
+
else if ('jimp' in lib && typeof lib.jimp?.Jimp === 'object') {
|
|
145
|
+
const jimp = await lib.jimp.Jimp.read(bufferOrFilePath);
|
|
300
146
|
const dimensions = {
|
|
301
|
-
width: jimp.
|
|
302
|
-
height: jimp.
|
|
147
|
+
width: jimp.width,
|
|
148
|
+
height: jimp.height
|
|
303
149
|
};
|
|
304
150
|
const buffer = await jimp
|
|
305
|
-
.
|
|
306
|
-
.
|
|
307
|
-
.getBufferAsync(MIME_JPEG);
|
|
151
|
+
.resize({ w: width, mode: lib.jimp.ResizeStrategy.BILINEAR })
|
|
152
|
+
.getBuffer('image/jpeg', { quality: 50 });
|
|
308
153
|
return {
|
|
309
154
|
buffer,
|
|
310
155
|
original: dimensions
|
|
@@ -315,176 +160,130 @@ const extractImageThumb = async (bufferOrFilePath, width = 32) => {
|
|
|
315
160
|
}
|
|
316
161
|
};
|
|
317
162
|
exports.extractImageThumb = extractImageThumb;
|
|
318
|
-
const encodeBase64EncodedStringForUpload = (b64) =>
|
|
319
|
-
.replace(/\+/g, '-')
|
|
320
|
-
.replace(/\//g, '_')
|
|
321
|
-
.replace(/\=+$/, '')));
|
|
163
|
+
const encodeBase64EncodedStringForUpload = (b64) => encodeURIComponent(b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''));
|
|
322
164
|
exports.encodeBase64EncodedStringForUpload = encodeBase64EncodedStringForUpload;
|
|
323
|
-
const generateProfilePicture = async (mediaUpload) => {
|
|
324
|
-
let
|
|
325
|
-
|
|
165
|
+
const generateProfilePicture = async (mediaUpload, dimensions) => {
|
|
166
|
+
let buffer;
|
|
167
|
+
const { width: w = 640, height: h = 640 } = dimensions || {};
|
|
326
168
|
if (Buffer.isBuffer(mediaUpload)) {
|
|
327
|
-
|
|
169
|
+
buffer = mediaUpload;
|
|
328
170
|
}
|
|
329
|
-
else
|
|
330
|
-
|
|
171
|
+
else {
|
|
172
|
+
// Use getStream to handle all WAMediaUpload types (Buffer, Stream, URL)
|
|
173
|
+
const { stream } = await (0, exports.getStream)(mediaUpload);
|
|
174
|
+
// Convert the resulting stream to a buffer
|
|
175
|
+
buffer = await (0, exports.toBuffer)(stream);
|
|
176
|
+
}
|
|
177
|
+
const lib = await getImageProcessingLibrary();
|
|
178
|
+
let img;
|
|
179
|
+
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
|
|
180
|
+
img = lib.sharp
|
|
181
|
+
.default(buffer)
|
|
182
|
+
.resize(w, h)
|
|
183
|
+
.jpeg({
|
|
184
|
+
quality: 50
|
|
185
|
+
})
|
|
186
|
+
.toBuffer();
|
|
187
|
+
}
|
|
188
|
+
else if ('jimp' in lib && typeof lib.jimp?.Jimp === 'function') {
|
|
189
|
+
const jimp = await lib.jimp.Jimp.read(buffer);
|
|
190
|
+
const min = Math.min(jimp.width, jimp.height);
|
|
191
|
+
const cropped = jimp.crop({ x: 0, y: 0, w: min, h: min });
|
|
192
|
+
img = cropped.resize({ w, h, mode: lib.jimp.ResizeStrategy.BILINEAR }).getBuffer('image/jpeg', { quality: 50 });
|
|
331
193
|
}
|
|
332
194
|
else {
|
|
333
|
-
|
|
195
|
+
throw new boom_1.Boom('No image processing library available');
|
|
334
196
|
}
|
|
335
|
-
const jimp = await jimp_1.default.read(bufferOrFilePath);
|
|
336
|
-
const cropped = jimp.getWidth() > jimp.getHeight() ? jimp.resize(550, -1) : jimp.resize(-1, 650);
|
|
337
|
-
img = cropped
|
|
338
|
-
.quality(100)
|
|
339
|
-
.getBufferAsync(jimp_1.default.MIME_JPEG);
|
|
340
197
|
return {
|
|
341
|
-
img: await img
|
|
198
|
+
img: await img
|
|
342
199
|
};
|
|
343
200
|
};
|
|
344
201
|
exports.generateProfilePicture = generateProfilePicture;
|
|
345
202
|
/** gets the SHA256 of the given media message */
|
|
346
203
|
const mediaMessageSHA256B64 = (message) => {
|
|
347
204
|
const media = Object.values(message)[0];
|
|
348
|
-
return
|
|
205
|
+
return media?.fileSha256 && Buffer.from(media.fileSha256).toString('base64');
|
|
349
206
|
};
|
|
350
207
|
exports.mediaMessageSHA256B64 = mediaMessageSHA256B64;
|
|
351
|
-
async function getAudioDuration(buffer) {
|
|
208
|
+
async function getAudioDuration(buffer, mimeType) {
|
|
352
209
|
const musicMetadata = await Promise.resolve().then(() => __importStar(require('music-metadata')));
|
|
353
210
|
let metadata;
|
|
354
211
|
const options = {
|
|
355
|
-
duration: true
|
|
212
|
+
duration: true,
|
|
213
|
+
...(mimeType ? { mimeType } : {})
|
|
356
214
|
};
|
|
357
215
|
if (Buffer.isBuffer(buffer)) {
|
|
358
|
-
metadata = await musicMetadata.parseBuffer(buffer, undefined, options);
|
|
216
|
+
metadata = await musicMetadata.parseBuffer(buffer, mimeType || undefined, options);
|
|
359
217
|
}
|
|
360
218
|
else if (typeof buffer === 'string') {
|
|
361
|
-
|
|
219
|
+
// parseFile tidak support mimeType langsung, tapi kita bisa baca sebagai buffer
|
|
220
|
+
// supaya mimeType hint bisa dipass — penting untuk m4a/aac yang nama filenya tanpa ekstensi
|
|
221
|
+
try {
|
|
222
|
+
metadata = await musicMetadata.parseFile(buffer, options);
|
|
223
|
+
}
|
|
224
|
+
catch (_) {
|
|
225
|
+
// fallback: baca file sebagai buffer lalu parse dengan mimeType
|
|
226
|
+
const { readFileSync } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
227
|
+
const buf = readFileSync(buffer);
|
|
228
|
+
metadata = await musicMetadata.parseBuffer(buf, mimeType || undefined, options);
|
|
229
|
+
}
|
|
362
230
|
}
|
|
363
231
|
else {
|
|
364
|
-
metadata = await musicMetadata.parseStream(buffer, undefined, options);
|
|
232
|
+
metadata = await musicMetadata.parseStream(buffer, mimeType || undefined, options);
|
|
365
233
|
}
|
|
366
|
-
|
|
234
|
+
const dur = metadata.format.duration;
|
|
235
|
+
// Jangan return NaN/undefined — WhatsApp akan tampilkan "Loading..." kalau seconds invalid
|
|
236
|
+
return (typeof dur === 'number' && !isNaN(dur) && isFinite(dur)) ? Math.round(dur) : 0;
|
|
367
237
|
}
|
|
368
|
-
|
|
238
|
+
/**
|
|
239
|
+
referenced from and modifying https://github.com/wppconnect-team/wa-js/blob/main/src/chat/functions/prepareAudioWaveform.ts
|
|
240
|
+
*/
|
|
369
241
|
async function getAudioWaveform(buffer, logger) {
|
|
370
242
|
try {
|
|
371
|
-
//
|
|
372
|
-
const {
|
|
373
|
-
const ffmpeg = require('fluent-ffmpeg');
|
|
374
|
-
// try to use ffmpeg-static if available (bundled binary)
|
|
375
|
-
try {
|
|
376
|
-
const ffmpegPath = require('ffmpeg-static');
|
|
377
|
-
if (ffmpegPath) {
|
|
378
|
-
ffmpeg.setFfmpegPath(ffmpegPath);
|
|
379
|
-
}
|
|
380
|
-
} catch (e) {
|
|
381
|
-
// ffmpeg-static not installed — hope system ffmpeg exists in PATH
|
|
382
|
-
logger?.debug?.('ffmpeg-static not available, relying on system ffmpeg');
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// normalize input to Buffer
|
|
243
|
+
// @ts-ignore
|
|
244
|
+
const { default: decoder } = await Promise.resolve().then(() => __importStar(require('audio-decode')));
|
|
386
245
|
let audioData;
|
|
387
246
|
if (Buffer.isBuffer(buffer)) {
|
|
388
247
|
audioData = buffer;
|
|
389
|
-
} else if (typeof buffer === 'string') {
|
|
390
|
-
// path given
|
|
391
|
-
const fs = require('fs');
|
|
392
|
-
audioData = await (async () => {
|
|
393
|
-
if (fs.existsSync(buffer)) return fs.readFileSync(buffer);
|
|
394
|
-
// otherwise try to read as URL/stream via toBuffer helper if available
|
|
395
|
-
return await exports.toBuffer(buffer);
|
|
396
|
-
})();
|
|
397
|
-
} else {
|
|
398
|
-
audioData = await exports.toBuffer(buffer);
|
|
399
248
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
// fallback: try a simpler approach using audio-decode if available
|
|
417
|
-
try {
|
|
418
|
-
(async () => {
|
|
419
|
-
const { default: decoder } = await eval('import("audio-decode")');
|
|
420
|
-
const audioBuf = await exports.toBuffer(audioData);
|
|
421
|
-
const audioDecoded = await decoder(audioBuf);
|
|
422
|
-
const raw = audioDecoded.getChannelData(0);
|
|
423
|
-
const result = normalizeToUint8(raw);
|
|
424
|
-
resolve(result);
|
|
425
|
-
})();
|
|
426
|
-
} catch (e) {
|
|
427
|
-
reject(err);
|
|
428
|
-
}
|
|
429
|
-
})
|
|
430
|
-
.on('end', () => {
|
|
431
|
-
try {
|
|
432
|
-
const buffer = Buffer.concat(chunks);
|
|
433
|
-
// buffer contains 16-bit signed PCM little endian
|
|
434
|
-
// convert to Float32 array (-1..1) first
|
|
435
|
-
const samples = new Int16Array(buffer.buffer, buffer.byteOffset, Math.floor(buffer.length / 2));
|
|
436
|
-
const floatSamples = new Float32Array(samples.length);
|
|
437
|
-
for (let i = 0; i < samples.length; i++) {
|
|
438
|
-
floatSamples[i] = samples[i] / 32768; // normalize int16 to [-1,1]
|
|
439
|
-
}
|
|
440
|
-
const result = normalizeToUint8(floatSamples);
|
|
441
|
-
resolve(result);
|
|
442
|
-
} catch (e) {
|
|
443
|
-
reject(e);
|
|
444
|
-
}
|
|
445
|
-
})
|
|
446
|
-
.pipe()
|
|
447
|
-
.on('data', (c) => chunks.push(c));
|
|
448
|
-
} catch (e) {
|
|
449
|
-
reject(e);
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
} catch (e) {
|
|
453
|
-
logger?.debug?.(e);
|
|
454
|
-
return undefined;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// helper: normalize float array (-1..1) into Uint8Array (0..100 per bar)
|
|
459
|
-
function normalizeToUint8(data, samples = 64) {
|
|
460
|
-
try {
|
|
461
|
-
const blockSize = Math.floor(data.length / samples) || 1;
|
|
462
|
-
const filtered = [];
|
|
249
|
+
else if (typeof buffer === 'string') {
|
|
250
|
+
const rStream = (0, fs_1.createReadStream)(buffer);
|
|
251
|
+
audioData = await (0, exports.toBuffer)(rStream);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
audioData = await (0, exports.toBuffer)(buffer);
|
|
255
|
+
}
|
|
256
|
+
// Skip audio-decode for large buffers (> 3MB)
|
|
257
|
+
if (audioData.length > 3 * 1024 * 1024) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const audioBuffer = await decoder(audioData);
|
|
261
|
+
const rawData = audioBuffer.getChannelData(0);
|
|
262
|
+
const samples = 64;
|
|
263
|
+
const blockSize = Math.floor(rawData.length / samples);
|
|
264
|
+
const filteredData = [];
|
|
463
265
|
for (let i = 0; i < samples; i++) {
|
|
464
|
-
|
|
266
|
+
const blockStart = blockSize * i;
|
|
465
267
|
let sum = 0;
|
|
466
|
-
for (let j = 0; j < blockSize
|
|
467
|
-
sum
|
|
268
|
+
for (let j = 0; j < blockSize; j++) {
|
|
269
|
+
sum = sum + Math.abs(rawData[blockStart + j]);
|
|
468
270
|
}
|
|
469
|
-
|
|
271
|
+
filteredData.push(sum / blockSize);
|
|
470
272
|
}
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
273
|
+
const multiplier = Math.pow(Math.max(...filteredData), -1);
|
|
274
|
+
const normalizedData = filteredData.map(n => n * multiplier);
|
|
275
|
+
const waveform = new Uint8Array(normalizedData.map(n => Math.floor(100 * n)));
|
|
276
|
+
return waveform;
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
476
279
|
}
|
|
477
280
|
}
|
|
478
|
-
|
|
479
281
|
const toReadable = (buffer) => {
|
|
480
|
-
const readable = new stream_1.Readable({ read: () => { } });
|
|
282
|
+
const readable = new stream_1.Readable({ read: () => { }, highWaterMark: 64 * 1024 });
|
|
481
283
|
readable.push(buffer);
|
|
482
284
|
readable.push(null);
|
|
483
285
|
return readable;
|
|
484
286
|
};
|
|
485
|
-
// ================================================================
|
|
486
|
-
// Tambahan utilitas media (opsional, jika Wileyss punya bawaan sendiri biarkan bagian bawah tetap ada)
|
|
487
|
-
// ================================================================
|
|
488
287
|
exports.toReadable = toReadable;
|
|
489
288
|
const toBuffer = async (stream) => {
|
|
490
289
|
const chunks = [];
|
|
@@ -502,7 +301,12 @@ const getStream = async (item, opts) => {
|
|
|
502
301
|
if ('stream' in item) {
|
|
503
302
|
return { stream: item.stream, type: 'readable' };
|
|
504
303
|
}
|
|
505
|
-
|
|
304
|
+
const urlStr = item.url.toString();
|
|
305
|
+
if (urlStr.startsWith('data:')) {
|
|
306
|
+
const buffer = Buffer.from(urlStr.split(',')[1], 'base64');
|
|
307
|
+
return { stream: (0, exports.toReadable)(buffer), type: 'buffer' };
|
|
308
|
+
}
|
|
309
|
+
if (urlStr.startsWith('http://') || urlStr.startsWith('https://')) {
|
|
506
310
|
return { stream: await (0, exports.getHttpStream)(item.url, opts), type: 'remote' };
|
|
507
311
|
}
|
|
508
312
|
return { stream: (0, fs_1.createReadStream)(item.url), type: 'file' };
|
|
@@ -510,7 +314,6 @@ const getStream = async (item, opts) => {
|
|
|
510
314
|
exports.getStream = getStream;
|
|
511
315
|
/** generates a thumbnail for a given media, if required */
|
|
512
316
|
async function generateThumbnail(file, mediaType, options) {
|
|
513
|
-
var _a;
|
|
514
317
|
let thumbnail;
|
|
515
318
|
let originalImageDimensions;
|
|
516
319
|
if (mediaType === 'image') {
|
|
@@ -519,37 +322,12 @@ async function generateThumbnail(file, mediaType, options) {
|
|
|
519
322
|
if (original.width && original.height) {
|
|
520
323
|
originalImageDimensions = {
|
|
521
324
|
width: original.width,
|
|
522
|
-
height: original.height
|
|
325
|
+
height: original.height
|
|
523
326
|
};
|
|
524
327
|
}
|
|
525
328
|
}
|
|
526
329
|
else if (mediaType === 'video') {
|
|
527
|
-
|
|
528
|
-
let videoPath = file;
|
|
529
|
-
if (Buffer.isBuffer(file) || file instanceof stream_1.Readable) {
|
|
530
|
-
videoPath = (0, path_1.join)(getTmpFilesDirectory(), (0, generics_1.generateMessageIDV2)() + '.mp4');
|
|
531
|
-
const buffer = Buffer.isBuffer(file) ? file : await (0, exports.toBuffer)(file);
|
|
532
|
-
await fs_1.promises.writeFile(videoPath, buffer);
|
|
533
|
-
}
|
|
534
|
-
const thumbnailBuffer = await (0, exports.extractVideoThumb)(videoPath);
|
|
535
|
-
const imgFilename = (0, path_1.join)(getTmpFilesDirectory(), (0, generics_1.generateMessageIDV2)() + '.jpg');
|
|
536
|
-
await fs_1.promises.writeFile(imgFilename, thumbnailBuffer);
|
|
537
|
-
const { buffer: processedThumbnailBuffer, original } = await (0, exports.extractImageThumb)(imgFilename);
|
|
538
|
-
thumbnail = processedThumbnailBuffer.toString('base64');
|
|
539
|
-
if (original.width && original.height) {
|
|
540
|
-
originalImageDimensions = {
|
|
541
|
-
width: original.width,
|
|
542
|
-
height: original.height,
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
await fs_1.promises.unlink(imgFilename);
|
|
546
|
-
if (videoPath !== file) {
|
|
547
|
-
await fs_1.promises.unlink(videoPath);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
catch (err) {
|
|
551
|
-
(_a = options.logger) === null || _a === void 0 ? void 0 : _a.debug('could not generate video thumb: ' + err);
|
|
552
|
-
}
|
|
330
|
+
// Video thumbnail generation skipped (ffmpeg removed)
|
|
553
331
|
}
|
|
554
332
|
return {
|
|
555
333
|
thumbnail,
|
|
@@ -557,67 +335,33 @@ async function generateThumbnail(file, mediaType, options) {
|
|
|
557
335
|
};
|
|
558
336
|
}
|
|
559
337
|
const getHttpStream = async (url, options = {}) => {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
let bodyPath;
|
|
568
|
-
let didSaveToTmpPath = false;
|
|
569
|
-
try {
|
|
570
|
-
const buffer = await (0, exports.toBuffer)(stream);
|
|
571
|
-
if (type === 'file') {
|
|
572
|
-
bodyPath = media.url;
|
|
573
|
-
}
|
|
574
|
-
else if (saveOriginalFileIfRequired) {
|
|
575
|
-
bodyPath = (0, path_1.join)(getTmpFilesDirectory(), mediaType + (0, generics_1.generateMessageIDV2)());
|
|
576
|
-
(0, fs_1.writeFileSync)(bodyPath, buffer);
|
|
577
|
-
didSaveToTmpPath = true;
|
|
578
|
-
}
|
|
579
|
-
const fileLength = buffer.length;
|
|
580
|
-
const fileSha256 = Crypto.createHash('sha256').update(buffer).digest();
|
|
581
|
-
stream === null || stream === void 0 ? void 0 : stream.destroy();
|
|
582
|
-
logger === null || logger === void 0 ? void 0 : logger.debug('prepare stream data successfully');
|
|
583
|
-
return {
|
|
584
|
-
mediaKey: undefined,
|
|
585
|
-
encWriteStream: buffer,
|
|
586
|
-
fileLength,
|
|
587
|
-
fileSha256,
|
|
588
|
-
fileEncSha256: undefined,
|
|
589
|
-
bodyPath,
|
|
590
|
-
didSaveToTmpPath
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
catch (error) {
|
|
594
|
-
stream.destroy();
|
|
595
|
-
if (didSaveToTmpPath) {
|
|
596
|
-
try {
|
|
597
|
-
await fs_1.promises.unlink(bodyPath);
|
|
598
|
-
}
|
|
599
|
-
catch (err) {
|
|
600
|
-
logger === null || logger === void 0 ? void 0 : logger.error({ err }, 'failed to save to tmp path');
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
throw error;
|
|
338
|
+
const response = await fetch(url.toString(), {
|
|
339
|
+
dispatcher: options.dispatcher,
|
|
340
|
+
method: 'GET',
|
|
341
|
+
headers: options.headers
|
|
342
|
+
});
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
throw new boom_1.Boom(`Failed to fetch stream from ${url}`, { statusCode: response.status, data: { url } });
|
|
604
345
|
}
|
|
346
|
+
// @ts-ignore Node18+ Readable.fromWeb exists
|
|
347
|
+
return response.body instanceof stream_1.Readable ? response.body : stream_1.Readable.fromWeb(response.body);
|
|
605
348
|
};
|
|
606
|
-
exports.
|
|
607
|
-
const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
|
|
349
|
+
exports.getHttpStream = getHttpStream;
|
|
350
|
+
const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
|
|
608
351
|
const { stream, type } = await (0, exports.getStream)(media, opts);
|
|
609
|
-
|
|
352
|
+
let finalStream = stream;
|
|
353
|
+
let opusConverted = false;
|
|
610
354
|
const mediaKey = Crypto.randomBytes(32);
|
|
611
355
|
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType);
|
|
612
|
-
const encWriteStream = new stream_1.Readable({ read: () => { } });
|
|
356
|
+
const encWriteStream = new stream_1.Readable({ read: () => { }, highWaterMark: 64 * 1024 });
|
|
613
357
|
let bodyPath;
|
|
614
358
|
let writeStream;
|
|
615
359
|
let didSaveToTmpPath = false;
|
|
616
360
|
if (type === 'file') {
|
|
617
|
-
bodyPath = media.url;
|
|
361
|
+
bodyPath = media.url?.toString?.() || media.url;
|
|
618
362
|
}
|
|
619
363
|
else if (saveOriginalFileIfRequired) {
|
|
620
|
-
bodyPath = (0, path_1.join)(getTmpFilesDirectory(), mediaType + (0,
|
|
364
|
+
bodyPath = (0, path_1.join)(getTmpFilesDirectory(), mediaType + (0, generics_js_1.generateMessageIDV2)());
|
|
621
365
|
writeStream = (0, fs_1.createWriteStream)(bodyPath);
|
|
622
366
|
didSaveToTmpPath = true;
|
|
623
367
|
}
|
|
@@ -626,12 +370,17 @@ const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfReq
|
|
|
626
370
|
let hmac = Crypto.createHmac('sha256', macKey).update(iv);
|
|
627
371
|
let sha256Plain = Crypto.createHash('sha256');
|
|
628
372
|
let sha256Enc = Crypto.createHash('sha256');
|
|
373
|
+
const onChunk = (buff) => {
|
|
374
|
+
sha256Enc = sha256Enc.update(buff);
|
|
375
|
+
hmac = hmac.update(buff);
|
|
376
|
+
encWriteStream.push(buff);
|
|
377
|
+
};
|
|
629
378
|
try {
|
|
630
|
-
for await (const data of
|
|
379
|
+
for await (const data of finalStream) {
|
|
631
380
|
fileLength += data.length;
|
|
632
|
-
if (type === 'remote'
|
|
633
|
-
|
|
634
|
-
|
|
381
|
+
if (type === 'remote' &&
|
|
382
|
+
opts?.maxContentLength &&
|
|
383
|
+
fileLength + data.length > opts.maxContentLength) {
|
|
635
384
|
throw new boom_1.Boom(`content length exceeded when encrypting "${type}"`, {
|
|
636
385
|
data: { media, type }
|
|
637
386
|
});
|
|
@@ -651,9 +400,10 @@ const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfReq
|
|
|
651
400
|
const fileEncSha256 = sha256Enc.digest();
|
|
652
401
|
encWriteStream.push(mac);
|
|
653
402
|
encWriteStream.push(null);
|
|
654
|
-
writeStream
|
|
655
|
-
|
|
656
|
-
|
|
403
|
+
writeStream?.end();
|
|
404
|
+
if (writeStream)
|
|
405
|
+
await (0, events_1.once)(writeStream, 'finish');
|
|
406
|
+
finalStream.destroy();
|
|
657
407
|
return {
|
|
658
408
|
mediaKey,
|
|
659
409
|
encWriteStream,
|
|
@@ -662,12 +412,13 @@ const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfReq
|
|
|
662
412
|
fileEncSha256,
|
|
663
413
|
fileSha256,
|
|
664
414
|
fileLength,
|
|
665
|
-
didSaveToTmpPath
|
|
415
|
+
didSaveToTmpPath,
|
|
416
|
+
opusConverted
|
|
666
417
|
};
|
|
667
418
|
}
|
|
668
419
|
catch (error) {
|
|
669
420
|
encWriteStream.destroy();
|
|
670
|
-
writeStream
|
|
421
|
+
writeStream?.destroy();
|
|
671
422
|
aes.destroy();
|
|
672
423
|
hmac.destroy();
|
|
673
424
|
sha256Plain.destroy();
|
|
@@ -677,17 +428,10 @@ const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfReq
|
|
|
677
428
|
try {
|
|
678
429
|
await fs_1.promises.unlink(bodyPath);
|
|
679
430
|
}
|
|
680
|
-
catch (
|
|
681
|
-
logger === null || logger === void 0 ? void 0 : logger.error({ err }, 'failed to save to tmp path');
|
|
682
|
-
}
|
|
431
|
+
catch (_) { }
|
|
683
432
|
}
|
|
684
433
|
throw error;
|
|
685
434
|
}
|
|
686
|
-
function onChunk(buff) {
|
|
687
|
-
sha256Enc = sha256Enc.update(buff);
|
|
688
|
-
hmac = hmac.update(buff);
|
|
689
|
-
encWriteStream.push(buff);
|
|
690
|
-
}
|
|
691
435
|
};
|
|
692
436
|
exports.encryptedStream = encryptedStream;
|
|
693
437
|
const DEF_HOST = 'mmg.whatsapp.net';
|
|
@@ -698,7 +442,7 @@ const toSmallestChunkSize = (num) => {
|
|
|
698
442
|
const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`;
|
|
699
443
|
exports.getUrlFromDirectPath = getUrlFromDirectPath;
|
|
700
444
|
const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
|
|
701
|
-
const isValidMediaUrl = url
|
|
445
|
+
const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/');
|
|
702
446
|
const downloadUrl = isValidMediaUrl ? url : (0, exports.getUrlFromDirectPath)(directPath);
|
|
703
447
|
if (!downloadUrl) {
|
|
704
448
|
throw new boom_1.Boom('No valid media URL or directPath present in message', { statusCode: 400 });
|
|
@@ -707,10 +451,15 @@ const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, o
|
|
|
707
451
|
return (0, exports.downloadEncryptedContent)(downloadUrl, keys, opts);
|
|
708
452
|
};
|
|
709
453
|
exports.downloadContentFromMessage = downloadContentFromMessage;
|
|
454
|
+
/**
|
|
455
|
+
* Decrypts and downloads an AES256-CBC encrypted file given the keys.
|
|
456
|
+
* Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
|
|
457
|
+
* */
|
|
710
458
|
const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
|
|
711
459
|
let bytesFetched = 0;
|
|
712
460
|
let startChunk = 0;
|
|
713
461
|
let firstBlockIsIV = false;
|
|
462
|
+
// if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
|
|
714
463
|
if (startByte) {
|
|
715
464
|
const chunk = toSmallestChunkSize(startByte || 0);
|
|
716
465
|
if (chunk) {
|
|
@@ -720,9 +469,14 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
|
|
|
720
469
|
}
|
|
721
470
|
}
|
|
722
471
|
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined;
|
|
472
|
+
const headersInit = options?.headers ? options.headers : undefined;
|
|
723
473
|
const headers = {
|
|
724
|
-
...(
|
|
725
|
-
|
|
474
|
+
...(headersInit
|
|
475
|
+
? Array.isArray(headersInit)
|
|
476
|
+
? Object.fromEntries(headersInit)
|
|
477
|
+
: headersInit
|
|
478
|
+
: {}),
|
|
479
|
+
Origin: index_js_2.DEFAULT_ORIGIN
|
|
726
480
|
};
|
|
727
481
|
if (startChunk || endChunk) {
|
|
728
482
|
headers.Range = `bytes=${startChunk}-`;
|
|
@@ -730,11 +484,10 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
|
|
|
730
484
|
headers.Range += endChunk;
|
|
731
485
|
}
|
|
732
486
|
}
|
|
487
|
+
// download the message
|
|
733
488
|
const fetched = await (0, exports.getHttpStream)(downloadUrl, {
|
|
734
|
-
...options || {},
|
|
735
|
-
headers
|
|
736
|
-
maxBodyLength: Infinity,
|
|
737
|
-
maxContentLength: Infinity,
|
|
489
|
+
...(options || {}),
|
|
490
|
+
headers
|
|
738
491
|
});
|
|
739
492
|
let remainingBytes = Buffer.from([]);
|
|
740
493
|
let aes;
|
|
@@ -762,6 +515,8 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
|
|
|
762
515
|
data = data.slice(AES_CHUNK_SIZE);
|
|
763
516
|
}
|
|
764
517
|
aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue);
|
|
518
|
+
// if an end byte that is not EOF is specified
|
|
519
|
+
// stop auto padding (PKCS7) -- otherwise throws an error for decryption
|
|
765
520
|
if (endByte) {
|
|
766
521
|
aes.setAutoPadding(false);
|
|
767
522
|
}
|
|
@@ -782,18 +537,16 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
|
|
|
782
537
|
catch (error) {
|
|
783
538
|
callback(error);
|
|
784
539
|
}
|
|
785
|
-
}
|
|
540
|
+
}
|
|
786
541
|
});
|
|
787
542
|
return fetched.pipe(output, { end: true });
|
|
788
543
|
};
|
|
789
544
|
exports.downloadEncryptedContent = downloadEncryptedContent;
|
|
790
545
|
function extensionForMediaMessage(message) {
|
|
791
|
-
const getExtension = (mimetype) => mimetype.split(';')[0]
|
|
546
|
+
const getExtension = (mimetype) => mimetype.split(';')[0]?.split('/')[1];
|
|
792
547
|
const type = Object.keys(message)[0];
|
|
793
548
|
let extension;
|
|
794
|
-
if (type === 'locationMessage' ||
|
|
795
|
-
type === 'liveLocationMessage' ||
|
|
796
|
-
type === 'productMessage') {
|
|
549
|
+
if (type === 'locationMessage' || type === 'liveLocationMessage' || type === 'productMessage') {
|
|
797
550
|
extension = '.jpeg';
|
|
798
551
|
}
|
|
799
552
|
else {
|
|
@@ -802,39 +555,166 @@ function extensionForMediaMessage(message) {
|
|
|
802
555
|
}
|
|
803
556
|
return extension;
|
|
804
557
|
}
|
|
558
|
+
const isNodeRuntime = () => {
|
|
559
|
+
return (typeof process !== 'undefined' &&
|
|
560
|
+
process.versions?.node !== null &&
|
|
561
|
+
typeof process.versions.bun === 'undefined' &&
|
|
562
|
+
typeof globalThis.Deno === 'undefined');
|
|
563
|
+
};
|
|
564
|
+
const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent }, redirectCount = 0) => {
|
|
565
|
+
if (redirectCount > 5) {
|
|
566
|
+
throw new Error('Too many redirects');
|
|
567
|
+
}
|
|
568
|
+
const parsedUrl = new url_1.URL(url);
|
|
569
|
+
const httpModule = parsedUrl.protocol === 'https:' ? await Promise.resolve().then(() => __importStar(require('https'))) : await Promise.resolve().then(() => __importStar(require('http')));
|
|
570
|
+
// Get file size for Content-Length header (required for Node.js streaming)
|
|
571
|
+
const fileStats = await fs_1.promises.stat(filePath);
|
|
572
|
+
const fileSize = fileStats.size;
|
|
573
|
+
return new Promise((resolve, reject) => {
|
|
574
|
+
const req = httpModule.request({
|
|
575
|
+
hostname: parsedUrl.hostname,
|
|
576
|
+
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
577
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
578
|
+
method: 'POST',
|
|
579
|
+
headers: {
|
|
580
|
+
...headers,
|
|
581
|
+
'Content-Length': fileSize
|
|
582
|
+
},
|
|
583
|
+
agent,
|
|
584
|
+
timeout: timeoutMs
|
|
585
|
+
}, res => {
|
|
586
|
+
// Handle redirects (3xx)
|
|
587
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
588
|
+
res.resume(); // Consume response to free resources
|
|
589
|
+
const newUrl = new url_1.URL(res.headers.location, url).toString();
|
|
590
|
+
resolve((0, exports.uploadWithNodeHttp)({
|
|
591
|
+
url: newUrl,
|
|
592
|
+
filePath,
|
|
593
|
+
headers,
|
|
594
|
+
timeoutMs,
|
|
595
|
+
agent
|
|
596
|
+
}, redirectCount + 1));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
let body = '';
|
|
600
|
+
res.on('data', chunk => (body += chunk));
|
|
601
|
+
res.on('end', () => {
|
|
602
|
+
try {
|
|
603
|
+
resolve(JSON.parse(body));
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
resolve(undefined);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
req.on('error', reject);
|
|
611
|
+
req.on('timeout', () => {
|
|
612
|
+
req.destroy();
|
|
613
|
+
reject(new Error('Upload timeout'));
|
|
614
|
+
});
|
|
615
|
+
const stream = (0, fs_1.createReadStream)(filePath);
|
|
616
|
+
stream.pipe(req);
|
|
617
|
+
stream.on('error', err => {
|
|
618
|
+
req.destroy();
|
|
619
|
+
reject(err);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
};
|
|
623
|
+
exports.uploadWithNodeHttp = uploadWithNodeHttp;
|
|
624
|
+
const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {
|
|
625
|
+
// Convert Node.js Readable to Web ReadableStream
|
|
626
|
+
const nodeStream = (0, fs_1.createReadStream)(filePath);
|
|
627
|
+
const webStream = stream_1.Readable.toWeb(nodeStream);
|
|
628
|
+
const response = await fetch(url, {
|
|
629
|
+
dispatcher: agent,
|
|
630
|
+
method: 'POST',
|
|
631
|
+
body: webStream,
|
|
632
|
+
headers,
|
|
633
|
+
duplex: 'half',
|
|
634
|
+
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
|
|
635
|
+
});
|
|
636
|
+
try {
|
|
637
|
+
return (await response.json());
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
/**
|
|
644
|
+
* Uploads media to WhatsApp servers.
|
|
645
|
+
*
|
|
646
|
+
* ## Why we have two upload implementations:
|
|
647
|
+
*
|
|
648
|
+
* Node.js's native `fetch` (powered by undici) has a known bug where it buffers
|
|
649
|
+
* the entire request body in memory before sending, even when using streams.
|
|
650
|
+
* This causes memory issues with large files (e.g., 1GB file = 1GB+ memory usage).
|
|
651
|
+
* See: https://github.com/nodejs/undici/issues/4058
|
|
652
|
+
*
|
|
653
|
+
* Other runtimes (Bun, Deno, browsers) correctly stream the request body without
|
|
654
|
+
* buffering, so we can use the web-standard Fetch API there.
|
|
655
|
+
*
|
|
656
|
+
* ## Future considerations:
|
|
657
|
+
* Once the undici bug is fixed, we can simplify this to use only the Fetch API
|
|
658
|
+
* across all runtimes. Monitor the GitHub issue for updates.
|
|
659
|
+
*/
|
|
660
|
+
const uploadMedia = async (params, logger) => {
|
|
661
|
+
if (isNodeRuntime()) {
|
|
662
|
+
return (0, exports.uploadWithNodeHttp)(params);
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
return uploadWithFetch(params);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
805
668
|
const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
|
|
806
|
-
return async (
|
|
807
|
-
|
|
669
|
+
return async (streamOrPath, { mediaType, fileEncSha256B64, timeoutMs, newsletter }) => {
|
|
670
|
+
// send a query JSON to obtain the url & auth token to upload our media
|
|
808
671
|
let uploadInfo = await refreshMediaConn(false);
|
|
809
672
|
let urls;
|
|
810
673
|
const hosts = [...customUploadHosts, ...uploadInfo.hosts];
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
674
|
+
fileEncSha256B64 = (0, exports.encodeBase64EncodedStringForUpload)(fileEncSha256B64);
|
|
675
|
+
// Prepare common headers
|
|
676
|
+
const customHeaders = (() => {
|
|
677
|
+
const hdrs = options?.headers;
|
|
678
|
+
if (!hdrs)
|
|
679
|
+
return {};
|
|
680
|
+
return Array.isArray(hdrs) ? Object.fromEntries(hdrs) : hdrs;
|
|
681
|
+
})();
|
|
682
|
+
const headers = {
|
|
683
|
+
...customHeaders,
|
|
684
|
+
'Content-Type': 'application/octet-stream',
|
|
685
|
+
Origin: index_js_2.DEFAULT_ORIGIN
|
|
686
|
+
};
|
|
687
|
+
// Collect buffer from Readable stream or read from file path
|
|
688
|
+
let reqBuffer;
|
|
689
|
+
if (Buffer.isBuffer(streamOrPath)) {
|
|
690
|
+
reqBuffer = streamOrPath;
|
|
691
|
+
}
|
|
692
|
+
else if (typeof streamOrPath === 'string') {
|
|
693
|
+
reqBuffer = await fs_1.promises.readFile(streamOrPath);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
// Readable stream
|
|
697
|
+
const chunks = [];
|
|
698
|
+
for await (const chunk of streamOrPath)
|
|
814
699
|
chunks.push(chunk);
|
|
815
|
-
|
|
700
|
+
reqBuffer = Buffer.concat(chunks);
|
|
816
701
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
let media = Defaults_1.MEDIA_PATH_MAP[mediaType];
|
|
702
|
+
// Newsletter uses different upload path
|
|
703
|
+
let mediaPath = index_js_2.MEDIA_PATH_MAP[mediaType];
|
|
820
704
|
if (newsletter) {
|
|
821
|
-
|
|
705
|
+
mediaPath = mediaPath?.replace('/mms/', '/newsletter/newsletter-');
|
|
822
706
|
}
|
|
823
707
|
for (const { hostname, maxContentLengthBytes } of hosts) {
|
|
824
|
-
logger.debug(`uploading to "${hostname}"`);
|
|
825
708
|
const auth = encodeURIComponent(uploadInfo.auth);
|
|
826
|
-
const url = `https://${hostname}${
|
|
709
|
+
const url = `https://${hostname}${mediaPath}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;
|
|
827
710
|
let result;
|
|
828
711
|
try {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
const body = await axios_1.default.post(url, reqBody, {
|
|
712
|
+
// Upload buffer directly like wiley (avoids file I/O issues)
|
|
713
|
+
const axios = (await Promise.resolve().then(() => __importStar(require('axios')))).default;
|
|
714
|
+
const body = await axios.post(url, reqBuffer, {
|
|
833
715
|
...options,
|
|
834
716
|
headers: {
|
|
835
|
-
...
|
|
836
|
-
'Content-Type': 'application/octet-stream',
|
|
837
|
-
'Origin': Defaults_1.DEFAULT_ORIGIN
|
|
717
|
+
...headers,
|
|
838
718
|
},
|
|
839
719
|
httpsAgent: fetchAgent,
|
|
840
720
|
timeout: timeoutMs,
|
|
@@ -843,11 +723,14 @@ const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options },
|
|
|
843
723
|
maxContentLength: Infinity,
|
|
844
724
|
});
|
|
845
725
|
result = body.data;
|
|
846
|
-
if (
|
|
726
|
+
if (result?.url || result?.direct_path) {
|
|
847
727
|
urls = {
|
|
848
728
|
mediaUrl: result.url,
|
|
849
729
|
directPath: result.direct_path,
|
|
850
|
-
handle: result.handle
|
|
730
|
+
handle: result.handle,
|
|
731
|
+
meta_hmac: result.meta_hmac,
|
|
732
|
+
fbid: result.fbid,
|
|
733
|
+
ts: result.ts
|
|
851
734
|
};
|
|
852
735
|
break;
|
|
853
736
|
}
|
|
@@ -857,11 +740,8 @@ const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options },
|
|
|
857
740
|
}
|
|
858
741
|
}
|
|
859
742
|
catch (error) {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
}
|
|
863
|
-
const isLast = hostname === ((_b = hosts[uploadInfo.hosts.length - 1]) === null || _b === void 0 ? void 0 : _b.hostname);
|
|
864
|
-
logger.warn({ trace: error.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`);
|
|
743
|
+
const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname;
|
|
744
|
+
logger.warn({ trace: error?.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`);
|
|
865
745
|
}
|
|
866
746
|
}
|
|
867
747
|
if (!urls) {
|
|
@@ -872,22 +752,28 @@ const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options },
|
|
|
872
752
|
};
|
|
873
753
|
exports.getWAUploadToServer = getWAUploadToServer;
|
|
874
754
|
const getMediaRetryKey = (mediaKey) => {
|
|
875
|
-
return (0,
|
|
755
|
+
return (0, crypto_js_1.hkdf)(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' });
|
|
876
756
|
};
|
|
877
|
-
|
|
757
|
+
/**
|
|
758
|
+
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
|
|
759
|
+
*/
|
|
760
|
+
const encryptMediaRetryRequest = (key, mediaKey, meId) => {
|
|
878
761
|
const recp = { stanzaId: key.id };
|
|
879
|
-
const recpBuffer =
|
|
762
|
+
const recpBuffer = index_js_1.proto.ServerErrorReceipt.encode(recp).finish();
|
|
880
763
|
const iv = Crypto.randomBytes(12);
|
|
881
|
-
const retryKey =
|
|
882
|
-
const ciphertext = (0,
|
|
764
|
+
const retryKey = getMediaRetryKey(mediaKey);
|
|
765
|
+
const ciphertext = (0, crypto_js_1.aesEncryptGCM)(recpBuffer, retryKey, iv, Buffer.from(key.id));
|
|
883
766
|
const req = {
|
|
884
767
|
tag: 'receipt',
|
|
885
768
|
attrs: {
|
|
886
769
|
id: key.id,
|
|
887
|
-
to: (0,
|
|
770
|
+
to: (0, index_js_3.jidNormalizedUser)(meId),
|
|
888
771
|
type: 'server-error'
|
|
889
772
|
},
|
|
890
773
|
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
|
|
891
777
|
{
|
|
892
778
|
tag: 'encrypt',
|
|
893
779
|
attrs: {},
|
|
@@ -900,7 +786,8 @@ const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
|
|
|
900
786
|
tag: 'rmr',
|
|
901
787
|
attrs: {
|
|
902
788
|
jid: key.remoteJid,
|
|
903
|
-
|
|
789
|
+
from_me: (!!key.fromMe).toString(),
|
|
790
|
+
// @ts-ignore
|
|
904
791
|
participant: key.participant || undefined
|
|
905
792
|
}
|
|
906
793
|
}
|
|
@@ -910,7 +797,7 @@ const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
|
|
|
910
797
|
};
|
|
911
798
|
exports.encryptMediaRetryRequest = encryptMediaRetryRequest;
|
|
912
799
|
const decodeMediaRetryNode = (node) => {
|
|
913
|
-
const rmrNode = (0,
|
|
800
|
+
const rmrNode = (0, index_js_3.getBinaryNodeChild)(node, 'rmr');
|
|
914
801
|
const event = {
|
|
915
802
|
key: {
|
|
916
803
|
id: node.attrs.id,
|
|
@@ -919,15 +806,18 @@ const decodeMediaRetryNode = (node) => {
|
|
|
919
806
|
participant: rmrNode.attrs.participant
|
|
920
807
|
}
|
|
921
808
|
};
|
|
922
|
-
const errorNode = (0,
|
|
809
|
+
const errorNode = (0, index_js_3.getBinaryNodeChild)(node, 'error');
|
|
923
810
|
if (errorNode) {
|
|
924
811
|
const errorCode = +errorNode.attrs.code;
|
|
925
|
-
event.error = new boom_1.Boom(`Failed to re-upload media (${errorCode})`, {
|
|
812
|
+
event.error = new boom_1.Boom(`Failed to re-upload media (${errorCode})`, {
|
|
813
|
+
data: errorNode.attrs,
|
|
814
|
+
statusCode: (0, exports.getStatusCodeForMediaRetry)(errorCode)
|
|
815
|
+
});
|
|
926
816
|
}
|
|
927
817
|
else {
|
|
928
|
-
const encryptedInfoNode = (0,
|
|
929
|
-
const ciphertext = (0,
|
|
930
|
-
const iv = (0,
|
|
818
|
+
const encryptedInfoNode = (0, index_js_3.getBinaryNodeChild)(node, 'encrypt');
|
|
819
|
+
const ciphertext = (0, index_js_3.getBinaryNodeChildBuffer)(encryptedInfoNode, 'enc_p');
|
|
820
|
+
const iv = (0, index_js_3.getBinaryNodeChildBuffer)(encryptedInfoNode, 'enc_iv');
|
|
931
821
|
if (ciphertext && iv) {
|
|
932
822
|
event.media = { ciphertext, iv };
|
|
933
823
|
}
|
|
@@ -938,20 +828,57 @@ const decodeMediaRetryNode = (node) => {
|
|
|
938
828
|
return event;
|
|
939
829
|
};
|
|
940
830
|
exports.decodeMediaRetryNode = decodeMediaRetryNode;
|
|
941
|
-
const decryptMediaRetryData =
|
|
942
|
-
const retryKey =
|
|
943
|
-
const plaintext = (0,
|
|
944
|
-
return
|
|
831
|
+
const decryptMediaRetryData = ({ ciphertext, iv }, mediaKey, msgId) => {
|
|
832
|
+
const retryKey = getMediaRetryKey(mediaKey);
|
|
833
|
+
const plaintext = (0, crypto_js_1.aesDecryptGCM)(ciphertext, retryKey, iv, Buffer.from(msgId));
|
|
834
|
+
return index_js_1.proto.MediaRetryNotification.decode(plaintext);
|
|
945
835
|
};
|
|
946
836
|
exports.decryptMediaRetryData = decryptMediaRetryData;
|
|
947
837
|
const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code];
|
|
948
838
|
exports.getStatusCodeForMediaRetry = getStatusCodeForMediaRetry;
|
|
949
839
|
const MEDIA_RETRY_STATUS_MAP = {
|
|
950
|
-
[
|
|
951
|
-
[
|
|
952
|
-
[
|
|
953
|
-
[
|
|
840
|
+
[index_js_1.proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
|
|
841
|
+
[index_js_1.proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
|
|
842
|
+
[index_js_1.proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
|
|
843
|
+
[index_js_1.proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
|
|
844
|
+
};
|
|
845
|
+
const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
|
|
846
|
+
const { stream, type } = await (0, exports.getStream)(media, opts);
|
|
847
|
+
let buffer = await (0, exports.toBuffer)(stream);
|
|
848
|
+
let opusConverted = false;
|
|
849
|
+
let bodyPath;
|
|
850
|
+
let didSaveToTmpPath = false;
|
|
851
|
+
try {
|
|
852
|
+
if (type === 'file') {
|
|
853
|
+
bodyPath = media.url?.toString?.() || media.url;
|
|
854
|
+
}
|
|
855
|
+
else if (saveOriginalFileIfRequired) {
|
|
856
|
+
bodyPath = (0, path_1.join)(getTmpFilesDirectory(), mediaType + (0, generics_js_1.generateMessageIDV2)());
|
|
857
|
+
const { writeFileSync } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
858
|
+
writeFileSync(bodyPath, buffer);
|
|
859
|
+
didSaveToTmpPath = true;
|
|
860
|
+
}
|
|
861
|
+
const fileLength = buffer.length;
|
|
862
|
+
const fileSha256 = Crypto.createHash('sha256').update(buffer).digest();
|
|
863
|
+
return {
|
|
864
|
+
mediaKey: undefined,
|
|
865
|
+
encWriteStream: buffer,
|
|
866
|
+
fileLength,
|
|
867
|
+
fileSha256,
|
|
868
|
+
fileEncSha256: undefined,
|
|
869
|
+
bodyPath,
|
|
870
|
+
didSaveToTmpPath,
|
|
871
|
+
opusConverted
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
if (didSaveToTmpPath && bodyPath) {
|
|
876
|
+
try {
|
|
877
|
+
await fs_1.promises.unlink(bodyPath);
|
|
878
|
+
}
|
|
879
|
+
catch (_) { }
|
|
880
|
+
}
|
|
881
|
+
throw error;
|
|
882
|
+
}
|
|
954
883
|
};
|
|
955
|
-
|
|
956
|
-
throw new Error('Function not implemented.');
|
|
957
|
-
}
|
|
884
|
+
exports.prepareStream = prepareStream;
|