@hansaka02/baileys 7.3.2 → 7.3.6
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/lib/Defaults/baileys-version.json +2 -2
- package/lib/Defaults/connection.js +51 -0
- package/lib/Defaults/constants.js +74 -0
- package/lib/Defaults/history.js +19 -0
- package/lib/Defaults/index.js +36 -142
- package/lib/Defaults/media.js +48 -0
- package/lib/Defaults/prefix.js +18 -0
- package/lib/Signal/Group/group-session-builder.js +10 -42
- package/lib/Signal/Group/group_cipher.js +9 -6
- package/lib/Signal/Group/index.js +39 -53
- package/lib/Signal/Group/keyhelper.js +8 -41
- package/lib/Signal/Group/sender-chain-key.js +5 -18
- package/lib/Signal/Group/sender-key-distribution-message.js +7 -7
- package/lib/Signal/Group/sender-key-message.js +12 -8
- package/lib/Signal/Group/sender-key-record.js +7 -16
- package/lib/Signal/Group/sender-key-state.js +15 -61
- package/lib/Signal/Group/sender-message-key.js +2 -2
- package/lib/Signal/libsignal.js +237 -177
- package/lib/Signal/lid-mapping.js +128 -71
- package/lib/Socket/Client/types.js +2 -2
- package/lib/Socket/Client/websocket.js +25 -16
- package/lib/Socket/business.js +46 -33
- package/lib/Socket/chats.js +286 -170
- package/lib/Socket/community.js +215 -77
- package/lib/Socket/groups.js +77 -61
- package/lib/Socket/index.js +4 -4
- package/lib/Socket/messages-recv.js +629 -457
- package/lib/Socket/messages-send.js +645 -656
- package/lib/Socket/mex.js +61 -0
- package/lib/Socket/newsletter.js +166 -245
- package/lib/Socket/socket.js +396 -170
- package/lib/Store/index.js +27 -11
- package/lib/Store/make-cache-manager-store.js +14 -15
- package/lib/Store/make-in-memory-store.js +28 -24
- package/lib/Types/LabelAssociation.js +2 -2
- package/lib/Types/Message.js +6 -6
- package/lib/Types/MexUpdates.js +5 -5
- package/lib/Types/Newsletter.js +32 -25
- package/lib/Types/State.js +4 -4
- package/lib/Types/index.js +28 -12
- package/lib/Utils/auth-utils.js +212 -375
- package/lib/Utils/baileys-event-stream.js +68 -69
- package/lib/Utils/browser-utils.js +43 -0
- package/lib/Utils/business.js +63 -53
- package/lib/Utils/chat-utils.js +241 -106
- package/lib/Utils/crypto.js +25 -45
- package/lib/Utils/decode-wa-message.js +361 -311
- package/lib/Utils/event-buffer.js +97 -42
- package/lib/Utils/generics.js +90 -207
- package/lib/Utils/history.js +29 -27
- package/lib/Utils/index.js +28 -14
- package/lib/Utils/link-preview.js +24 -62
- package/lib/Utils/logger.js +5 -5
- package/lib/Utils/lt-hash.js +29 -23
- package/lib/Utils/make-mutex.js +26 -28
- package/lib/Utils/message-retry-manager.js +55 -7
- package/lib/Utils/messages-media.js +434 -247
- package/lib/Utils/messages.js +963 -917
- package/lib/Utils/noise-handler.js +60 -20
- package/lib/Utils/pre-key-manager.js +126 -0
- package/lib/Utils/process-message.js +216 -141
- package/lib/Utils/signal.js +75 -37
- package/lib/Utils/use-multi-file-auth-state.js +18 -22
- package/lib/Utils/validate-connection.js +96 -66
- package/lib/WABinary/constants.js +1268 -1268
- package/lib/WABinary/decode.js +62 -34
- package/lib/WABinary/encode.js +57 -36
- package/lib/WABinary/generic-utils.js +4 -4
- package/lib/WABinary/index.js +27 -11
- package/lib/WABinary/jid-utils.js +58 -11
- package/lib/WAM/constants.js +19064 -11563
- package/lib/WAM/encode.js +71 -14
- package/lib/WAM/index.js +27 -11
- package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +20 -16
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +2 -2
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +7 -4
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +2 -2
- package/lib/WAUSync/Protocols/USyncLIDProtocol.js +0 -2
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +2 -2
- package/lib/WAUSync/Protocols/index.js +27 -11
- package/lib/WAUSync/USyncQuery.js +51 -28
- package/lib/WAUSync/index.js +27 -11
- package/lib/index.js +60 -31
- package/package.json +12 -17
- package/WAProto/AICommon/AICommon.d.ts +0 -11702
- package/WAProto/Adv/Adv.d.ts +0 -643
- package/WAProto/BotMetadata/BotMetadata.d.ts +0 -5654
- package/WAProto/Cert/Cert.d.ts +0 -613
- package/WAProto/ChatLockSettings/ChatLockSettings.d.ts +0 -476
- package/WAProto/CompanionReg/CompanionReg.d.ts +0 -1361
- package/WAProto/DeviceCapabilities/DeviceCapabilities.d.ts +0 -577
- package/WAProto/E2E/E2E.d.ts +0 -41724
- package/WAProto/Ephemeral/Ephemeral.d.ts +0 -114
- package/WAProto/HistorySync/HistorySync.d.ts +0 -51700
- package/WAProto/LidMigrationSyncPayload/LidMigrationSyncPayload.d.ts +0 -229
- package/WAProto/MdStorageChatRowOpaqueData/MdStorageChatRowOpaqueData.d.ts +0 -583
- package/WAProto/MdStorageMsgRowOpaqueData/MdStorageMsgRowOpaqueData.d.ts +0 -42897
- package/WAProto/MmsRetry/MmsRetry.d.ts +0 -243
- package/WAProto/Protocol/Protocol.d.ts +0 -270
- package/WAProto/Reporting/Reporting.d.ts +0 -371
- package/WAProto/ServerSync/ServerSync.d.ts +0 -1285
- package/WAProto/SignalLocalStorageProtocol/SignalLocalStorageProtocol.d.ts +0 -1868
- package/WAProto/SignalWhisperTextProtocol/SignalWhisperTextProtocol.d.ts +0 -767
- package/WAProto/StatusAttributions/StatusAttributions.d.ts +0 -1027
- package/WAProto/SyncAction/SyncAction.d.ts +0 -11193
- package/WAProto/UserPassword/UserPassword.d.ts +0 -363
- package/WAProto/VnameCert/VnameCert.d.ts +0 -821
- package/WAProto/Wa6/Wa6.d.ts +0 -2128
- package/WAProto/Web/Web.d.ts +0 -46383
- package/WAProto/index.d.ts +0 -55
- package/lib/Defaults/index.d.ts +0 -77
- package/lib/Signal/Group/ciphertext-message.d.ts +0 -9
- package/lib/Signal/Group/group-session-builder.d.ts +0 -17
- package/lib/Signal/Group/group_cipher.d.ts +0 -19
- package/lib/Signal/Group/index.d.ts +0 -11
- package/lib/Signal/Group/keyhelper.d.ts +0 -16
- package/lib/Signal/Group/sender-chain-key.d.ts +0 -14
- package/lib/Signal/Group/sender-key-distribution-message.d.ts +0 -17
- package/lib/Signal/Group/sender-key-message.d.ts +0 -19
- package/lib/Signal/Group/sender-key-name.d.ts +0 -19
- package/lib/Signal/Group/sender-key-record.d.ts +0 -32
- package/lib/Signal/Group/sender-key-state.d.ts +0 -44
- package/lib/Signal/Group/sender-message-key.d.ts +0 -11
- package/lib/Signal/libsignal.d.ts +0 -8
- package/lib/Signal/lid-mapping.d.ts +0 -28
- package/lib/Socket/Client/index.d.ts +0 -2
- package/lib/Socket/Client/types.d.ts +0 -16
- package/lib/Socket/Client/websocket.d.ts +0 -13
- package/lib/Socket/business.d.ts +0 -187
- package/lib/Socket/chats.d.ts +0 -97
- package/lib/Socket/community.d.ts +0 -129
- package/lib/Socket/groups.d.ts +0 -129
- package/lib/Socket/index.d.ts +0 -191
- package/lib/Socket/messages-recv.d.ts +0 -174
- package/lib/Socket/messages-send.d.ts +0 -165
- package/lib/Socket/newsletter.d.ts +0 -145
- package/lib/Socket/socket.d.ts +0 -45
- package/lib/Socket/usync.d.ts +0 -37
- package/lib/Socket/usync.js +0 -83
- package/lib/Store/index.d.ts +0 -4
- package/lib/Store/make-cache-manager-store.d.ts +0 -14
- package/lib/Store/make-in-memory-store.d.ts +0 -123
- package/lib/Store/make-ordered-dictionary.d.ts +0 -12
- package/lib/Store/object-repository.d.ts +0 -10
- package/lib/Types/Auth.d.ts +0 -121
- package/lib/Types/Bussiness.d.ts +0 -28
- package/lib/Types/Call.d.ts +0 -14
- package/lib/Types/Chat.d.ts +0 -143
- package/lib/Types/Contact.d.ts +0 -23
- package/lib/Types/Events.d.ts +0 -226
- package/lib/Types/GroupMetadata.d.ts +0 -66
- package/lib/Types/Label.d.ts +0 -48
- package/lib/Types/LabelAssociation.d.ts +0 -35
- package/lib/Types/Message.d.ts +0 -484
- package/lib/Types/MexUpdates.d.ts +0 -9
- package/lib/Types/Newsletter.d.ts +0 -109
- package/lib/Types/Product.d.ts +0 -92
- package/lib/Types/Signal.d.ts +0 -98
- package/lib/Types/Socket.d.ts +0 -141
- package/lib/Types/State.d.ts +0 -41
- package/lib/Types/USync.d.ts +0 -26
- package/lib/Types/index.d.ts +0 -80
- package/lib/Utils/auth-utils.d.ts +0 -21
- package/lib/Utils/baileys-event-stream.d.ts +0 -18
- package/lib/Utils/business.d.ts +0 -29
- package/lib/Utils/chat-utils.d.ts +0 -82
- package/lib/Utils/crypto.d.ts +0 -56
- package/lib/Utils/decode-wa-message.d.ts +0 -53
- package/lib/Utils/event-buffer.d.ts +0 -39
- package/lib/Utils/generics.d.ts +0 -117
- package/lib/Utils/history.d.ts +0 -23
- package/lib/Utils/index.d.ts +0 -20
- package/lib/Utils/link-preview.d.ts +0 -23
- package/lib/Utils/logger.d.ts +0 -13
- package/lib/Utils/lt-hash.d.ts +0 -14
- package/lib/Utils/make-mutex.d.ts +0 -9
- package/lib/Utils/message-retry-manager.d.ts +0 -88
- package/lib/Utils/messages-media.d.ts +0 -135
- package/lib/Utils/messages.d.ts +0 -105
- package/lib/Utils/noise-handler.d.ts +0 -20
- package/lib/Utils/process-message.d.ts +0 -49
- package/lib/Utils/signal.d.ts +0 -42
- package/lib/Utils/use-mongo-file-auth-state.d.ts +0 -6
- package/lib/Utils/use-mongo-file-auth-state.js +0 -84
- package/lib/Utils/use-multi-file-auth-state.d.ts +0 -13
- package/lib/Utils/use-single-file-auth-state.d.ts +0 -13
- package/lib/Utils/use-single-file-auth-state.js +0 -80
- package/lib/Utils/validate-connection.d.ts +0 -13
- package/lib/WABinary/constants.d.ts +0 -30
- package/lib/WABinary/decode.d.ts +0 -9
- package/lib/WABinary/encode.d.ts +0 -3
- package/lib/WABinary/generic-utils.d.ts +0 -28
- package/lib/WABinary/index.d.ts +0 -5
- package/lib/WABinary/jid-utils.d.ts +0 -58
- package/lib/WABinary/types.d.ts +0 -22
- package/lib/WAM/BinaryInfo.d.ts +0 -16
- package/lib/WAM/constants.d.ts +0 -47
- package/lib/WAM/encode.d.ts +0 -3
- package/lib/WAM/index.d.ts +0 -3
- package/lib/WAUSync/Protocols/USyncBotProfileProtocol.d.ts +0 -28
- package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts +0 -10
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.d.ts +0 -26
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +0 -14
- package/lib/WAUSync/Protocols/USyncLIDProtocol.d.ts +0 -10
- package/lib/WAUSync/Protocols/USyncStatusProtocol.d.ts +0 -14
- package/lib/WAUSync/Protocols/index.d.ts +0 -6
- package/lib/WAUSync/USyncQuery.d.ts +0 -31
- package/lib/WAUSync/USyncUser.d.ts +0 -12
- package/lib/WAUSync/index.d.ts +0 -3
- package/lib/index.d.ts +0 -13
|
@@ -1,75 +1,80 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
if (k2 === undefined) k2 = k
|
|
5
|
-
var desc = Object.getOwnPropertyDescriptor(m, k)
|
|
6
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
-
desc = { enumerable: true, get: function() { return m[k] } }
|
|
8
|
-
}
|
|
9
|
-
Object.defineProperty(o, k2, desc)
|
|
10
|
-
}) : (function(o, m, k, k2) {
|
|
11
|
-
if (k2 === undefined) k2 = k
|
|
12
|
-
o[k2] = m[k]
|
|
13
|
-
}))
|
|
14
|
-
|
|
15
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v })
|
|
17
|
-
}) : function(o, v) {
|
|
18
|
-
o["default"] = v
|
|
19
|
-
})
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true })
|
|
20
4
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
5
|
+
const { Boom } = require("@hapi/boom")
|
|
6
|
+
const { exec } = require("child_process")
|
|
7
|
+
const { once } = require("events")
|
|
8
|
+
const {
|
|
9
|
+
createHash,
|
|
10
|
+
randomBytes,
|
|
11
|
+
createHmac,
|
|
12
|
+
createCipheriv,
|
|
13
|
+
createDecipheriv
|
|
14
|
+
} = require("crypto")
|
|
15
|
+
const {
|
|
16
|
+
promises,
|
|
17
|
+
createReadStream,
|
|
18
|
+
createWriteStream
|
|
19
|
+
} = require("fs")
|
|
20
|
+
const {
|
|
21
|
+
parseBuffer,
|
|
22
|
+
parseFile,
|
|
23
|
+
parseStream
|
|
24
|
+
} = require('music-metadata')
|
|
25
|
+
const {
|
|
26
|
+
default: decoder
|
|
27
|
+
} = require("audio-decode")
|
|
28
|
+
const { tmpdir } = require("os")
|
|
29
|
+
const { join } = require("path")
|
|
30
|
+
const {
|
|
31
|
+
Readable,
|
|
32
|
+
Transform
|
|
33
|
+
} = require("stream")
|
|
34
|
+
const { proto } = require("../../WAProto")
|
|
35
|
+
const {
|
|
36
|
+
MEDIA_PATH_MAP,
|
|
37
|
+
MEDIA_HKDF_KEY_MAPPING
|
|
38
|
+
} = require("../Defaults/media")
|
|
39
|
+
const { DEFAULT_ORIGIN } = require("../Defaults/constants")
|
|
40
|
+
const {
|
|
41
|
+
getBinaryNodeChild,
|
|
42
|
+
getBinaryNodeChildBuffer,
|
|
43
|
+
jidNormalizedUser
|
|
44
|
+
} = require("../WABinary")
|
|
45
|
+
const {
|
|
46
|
+
aesDecryptGCM,
|
|
47
|
+
aesEncryptGCM,
|
|
48
|
+
hkdf
|
|
49
|
+
} = require("./crypto")
|
|
50
|
+
const { generateMessageID } = require("./generics")
|
|
28
51
|
|
|
29
|
-
|
|
30
|
-
return (mod && mod.__esModule) ? mod : { "default": mod }
|
|
31
|
-
}
|
|
52
|
+
const getTmpFilesDirectory = () => tmpdir()
|
|
32
53
|
|
|
33
|
-
|
|
54
|
+
const getImageProcessingLibrary = () => {
|
|
55
|
+
let sharp, jimp
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
sharp = require('sharp')
|
|
59
|
+
} catch {}
|
|
34
60
|
|
|
35
|
-
const boom_1 = require("@hapi/boom")
|
|
36
|
-
const axios_1 = __importDefault(require("axios"))
|
|
37
|
-
const child_process_1 = require("child_process")
|
|
38
|
-
const Crypto = __importStar(require("crypto"))
|
|
39
|
-
const events_1 = require("events")
|
|
40
|
-
const fs_1 = require("fs")
|
|
41
|
-
const os_1 = require("os")
|
|
42
|
-
const path_1 = require("path")
|
|
43
|
-
const stream_1 = require("stream")
|
|
44
|
-
const WAProto_1 = require("../../WAProto")
|
|
45
|
-
const Defaults_1 = require("../Defaults")
|
|
46
|
-
const WABinary_1 = require("../WABinary")
|
|
47
|
-
const crypto_1 = require("./crypto")
|
|
48
|
-
const generics_1 = require("./generics")
|
|
49
|
-
|
|
50
|
-
const getImageProcessingLibrary = async () => {
|
|
51
|
-
const [_jimp, sharp] = await Promise.all([
|
|
52
|
-
(async () => {
|
|
53
|
-
const jimp = await (Promise.resolve().then(() => __importStar(require('jimp'))).catch(() => { }))
|
|
54
|
-
return jimp
|
|
55
|
-
})(),
|
|
56
|
-
(async () => {
|
|
57
|
-
const sharp = await (Promise.resolve().then(() => __importStar(require('sharp'))).catch(() => { }))
|
|
58
|
-
return sharp
|
|
59
|
-
})()
|
|
60
|
-
])
|
|
61
61
|
if (sharp) {
|
|
62
62
|
return { sharp }
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
jimp = require('jimp')
|
|
67
|
+
} catch {}
|
|
68
|
+
|
|
65
69
|
if (jimp) {
|
|
66
70
|
return { jimp }
|
|
67
71
|
}
|
|
68
|
-
|
|
72
|
+
|
|
73
|
+
throw new Boom('No image processing library available')
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
const hkdfInfoKey = (type) => {
|
|
72
|
-
const hkdfInfo =
|
|
77
|
+
const hkdfInfo = MEDIA_HKDF_KEY_MAPPING[type]
|
|
73
78
|
return `WhatsApp ${hkdfInfo} Keys`
|
|
74
79
|
}
|
|
75
80
|
|
|
@@ -78,9 +83,9 @@ const getRawMediaUploadData = async (media, mediaType, logger) => {
|
|
|
78
83
|
|
|
79
84
|
logger?.debug('got stream for raw upload')
|
|
80
85
|
|
|
81
|
-
const hasher =
|
|
82
|
-
const filePath =
|
|
83
|
-
const fileWriteStream =
|
|
86
|
+
const hasher = createHash('sha256')
|
|
87
|
+
const filePath = join(getTmpFilesDirectory(), mediaType + generateMessageID())
|
|
88
|
+
const fileWriteStream = createWriteStream(filePath)
|
|
84
89
|
|
|
85
90
|
let fileLength = 0
|
|
86
91
|
|
|
@@ -90,12 +95,12 @@ const getRawMediaUploadData = async (media, mediaType, logger) => {
|
|
|
90
95
|
hasher.update(data)
|
|
91
96
|
|
|
92
97
|
if (!fileWriteStream.write(data)) {
|
|
93
|
-
await
|
|
98
|
+
await once(fileWriteStream, 'drain')
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
fileWriteStream.end()
|
|
98
|
-
await
|
|
103
|
+
await once(fileWriteStream, 'finish')
|
|
99
104
|
stream.destroy()
|
|
100
105
|
|
|
101
106
|
const fileSha256 = hasher.digest()
|
|
@@ -113,7 +118,7 @@ const getRawMediaUploadData = async (media, mediaType, logger) => {
|
|
|
113
118
|
stream.destroy()
|
|
114
119
|
|
|
115
120
|
try {
|
|
116
|
-
await
|
|
121
|
+
await promises.unlink(filePath)
|
|
117
122
|
}
|
|
118
123
|
catch {
|
|
119
124
|
//
|
|
@@ -125,85 +130,60 @@ const getRawMediaUploadData = async (media, mediaType, logger) => {
|
|
|
125
130
|
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
|
126
131
|
async function getMediaKeys(buffer, mediaType) {
|
|
127
132
|
if (!buffer) {
|
|
128
|
-
throw new
|
|
133
|
+
throw new Boom('Cannot derive from empty media key')
|
|
129
134
|
}
|
|
130
135
|
if (typeof buffer === 'string') {
|
|
131
136
|
buffer = Buffer.from(buffer.replace('data:base64,', ''), 'base64')
|
|
132
137
|
}
|
|
133
138
|
// expand using HKDF to 112 bytes, also pass in the relevant app info
|
|
134
|
-
const expandedMediaKey = await
|
|
139
|
+
const expandedMediaKey = await hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
|
|
135
140
|
return {
|
|
136
141
|
iv: expandedMediaKey.slice(0, 16),
|
|
137
142
|
cipherKey: expandedMediaKey.slice(16, 48),
|
|
138
|
-
macKey: expandedMediaKey.slice(48, 80)
|
|
143
|
+
macKey: expandedMediaKey.slice(48, 80)
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
/** Extracts video thumb using FFMPEG */
|
|
143
|
-
const extractVideoThumb = (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
'-vcodec', 'mjpeg',
|
|
153
|
-
'pipe:1'
|
|
154
|
-
]
|
|
155
|
-
|
|
156
|
-
const ffmpeg = child_process_1.spawn('ffmpeg', args)
|
|
157
|
-
const chunks = []
|
|
158
|
-
let errorOutput = ''
|
|
159
|
-
|
|
160
|
-
ffmpeg.stdout.on('data', chunk => chunks.push(chunk))
|
|
161
|
-
ffmpeg.stderr.on('data', data => {
|
|
162
|
-
errorOutput += data.toString()
|
|
163
|
-
})
|
|
164
|
-
ffmpeg.on('error', reject)
|
|
165
|
-
ffmpeg.on('close', code => {
|
|
166
|
-
if (code === 0) return resolve(Buffer.concat(chunks))
|
|
167
|
-
reject(new Error(`ffmpeg exited with code ${code}\n${errorOutput}`))
|
|
168
|
-
})
|
|
148
|
+
const extractVideoThumb = async (path, destPath, time, size) => new Promise((resolve, reject) => {
|
|
149
|
+
const cmd = `ffmpeg -ss ${time} -i ${path} -y -vf scale=${size.width}:-1 -vframes 1 -f image2 ${destPath}`
|
|
150
|
+
exec(cmd, err => {
|
|
151
|
+
if (err) {
|
|
152
|
+
reject(err)
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
resolve()
|
|
156
|
+
}
|
|
169
157
|
})
|
|
170
|
-
}
|
|
158
|
+
})
|
|
171
159
|
|
|
172
160
|
const extractImageThumb = async (bufferOrFilePath, width = 32, quality = 50) => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
if (bufferOrFilePath instanceof stream_1.Readable) {
|
|
161
|
+
// TODO: Move entirely to sharp, removing jimp as it supports readable streams
|
|
162
|
+
// This will have positive speed and performance impacts as well as minimizing RAM usage.
|
|
163
|
+
if (bufferOrFilePath instanceof Readable) {
|
|
178
164
|
bufferOrFilePath = await toBuffer(bufferOrFilePath)
|
|
179
165
|
}
|
|
166
|
+
|
|
180
167
|
const lib = await getImageProcessingLibrary()
|
|
181
|
-
|
|
182
|
-
|
|
168
|
+
|
|
169
|
+
if ('sharp' in lib && typeof lib.sharp === 'function') {
|
|
170
|
+
const img = lib.sharp(bufferOrFilePath)
|
|
183
171
|
const dimensions = await img.metadata()
|
|
184
|
-
const buffer = await img
|
|
185
|
-
.resize({
|
|
186
|
-
width,
|
|
187
|
-
height: width,
|
|
188
|
-
fit: 'contain',
|
|
189
|
-
background: { r: 255, g: 255, b: 255, alpha: 0 }
|
|
190
|
-
})
|
|
191
|
-
.jpeg({ quality })
|
|
192
|
-
.toBuffer()
|
|
172
|
+
const buffer = await img.resize(width).jpeg({ quality: 50 }).toBuffer()
|
|
193
173
|
return {
|
|
194
174
|
buffer,
|
|
195
175
|
original: {
|
|
196
176
|
width: dimensions.width,
|
|
197
|
-
height: dimensions.height
|
|
198
|
-
}
|
|
177
|
+
height: dimensions.height
|
|
178
|
+
}
|
|
199
179
|
}
|
|
200
180
|
}
|
|
201
|
-
else if ('jimp' in lib && typeof lib.jimp
|
|
202
|
-
|
|
181
|
+
else if ('jimp' in lib && typeof lib.jimp.read === 'function') {
|
|
182
|
+
const { read, MIME_JPEG, RESIZE_BEZIER, AUTO } = lib.jimp
|
|
203
183
|
const jimp = await read(bufferOrFilePath)
|
|
204
184
|
const dimensions = {
|
|
205
185
|
width: jimp.getWidth(),
|
|
206
|
-
height: jimp.getHeight()
|
|
186
|
+
height: jimp.getHeight()
|
|
207
187
|
}
|
|
208
188
|
const buffer = await jimp
|
|
209
189
|
.quality(quality)
|
|
@@ -215,7 +195,7 @@ const extractImageThumb = async (bufferOrFilePath, width = 32, quality = 50) =>
|
|
|
215
195
|
}
|
|
216
196
|
}
|
|
217
197
|
else {
|
|
218
|
-
throw new
|
|
198
|
+
throw new Boom('No image processing library available')
|
|
219
199
|
}
|
|
220
200
|
}
|
|
221
201
|
|
|
@@ -224,52 +204,53 @@ const encodeBase64EncodedStringForUpload = (b64) => (encodeURIComponent(b64
|
|
|
224
204
|
.replace(/\//g, '_')
|
|
225
205
|
.replace(/\=+$/, '')))
|
|
226
206
|
|
|
227
|
-
const generateProfilePicture = async (mediaUpload) => {
|
|
228
|
-
let
|
|
207
|
+
const generateProfilePicture = async (mediaUpload, dimensions) => {
|
|
208
|
+
let buffer
|
|
209
|
+
|
|
210
|
+
const { width: w = 640, height: h = 640 } = dimensions || {}
|
|
211
|
+
|
|
229
212
|
if (Buffer.isBuffer(mediaUpload)) {
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
else if ('url' in mediaUpload) {
|
|
233
|
-
bufferOrFilePath = mediaUpload.url.toString()
|
|
213
|
+
buffer = mediaUpload
|
|
234
214
|
}
|
|
235
215
|
else {
|
|
236
|
-
|
|
216
|
+
// Use getStream to handle all WAMediaUpload types (Buffer, Stream, URL)
|
|
217
|
+
const { stream } = await getStream(mediaUpload)
|
|
218
|
+
// Convert the resulting stream to a buffer
|
|
219
|
+
buffer = await toBuffer(stream)
|
|
237
220
|
}
|
|
238
221
|
const lib = await getImageProcessingLibrary()
|
|
222
|
+
|
|
239
223
|
let img
|
|
224
|
+
|
|
240
225
|
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
|
|
241
|
-
img =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
.toBuffer()
|
|
226
|
+
img = lib.sharp
|
|
227
|
+
.default(buffer)
|
|
228
|
+
.resize(w, h)
|
|
229
|
+
.jpeg({
|
|
230
|
+
quality: 50
|
|
231
|
+
}).toBuffer()
|
|
247
232
|
}
|
|
248
233
|
else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
const cropped = image.crop(0, 0, min, max)
|
|
254
|
-
img = await cropped.scaleToFit(720, 720).getBufferAsync(MIME_JPEG)
|
|
234
|
+
const jimp = await lib.jimp.read(buffer)
|
|
235
|
+
const min = Math.min(jimp.width, jimp.height)
|
|
236
|
+
const cropped = jimp.crop({ x: 0, y: 0, w: min, h: min })
|
|
237
|
+
img = cropped.resize({ w, h, mode: lib.jimp.ResizeStrategy.BILINEAR }).getBuffer('image/jpeg', { quality: 50 })
|
|
255
238
|
}
|
|
256
239
|
else {
|
|
257
|
-
throw new
|
|
240
|
+
throw new Boom('No image processing library available');
|
|
258
241
|
}
|
|
259
242
|
return {
|
|
260
|
-
img: await img
|
|
243
|
+
img: await img
|
|
261
244
|
}
|
|
262
245
|
}
|
|
263
246
|
|
|
264
|
-
|
|
265
247
|
/** gets the SHA256 of the given media message */
|
|
266
248
|
const mediaMessageSHA256B64 = (message) => {
|
|
267
249
|
const media = Object.values(message)[0]
|
|
268
|
-
return
|
|
250
|
+
return media?.fileSha256 && Buffer.from(media.fileSha256).toString('base64')
|
|
269
251
|
}
|
|
270
252
|
|
|
271
253
|
async function getAudioDuration(buffer) {
|
|
272
|
-
const musicMetadata = await Promise.resolve().then(() => __importStar(require('music-metadata')))
|
|
273
254
|
const options = {
|
|
274
255
|
duration: true
|
|
275
256
|
}
|
|
@@ -277,15 +258,15 @@ async function getAudioDuration(buffer) {
|
|
|
277
258
|
let metadata
|
|
278
259
|
|
|
279
260
|
if (Buffer.isBuffer(buffer)) {
|
|
280
|
-
metadata = await
|
|
261
|
+
metadata = await parseBuffer(buffer, undefined, options)
|
|
281
262
|
}
|
|
282
263
|
else if (typeof buffer === 'string') {
|
|
283
|
-
metadata = await
|
|
264
|
+
metadata = await parseFile(buffer, options)
|
|
284
265
|
}
|
|
285
266
|
else {
|
|
286
|
-
metadata = await
|
|
267
|
+
metadata = await parseStream(buffer, undefined, options)
|
|
287
268
|
}
|
|
288
|
-
return metadata.format
|
|
269
|
+
return metadata.format?.duration
|
|
289
270
|
}
|
|
290
271
|
|
|
291
272
|
/**
|
|
@@ -293,23 +274,25 @@ async function getAudioDuration(buffer) {
|
|
|
293
274
|
*/
|
|
294
275
|
async function getAudioWaveform(buffer, logger) {
|
|
295
276
|
try {
|
|
296
|
-
const { default: decoder } = await eval('import(\'audio-decode\')')
|
|
297
277
|
let audioData
|
|
278
|
+
|
|
298
279
|
if (Buffer.isBuffer(buffer)) {
|
|
299
280
|
audioData = buffer
|
|
300
281
|
}
|
|
301
282
|
else if (typeof buffer === 'string') {
|
|
302
|
-
const rStream =
|
|
283
|
+
const rStream = createReadStream(buffer)
|
|
303
284
|
audioData = await toBuffer(rStream)
|
|
304
285
|
}
|
|
305
286
|
else {
|
|
306
287
|
audioData = await toBuffer(buffer)
|
|
307
288
|
}
|
|
289
|
+
|
|
308
290
|
const audioBuffer = await decoder(audioData)
|
|
309
291
|
const rawData = audioBuffer.getChannelData(0) // We only need to work with one channel of data
|
|
310
292
|
const samples = 64 // Number of samples we want to have in our final data set
|
|
311
293
|
const blockSize = Math.floor(rawData.length / samples) // the number of samples in each subdivision
|
|
312
294
|
const filteredData = []
|
|
295
|
+
|
|
313
296
|
for (let i = 0; i < samples; i++) {
|
|
314
297
|
const blockStart = blockSize * i // the location of the first sample in the block
|
|
315
298
|
let sum = 0
|
|
@@ -318,9 +301,11 @@ async function getAudioWaveform(buffer, logger) {
|
|
|
318
301
|
}
|
|
319
302
|
filteredData.push(sum / blockSize) // divide the sum by the block size to get the average
|
|
320
303
|
}
|
|
304
|
+
|
|
321
305
|
// This guarantees that the largest data point will be set to 1, and the rest of the data will scale proportionally.
|
|
322
306
|
const multiplier = Math.pow(Math.max(...filteredData), -1)
|
|
323
307
|
const normalizedData = filteredData.map((n) => n * multiplier)
|
|
308
|
+
|
|
324
309
|
// Generate waveform like WhatsApp
|
|
325
310
|
const waveform = new Uint8Array(normalizedData.map((n) => Math.floor(100 * n)))
|
|
326
311
|
return waveform
|
|
@@ -331,7 +316,7 @@ async function getAudioWaveform(buffer, logger) {
|
|
|
331
316
|
}
|
|
332
317
|
|
|
333
318
|
const toReadable = (buffer) => {
|
|
334
|
-
const readable = new
|
|
319
|
+
const readable = new Readable({ read: () => { } })
|
|
335
320
|
readable.push(buffer)
|
|
336
321
|
readable.push(null)
|
|
337
322
|
return readable
|
|
@@ -366,30 +351,38 @@ const getStream = async (item, opts) => {
|
|
|
366
351
|
return { stream: await getHttpStream(item.url, opts), type: 'remote' }
|
|
367
352
|
}
|
|
368
353
|
|
|
369
|
-
return { stream:
|
|
354
|
+
return { stream: createReadStream(item.url), type: 'file' }
|
|
370
355
|
}
|
|
371
356
|
|
|
372
357
|
/** generates a thumbnail for a given media, if required */
|
|
373
358
|
async function generateThumbnail(file, mediaType, options) {
|
|
374
359
|
let thumbnail
|
|
375
360
|
let originalImageDimensions
|
|
361
|
+
|
|
376
362
|
if (mediaType === 'image') {
|
|
377
|
-
const { buffer, original } = await extractImageThumb(file
|
|
363
|
+
const { buffer, original } = await extractImageThumb(file)
|
|
364
|
+
|
|
378
365
|
thumbnail = buffer.toString('base64')
|
|
366
|
+
|
|
379
367
|
if (original.width && original.height) {
|
|
380
368
|
originalImageDimensions = {
|
|
381
369
|
width: original.width,
|
|
382
|
-
height: original.height
|
|
370
|
+
height: original.height
|
|
383
371
|
}
|
|
384
372
|
}
|
|
385
373
|
}
|
|
386
374
|
else if (mediaType === 'video') {
|
|
375
|
+
const imgFilename = join(getTmpFilesDirectory(), generateMessageID() + '.jpg')
|
|
387
376
|
try {
|
|
388
|
-
|
|
377
|
+
await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 })
|
|
378
|
+
const buff = await promises.readFile(imgFilename)
|
|
379
|
+
|
|
389
380
|
thumbnail = buff.toString('base64')
|
|
381
|
+
|
|
382
|
+
await promises.unlink(imgFilename)
|
|
390
383
|
}
|
|
391
384
|
catch (err) {
|
|
392
|
-
options
|
|
385
|
+
options.logger?.debug('could not generate video thumb: ' + err)
|
|
393
386
|
}
|
|
394
387
|
}
|
|
395
388
|
return {
|
|
@@ -399,16 +392,25 @@ async function generateThumbnail(file, mediaType, options) {
|
|
|
399
392
|
}
|
|
400
393
|
|
|
401
394
|
const getHttpStream = async (url, options = {}) => {
|
|
402
|
-
const
|
|
403
|
-
|
|
395
|
+
const response = await fetch(url.toString(), {
|
|
396
|
+
dispatcher: options.dispatcher,
|
|
397
|
+
method: 'GET',
|
|
398
|
+
headers: options.headers
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
if (!response.ok) {
|
|
402
|
+
throw new Boom(`Failed to fetch stream from ${url}`, { statusCode: response.status, data: { url } })
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return response.body instanceof Readable ? response.body : Readable.fromWeb(response.body)
|
|
404
406
|
}
|
|
405
407
|
|
|
406
|
-
const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
|
|
408
|
+
/*const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
|
|
407
409
|
const { stream, type } = await getStream(media, opts)
|
|
408
410
|
logger?.debug('fetched media stream')
|
|
409
411
|
|
|
410
|
-
const encFilePath =
|
|
411
|
-
const encFileWriteStream =
|
|
412
|
+
const encFilePath = join(tmpdir(), mediaType + generateMessageID() + '-plain')
|
|
413
|
+
const encFileWriteStream = createWriteStream(encFilePath)
|
|
412
414
|
|
|
413
415
|
let originalFilePath
|
|
414
416
|
let originalFileStream
|
|
@@ -416,12 +418,12 @@ const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequi
|
|
|
416
418
|
if (type === 'file') {
|
|
417
419
|
originalFilePath = media.url.toString()
|
|
418
420
|
} else if (saveOriginalFileIfRequired) {
|
|
419
|
-
originalFilePath =
|
|
420
|
-
originalFileStream =
|
|
421
|
+
originalFilePath = join(tmpdir(), mediaType + generateMessageID() + '-original')
|
|
422
|
+
originalFileStream = createWriteStream(originalFilePath)
|
|
421
423
|
}
|
|
422
424
|
|
|
423
425
|
let fileLength = 0
|
|
424
|
-
const sha256 =
|
|
426
|
+
const sha256 = createHash('sha256')
|
|
425
427
|
|
|
426
428
|
try {
|
|
427
429
|
for await (const data of stream) {
|
|
@@ -430,7 +432,7 @@ const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequi
|
|
|
430
432
|
if (type === 'remote'
|
|
431
433
|
&& opts?.maxContentLength
|
|
432
434
|
&& fileLength + data.length > opts.maxContentLength) {
|
|
433
|
-
throw new
|
|
435
|
+
throw new Boom(`content length exceeded when preparing "${type}"`, {
|
|
434
436
|
data: { media, type }
|
|
435
437
|
})
|
|
436
438
|
}
|
|
@@ -439,7 +441,7 @@ const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequi
|
|
|
439
441
|
encFileWriteStream.write(data)
|
|
440
442
|
|
|
441
443
|
if (originalFileStream && !originalFileStream.write(data)) {
|
|
442
|
-
await
|
|
444
|
+
await once(originalFileStream, 'drain')
|
|
443
445
|
}
|
|
444
446
|
}
|
|
445
447
|
|
|
@@ -466,68 +468,99 @@ const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequi
|
|
|
466
468
|
sha256.destroy()
|
|
467
469
|
stream.destroy()
|
|
468
470
|
try {
|
|
469
|
-
await
|
|
471
|
+
await promises.unlink(encFilePath)
|
|
470
472
|
if (originalFilePath && didSaveToTmpPath) {
|
|
471
|
-
await
|
|
473
|
+
await promises.unlink(originalFilePath)
|
|
472
474
|
}
|
|
473
475
|
} catch (err) {
|
|
474
476
|
logger?.error({ err }, 'failed deleting tmp files')
|
|
475
477
|
}
|
|
476
478
|
throw error
|
|
477
479
|
}
|
|
478
|
-
}
|
|
480
|
+
}*/
|
|
479
481
|
|
|
480
482
|
const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
|
|
481
483
|
const { stream, type } = await getStream(media, opts)
|
|
484
|
+
|
|
482
485
|
logger?.debug('fetched media stream')
|
|
483
|
-
|
|
486
|
+
|
|
487
|
+
const mediaKey = randomBytes(32)
|
|
484
488
|
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType)
|
|
485
|
-
const encFilePath =
|
|
486
|
-
const encFileWriteStream =
|
|
487
|
-
|
|
489
|
+
const encFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '-enc')
|
|
490
|
+
const encFileWriteStream = createWriteStream(encFilePath)
|
|
491
|
+
|
|
492
|
+
let originalFileStream;
|
|
488
493
|
let originalFilePath
|
|
494
|
+
|
|
489
495
|
if (saveOriginalFileIfRequired) {
|
|
490
|
-
originalFilePath =
|
|
491
|
-
originalFileStream =
|
|
496
|
+
originalFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '-original')
|
|
497
|
+
originalFileStream = createWriteStream(originalFilePath)
|
|
492
498
|
}
|
|
499
|
+
|
|
493
500
|
let fileLength = 0
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
const
|
|
497
|
-
const
|
|
498
|
-
const
|
|
501
|
+
|
|
502
|
+
const aes = createCipheriv('aes-256-cbc', cipherKey, iv)
|
|
503
|
+
const hmac = createHmac('sha256', macKey).update(iv)
|
|
504
|
+
const sha256Plain = createHash('sha256');
|
|
505
|
+
const sha256Enc = createHash('sha256')
|
|
506
|
+
|
|
507
|
+
const onChunk = async (buff) => {
|
|
499
508
|
sha256Enc.update(buff)
|
|
500
509
|
hmac.update(buff)
|
|
501
|
-
|
|
510
|
+
|
|
511
|
+
// Handle backpressure: if write returns false, wait for drain
|
|
512
|
+
if (!encFileWriteStream.write(buff)) {
|
|
513
|
+
await once(encFileWriteStream, 'drain')
|
|
514
|
+
}
|
|
502
515
|
}
|
|
516
|
+
|
|
503
517
|
try {
|
|
504
518
|
for await (const data of stream) {
|
|
505
519
|
fileLength += data.length
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
520
|
+
|
|
521
|
+
if (type === 'remote' &&
|
|
522
|
+
opts?.maxContentLength &&
|
|
523
|
+
fileLength + data.length > opts.maxContentLength) {
|
|
524
|
+
throw new Boom(`content length exceeded when encrypting "${type}"`, {
|
|
510
525
|
data: { media, type }
|
|
511
526
|
})
|
|
512
527
|
}
|
|
528
|
+
|
|
513
529
|
if (originalFileStream) {
|
|
514
530
|
if (!originalFileStream.write(data)) {
|
|
515
|
-
await
|
|
531
|
+
await once(originalFileStream, 'drain')
|
|
516
532
|
}
|
|
517
533
|
}
|
|
534
|
+
|
|
518
535
|
sha256Plain.update(data)
|
|
519
|
-
|
|
536
|
+
|
|
537
|
+
await onChunk(aes.update(data))
|
|
520
538
|
}
|
|
521
|
-
|
|
539
|
+
|
|
540
|
+
await onChunk(aes.final())
|
|
522
541
|
const mac = hmac.digest().slice(0, 10)
|
|
542
|
+
|
|
523
543
|
sha256Enc.update(mac)
|
|
544
|
+
|
|
524
545
|
const fileSha256 = sha256Plain.digest()
|
|
525
546
|
const fileEncSha256 = sha256Enc.digest()
|
|
547
|
+
|
|
526
548
|
encFileWriteStream.write(mac)
|
|
549
|
+
|
|
550
|
+
const encFinishPromise = once(encFileWriteStream, 'finish')
|
|
551
|
+
const originalFinishPromise = originalFileStream ? once(originalFileStream, 'finish') : Promise.resolve()
|
|
552
|
+
|
|
527
553
|
encFileWriteStream.end()
|
|
528
|
-
originalFileStream?.end?.
|
|
554
|
+
originalFileStream?.end?.()
|
|
529
555
|
stream.destroy()
|
|
556
|
+
|
|
557
|
+
// Wait for write streams to fully flush to disk
|
|
558
|
+
// This helps reduce memory pressure by allowing OS to release buffers
|
|
559
|
+
await encFinishPromise
|
|
560
|
+
await originalFinishPromise
|
|
561
|
+
|
|
530
562
|
logger?.debug('encrypted data successfully')
|
|
563
|
+
|
|
531
564
|
return {
|
|
532
565
|
mediaKey,
|
|
533
566
|
originalFilePath,
|
|
@@ -541,16 +574,18 @@ const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfReq
|
|
|
541
574
|
catch (error) {
|
|
542
575
|
// destroy all streams with error
|
|
543
576
|
encFileWriteStream.destroy()
|
|
544
|
-
originalFileStream?.destroy?.
|
|
577
|
+
originalFileStream?.destroy?.()
|
|
545
578
|
aes.destroy()
|
|
546
579
|
hmac.destroy()
|
|
547
580
|
sha256Plain.destroy()
|
|
548
581
|
sha256Enc.destroy()
|
|
549
582
|
stream.destroy()
|
|
583
|
+
|
|
550
584
|
try {
|
|
551
|
-
await
|
|
585
|
+
await promises.unlink(encFilePath)
|
|
586
|
+
|
|
552
587
|
if (originalFilePath) {
|
|
553
|
-
await
|
|
588
|
+
await promises.unlink(originalFilePath)
|
|
554
589
|
}
|
|
555
590
|
}
|
|
556
591
|
catch (err) {
|
|
@@ -574,7 +609,7 @@ const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, o
|
|
|
574
609
|
const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath)
|
|
575
610
|
|
|
576
611
|
if (!downloadUrl) {
|
|
577
|
-
throw new
|
|
612
|
+
throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 })
|
|
578
613
|
}
|
|
579
614
|
|
|
580
615
|
const keys = await getMediaKeys(mediaKey, type)
|
|
@@ -589,39 +624,51 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
|
|
|
589
624
|
let bytesFetched = 0
|
|
590
625
|
let startChunk = 0
|
|
591
626
|
let firstBlockIsIV = false
|
|
627
|
+
|
|
592
628
|
// if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
|
|
593
629
|
if (startByte) {
|
|
594
630
|
const chunk = toSmallestChunkSize(startByte || 0)
|
|
631
|
+
|
|
595
632
|
if (chunk) {
|
|
596
633
|
startChunk = chunk - AES_CHUNK_SIZE
|
|
597
634
|
bytesFetched = chunk
|
|
598
635
|
firstBlockIsIV = true
|
|
599
636
|
}
|
|
600
637
|
}
|
|
638
|
+
|
|
601
639
|
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined
|
|
640
|
+
const headersInit = options?.headers ? options.headers : undefined
|
|
602
641
|
const headers = {
|
|
603
|
-
...(
|
|
604
|
-
|
|
642
|
+
...(headersInit
|
|
643
|
+
? Array.isArray(headersInit)
|
|
644
|
+
? Object.fromEntries(headersInit)
|
|
645
|
+
: headersInit
|
|
646
|
+
: {}),
|
|
647
|
+
Origin: DEFAULT_ORIGIN
|
|
605
648
|
}
|
|
649
|
+
|
|
606
650
|
if (startChunk || endChunk) {
|
|
607
651
|
headers.Range = `bytes=${startChunk}-`
|
|
652
|
+
|
|
608
653
|
if (endChunk) {
|
|
609
654
|
headers.Range += endChunk
|
|
610
655
|
}
|
|
611
656
|
}
|
|
657
|
+
|
|
612
658
|
// download the message
|
|
613
659
|
const fetched = await getHttpStream(downloadUrl, {
|
|
614
|
-
...options || {},
|
|
615
|
-
headers
|
|
616
|
-
maxBodyLength: Infinity,
|
|
617
|
-
maxContentLength: Infinity,
|
|
660
|
+
...(options || {}),
|
|
661
|
+
headers
|
|
618
662
|
})
|
|
663
|
+
|
|
619
664
|
let remainingBytes = Buffer.from([])
|
|
620
665
|
let aes
|
|
666
|
+
|
|
621
667
|
const pushBytes = (bytes, push) => {
|
|
622
668
|
if (startByte || endByte) {
|
|
623
669
|
const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0)
|
|
624
670
|
const end = bytesFetched + bytes.length < endByte ? undefined : Math.max(endByte - bytesFetched, 0)
|
|
671
|
+
|
|
625
672
|
push(bytes.slice(start, end))
|
|
626
673
|
bytesFetched += bytes.length
|
|
627
674
|
}
|
|
@@ -629,19 +676,26 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
|
|
|
629
676
|
push(bytes)
|
|
630
677
|
}
|
|
631
678
|
}
|
|
632
|
-
|
|
679
|
+
|
|
680
|
+
const output = new Transform({
|
|
633
681
|
transform(chunk, _, callback) {
|
|
634
682
|
let data = Buffer.concat([remainingBytes, chunk])
|
|
683
|
+
|
|
635
684
|
const decryptLength = toSmallestChunkSize(data.length)
|
|
685
|
+
|
|
636
686
|
remainingBytes = data.slice(decryptLength)
|
|
637
687
|
data = data.slice(0, decryptLength)
|
|
688
|
+
|
|
638
689
|
if (!aes) {
|
|
639
690
|
let ivValue = iv
|
|
691
|
+
|
|
640
692
|
if (firstBlockIsIV) {
|
|
641
693
|
ivValue = data.slice(0, AES_CHUNK_SIZE)
|
|
642
694
|
data = data.slice(AES_CHUNK_SIZE)
|
|
643
695
|
}
|
|
644
|
-
|
|
696
|
+
|
|
697
|
+
aes = createDecipheriv('aes-256-cbc', cipherKey, ivValue)
|
|
698
|
+
|
|
645
699
|
// if an end byte that is not EOF is specified
|
|
646
700
|
// stop auto padding (PKCS7) -- otherwise throws an error for decryption
|
|
647
701
|
if (endByte) {
|
|
@@ -664,8 +718,9 @@ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startB
|
|
|
664
718
|
catch (error) {
|
|
665
719
|
callback(error)
|
|
666
720
|
}
|
|
667
|
-
}
|
|
721
|
+
}
|
|
668
722
|
})
|
|
723
|
+
|
|
669
724
|
return fetched.pipe(output, { end: true })
|
|
670
725
|
}
|
|
671
726
|
|
|
@@ -685,43 +740,176 @@ function extensionForMediaMessage(message) {
|
|
|
685
740
|
return extension
|
|
686
741
|
}
|
|
687
742
|
|
|
743
|
+
const isNodeRuntime = () => {
|
|
744
|
+
return (typeof process !== 'undefined' &&
|
|
745
|
+
process.versions?.node !== null &&
|
|
746
|
+
typeof process.versions.bun === 'undefined' &&
|
|
747
|
+
typeof globalThis.Deno === 'undefined')
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent }, redirectCount = 0) => {
|
|
751
|
+
if (redirectCount > 5) {
|
|
752
|
+
throw new Error('Too many redirects')
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const parsedUrl = new URL(url)
|
|
756
|
+
const httpModule = parsedUrl.protocol === 'https:' ? require('https') : require('http')
|
|
757
|
+
|
|
758
|
+
// Get file size for Content-Length header (required for Node.js streaming)
|
|
759
|
+
const fileStats = await promises.stat(filePath)
|
|
760
|
+
const fileSize = fileStats.size
|
|
761
|
+
|
|
762
|
+
return new Promise((resolve, reject) => {
|
|
763
|
+
const req = httpModule.request({
|
|
764
|
+
hostname: parsedUrl.hostname,
|
|
765
|
+
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
766
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
767
|
+
method: 'POST',
|
|
768
|
+
headers: {
|
|
769
|
+
...headers,
|
|
770
|
+
'Content-Length': fileSize
|
|
771
|
+
},
|
|
772
|
+
agent,
|
|
773
|
+
timeout: timeoutMs
|
|
774
|
+
}, res => {
|
|
775
|
+
// Handle redirects (3xx)
|
|
776
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
777
|
+
res.resume() // Consume response to free resources
|
|
778
|
+
|
|
779
|
+
const newUrl = new URL(res.headers.location, url).toString()
|
|
780
|
+
|
|
781
|
+
resolve(uploadWithNodeHttp({
|
|
782
|
+
url: newUrl,
|
|
783
|
+
filePath,
|
|
784
|
+
headers,
|
|
785
|
+
timeoutMs,
|
|
786
|
+
agent
|
|
787
|
+
}, redirectCount + 1))
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
let body = ''
|
|
792
|
+
|
|
793
|
+
res.on('data', chunk => (body += chunk))
|
|
794
|
+
res.on('end', () => {
|
|
795
|
+
try {
|
|
796
|
+
resolve(JSON.parse(body))
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
resolve(undefined)
|
|
800
|
+
}
|
|
801
|
+
})
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
req.on('error', reject)
|
|
805
|
+
req.on('timeout', () => {
|
|
806
|
+
req.destroy()
|
|
807
|
+
reject(new Error('Upload timeout'))
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
const stream = createReadStream(filePath)
|
|
811
|
+
|
|
812
|
+
stream.pipe(req)
|
|
813
|
+
stream.on('error', err => {
|
|
814
|
+
req.destroy()
|
|
815
|
+
reject(err)
|
|
816
|
+
})
|
|
817
|
+
})
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {
|
|
821
|
+
// Convert Node.js Readable to Web ReadableStream
|
|
822
|
+
const nodeStream = createReadStream(filePath)
|
|
823
|
+
const webStream = Readable.toWeb(nodeStream)
|
|
824
|
+
const response = await fetch(url, {
|
|
825
|
+
dispatcher: agent,
|
|
826
|
+
method: 'POST',
|
|
827
|
+
body: webStream,
|
|
828
|
+
headers,
|
|
829
|
+
duplex: 'half',
|
|
830
|
+
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
try {
|
|
834
|
+
return (await response.json())
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return undefined
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Uploads media to WhatsApp servers.
|
|
843
|
+
*
|
|
844
|
+
* ## Why we have two upload implementations:
|
|
845
|
+
*
|
|
846
|
+
* Node.js's native `fetch` (powered by undici) has a known bug where it buffers
|
|
847
|
+
* the entire request body in memory before sending, even when using streams.
|
|
848
|
+
* This causes memory issues with large files (e.g., 1GB file = 1GB+ memory usage).
|
|
849
|
+
* See: https://github.com/nodejs/undici/issues/4058
|
|
850
|
+
*
|
|
851
|
+
* Other runtimes (Bun, Deno, browsers) correctly stream the request body without
|
|
852
|
+
* buffering, so we can use the web-standard Fetch API there.
|
|
853
|
+
*
|
|
854
|
+
* ## Future considerations:
|
|
855
|
+
* Once the undici bug is fixed, we can simplify this to use only the Fetch API
|
|
856
|
+
* across all runtimes. Monitor the GitHub issue for updates.
|
|
857
|
+
*/
|
|
858
|
+
const uploadMedia = async (params, logger) => {
|
|
859
|
+
if (isNodeRuntime()) {
|
|
860
|
+
logger?.debug('Using Node.js https module for upload (avoids undici buffering bug)')
|
|
861
|
+
return uploadWithNodeHttp(params)
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
logger?.debug('Using web-standard Fetch API for upload');
|
|
865
|
+
return uploadWithFetch(params)
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
688
869
|
const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
|
|
689
|
-
return async (filePath, { mediaType, fileEncSha256B64,
|
|
870
|
+
return async (filePath, { mediaType, fileEncSha256B64, timeoutMs }) => {
|
|
690
871
|
// send a query JSON to obtain the url & auth token to upload our media
|
|
691
872
|
let uploadInfo = await refreshMediaConn(false)
|
|
692
873
|
let urls
|
|
693
|
-
|
|
874
|
+
|
|
694
875
|
const hosts = [...customUploadHosts, ...uploadInfo.hosts]
|
|
876
|
+
|
|
695
877
|
fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
|
|
696
|
-
|
|
697
|
-
|
|
878
|
+
|
|
879
|
+
// Prepare common headers
|
|
880
|
+
const customHeaders = (() => {
|
|
881
|
+
const hdrs = options?.headers;
|
|
882
|
+
if (!hdrs)
|
|
883
|
+
return {};
|
|
884
|
+
return Array.isArray(hdrs) ? Object.fromEntries(hdrs) : hdrs
|
|
885
|
+
})()
|
|
886
|
+
|
|
887
|
+
const headers = {
|
|
888
|
+
...customHeaders,
|
|
889
|
+
'Content-Type': 'application/octet-stream',
|
|
890
|
+
Origin: DEFAULT_ORIGIN
|
|
698
891
|
}
|
|
892
|
+
|
|
699
893
|
for (const { hostname } of hosts) {
|
|
700
894
|
logger.debug(`uploading to "${hostname}"`)
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
895
|
+
|
|
896
|
+
const auth = encodeURIComponent(uploadInfo.auth)
|
|
897
|
+
const url = `https://${hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
|
|
898
|
+
|
|
704
899
|
let result
|
|
900
|
+
|
|
705
901
|
try {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
headers
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
httpsAgent: fetchAgent,
|
|
715
|
-
timeout: timeoutMs,
|
|
716
|
-
responseType: 'json',
|
|
717
|
-
maxBodyLength: Infinity,
|
|
718
|
-
maxContentLength: Infinity,
|
|
719
|
-
})
|
|
720
|
-
result = body.data
|
|
721
|
-
if (result?.url || result?.directPath) {
|
|
902
|
+
result = await uploadMedia({
|
|
903
|
+
url,
|
|
904
|
+
filePath,
|
|
905
|
+
headers,
|
|
906
|
+
timeoutMs,
|
|
907
|
+
agent: fetchAgent
|
|
908
|
+
}, logger);
|
|
909
|
+
if (result?.url || result?.direct_path) {
|
|
722
910
|
urls = {
|
|
723
911
|
mediaUrl: result.url,
|
|
724
|
-
directPath: result.direct_path,
|
|
912
|
+
directPath: result.direct_path,
|
|
725
913
|
meta_hmac: result.meta_hmac,
|
|
726
914
|
fbid: result.fbid,
|
|
727
915
|
ts: result.ts
|
|
@@ -734,37 +922,36 @@ const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options },
|
|
|
734
922
|
}
|
|
735
923
|
}
|
|
736
924
|
catch (error) {
|
|
737
|
-
if (axios_1.default.isAxiosError(error)) {
|
|
738
|
-
result = error.response?.data
|
|
739
|
-
}
|
|
740
925
|
const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname
|
|
741
|
-
logger.warn({ trace: error
|
|
926
|
+
logger.warn({ trace: error?.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`)
|
|
742
927
|
}
|
|
743
928
|
}
|
|
929
|
+
|
|
744
930
|
if (!urls) {
|
|
745
|
-
throw new
|
|
931
|
+
throw new Boom('Media upload failed on all hosts', { statusCode: 500 })
|
|
746
932
|
}
|
|
933
|
+
|
|
747
934
|
return urls
|
|
748
935
|
}
|
|
749
936
|
}
|
|
750
937
|
|
|
751
938
|
const getMediaRetryKey = (mediaKey) => {
|
|
752
|
-
return
|
|
939
|
+
return hkdf(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' })
|
|
753
940
|
}
|
|
754
941
|
/**
|
|
755
942
|
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
|
|
756
943
|
*/
|
|
757
944
|
const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
|
|
758
945
|
const recp = { stanzaId: key.id }
|
|
759
|
-
const recpBuffer =
|
|
760
|
-
const iv =
|
|
946
|
+
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
|
|
947
|
+
const iv = randomBytes(12)
|
|
761
948
|
const retryKey = await getMediaRetryKey(mediaKey)
|
|
762
|
-
const ciphertext =
|
|
949
|
+
const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id))
|
|
763
950
|
const req = {
|
|
764
951
|
tag: 'receipt',
|
|
765
952
|
attrs: {
|
|
766
953
|
id: key.id,
|
|
767
|
-
to:
|
|
954
|
+
to: jidNormalizedUser(meId),
|
|
768
955
|
type: 'server-error'
|
|
769
956
|
},
|
|
770
957
|
content: [
|
|
@@ -783,8 +970,7 @@ const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
|
|
|
783
970
|
tag: 'rmr',
|
|
784
971
|
attrs: {
|
|
785
972
|
jid: key.remoteJid,
|
|
786
|
-
|
|
787
|
-
// @ts-ignore
|
|
973
|
+
from_me: (!!key.fromMe).toString(),
|
|
788
974
|
participant: key.participant || undefined
|
|
789
975
|
}
|
|
790
976
|
}
|
|
@@ -794,7 +980,7 @@ const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
|
|
|
794
980
|
}
|
|
795
981
|
|
|
796
982
|
const decodeMediaRetryNode = (node) => {
|
|
797
|
-
const rmrNode =
|
|
983
|
+
const rmrNode = getBinaryNodeChild(node, 'rmr')
|
|
798
984
|
const event = {
|
|
799
985
|
key: {
|
|
800
986
|
id: node.attrs.id,
|
|
@@ -803,20 +989,20 @@ const decodeMediaRetryNode = (node) => {
|
|
|
803
989
|
participant: rmrNode.attrs.participant
|
|
804
990
|
}
|
|
805
991
|
}
|
|
806
|
-
const errorNode =
|
|
992
|
+
const errorNode = getBinaryNodeChild(node, 'error')
|
|
807
993
|
if (errorNode) {
|
|
808
994
|
const errorCode = +errorNode.attrs.code
|
|
809
|
-
event.error = new
|
|
995
|
+
event.error = new Boom(`Failed to re-upload media (${errorCode})`, { data: errorNode.attrs, statusCode: getStatusCodeForMediaRetry(errorCode) })
|
|
810
996
|
}
|
|
811
997
|
else {
|
|
812
|
-
const encryptedInfoNode =
|
|
813
|
-
const ciphertext =
|
|
814
|
-
const iv =
|
|
998
|
+
const encryptedInfoNode = getBinaryNodeChild(node, 'encrypt')
|
|
999
|
+
const ciphertext = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_p')
|
|
1000
|
+
const iv = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_iv')
|
|
815
1001
|
if (ciphertext && iv) {
|
|
816
1002
|
event.media = { ciphertext, iv }
|
|
817
1003
|
}
|
|
818
1004
|
else {
|
|
819
|
-
event.error = new
|
|
1005
|
+
event.error = new Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 })
|
|
820
1006
|
}
|
|
821
1007
|
}
|
|
822
1008
|
return event
|
|
@@ -824,17 +1010,17 @@ const decodeMediaRetryNode = (node) => {
|
|
|
824
1010
|
|
|
825
1011
|
const decryptMediaRetryData = async ({ ciphertext, iv }, mediaKey, msgId) => {
|
|
826
1012
|
const retryKey = await getMediaRetryKey(mediaKey)
|
|
827
|
-
const plaintext =
|
|
828
|
-
return
|
|
1013
|
+
const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId))
|
|
1014
|
+
return proto.MediaRetryNotification.decode(plaintext)
|
|
829
1015
|
}
|
|
830
1016
|
|
|
831
1017
|
const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code]
|
|
832
1018
|
|
|
833
1019
|
const MEDIA_RETRY_STATUS_MAP = {
|
|
834
|
-
[
|
|
835
|
-
[
|
|
836
|
-
[
|
|
837
|
-
[
|
|
1020
|
+
[proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
|
|
1021
|
+
[proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
|
|
1022
|
+
[proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
|
|
1023
|
+
[proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418,
|
|
838
1024
|
}
|
|
839
1025
|
|
|
840
1026
|
module.exports = {
|
|
@@ -852,12 +1038,13 @@ module.exports = {
|
|
|
852
1038
|
getStream,
|
|
853
1039
|
generateThumbnail,
|
|
854
1040
|
getHttpStream,
|
|
855
|
-
prepareStream,
|
|
1041
|
+
//prepareStream,
|
|
856
1042
|
encryptedStream,
|
|
857
1043
|
getUrlFromDirectPath,
|
|
858
1044
|
downloadContentFromMessage,
|
|
859
1045
|
downloadEncryptedContent,
|
|
860
1046
|
extensionForMediaMessage,
|
|
1047
|
+
uploadWithNodeHttp,
|
|
861
1048
|
getRawMediaUploadData,
|
|
862
1049
|
getWAUploadToServer,
|
|
863
1050
|
getMediaRetryKey,
|