@badzz88/baileys 8.4.7 → 8.4.9

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