kage-baileys 3.0.6 → 3.2.4

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 (217) hide show
  1. package/Example/connection.cjs +4 -0
  2. package/README.MD +4726 -48
  3. package/README.md +5941 -0
  4. package/WAProto/WAProto.proto +3395 -415
  5. package/WAProto/compat.cjs +12 -0
  6. package/WAProto/index.d.ts +47104 -19888
  7. package/WAProto/index.js +131979 -57361
  8. package/lib/Addon/http-error.js +43 -0
  9. package/lib/Addon/index.js +32 -0
  10. package/lib/Addon/keyed-db.js +250 -0
  11. package/lib/Addon/link-preview.js +82 -0
  12. package/lib/Defaults/baileys-version.json +3 -0
  13. package/lib/Defaults/connection.js +110 -0
  14. package/lib/Defaults/constants.js +156 -0
  15. package/lib/Defaults/history.js +46 -0
  16. package/lib/Defaults/index.js +230 -0
  17. package/lib/Defaults/media.js +104 -0
  18. package/lib/Defaults/phonenumber-mcc.json +223 -0
  19. package/lib/Defaults/prefix.js +44 -0
  20. package/lib/Extensions/business-advanced.js +382 -0
  21. package/lib/Extensions/business.js +159 -0
  22. package/lib/Extensions/chats-advanced.js +1106 -0
  23. package/lib/Extensions/chats.js +349 -0
  24. package/lib/Extensions/communities-advanced.js +430 -0
  25. package/lib/Extensions/community.js +94 -0
  26. package/lib/Extensions/groups-advanced.js +343 -0
  27. package/lib/Extensions/index.js +30 -0
  28. package/lib/Extensions/messages-recv-advanced.js +1690 -0
  29. package/lib/Extensions/messages-send-advanced.js +622 -0
  30. package/lib/Extensions/messages-send.js +1380 -0
  31. package/lib/Extensions/newsletter-advanced.js +228 -0
  32. package/lib/Extensions/newsletter.js +72 -0
  33. package/lib/Extensions/socket.js +389 -0
  34. package/lib/Modded/message_builder.js +2084 -4
  35. package/lib/Signal/Group/ciphertext-message.js +54 -0
  36. package/lib/Signal/Group/group-session-builder.js +124 -0
  37. package/lib/Signal/Group/group_cipher.js +216 -0
  38. package/lib/Signal/Group/index.js +124 -0
  39. package/lib/Signal/Group/keyhelper.js +108 -0
  40. package/lib/Signal/Group/sender-chain-key.js +83 -0
  41. package/lib/Signal/Group/sender-key-distribution-message.js +157 -0
  42. package/lib/Signal/Group/sender-key-message.js +166 -0
  43. package/lib/Signal/Group/sender-key-name.js +130 -0
  44. package/lib/Signal/Group/sender-key-record.js +113 -0
  45. package/lib/Signal/Group/sender-key-state.js +208 -0
  46. package/lib/Signal/Group/sender-message-key.js +82 -0
  47. package/lib/Signal/libsignal.js +935 -0
  48. package/lib/Signal/lid-mapping.js +545 -1
  49. package/lib/Socket/Client/index.js +60 -0
  50. package/lib/Socket/Client/types.js +51 -0
  51. package/lib/Socket/Client/websocket.js +152 -0
  52. package/lib/Socket/business.js +831 -0
  53. package/lib/Socket/chats.js +2535 -49
  54. package/lib/Socket/communities.js +450 -1
  55. package/lib/Socket/community.js +1022 -0
  56. package/lib/Socket/dugong.js +1 -1
  57. package/lib/Socket/groups.js +747 -0
  58. package/lib/Socket/index.js +60 -2
  59. package/lib/Socket/messages-recv.js +3450 -5
  60. package/lib/Socket/messages-send.js +3123 -0
  61. package/lib/Socket/mex.js +126 -1
  62. package/lib/Socket/newsletter.js +465 -0
  63. package/lib/Socket/socket.js +2217 -1
  64. package/lib/Store/index.js +79 -0
  65. package/lib/Store/make-cache-manager-store.js +186 -0
  66. package/lib/Store/make-in-memory-store.js +878 -0
  67. package/lib/Store/make-ordered-dictionary.js +187 -0
  68. package/lib/Store/object-repository.js +78 -0
  69. package/lib/Types/Auth.js +25 -0
  70. package/lib/Types/Bussines.js +25 -1
  71. package/lib/Types/Call.js +25 -0
  72. package/lib/Types/Chat.js +39 -0
  73. package/lib/Types/Contact.js +25 -0
  74. package/lib/Types/Events.js +25 -0
  75. package/lib/Types/GroupMetadata.js +25 -0
  76. package/lib/Types/Label.js +76 -0
  77. package/lib/Types/LabelAssociation.js +40 -0
  78. package/lib/Types/Message.js +57 -0
  79. package/lib/Types/Mex.js +56 -1
  80. package/lib/Types/MexUpdates.js +46 -0
  81. package/lib/Types/Newsletter.js +56 -0
  82. package/lib/Types/Product.js +25 -0
  83. package/lib/Types/RichType.js +23 -0
  84. package/lib/Types/Signal.js +25 -0
  85. package/lib/Types/Socket.js +25 -0
  86. package/lib/Types/State.js +92 -0
  87. package/lib/Types/USync.js +25 -0
  88. package/lib/Types/index.js +133 -0
  89. package/lib/Utils/auth-utils.js +703 -0
  90. package/lib/Utils/automation-tools.js +51 -16
  91. package/lib/Utils/baileys-event-stream.js +146 -0
  92. package/lib/Utils/browser-utils.js +95 -0
  93. package/lib/Utils/business.js +525 -0
  94. package/lib/Utils/chat-utils.js +1861 -0
  95. package/lib/Utils/companion-reg-client-utils.js +55 -1
  96. package/lib/Utils/crypto.js +355 -0
  97. package/lib/Utils/decode-wa-message.js +709 -0
  98. package/lib/Utils/event-buffer.js +1323 -8
  99. package/lib/Utils/generics.js +945 -1
  100. package/lib/Utils/history.js +266 -0
  101. package/lib/Utils/identity-change-handler.js +62 -1
  102. package/lib/Utils/index.js +114 -0
  103. package/lib/Utils/link-preview.js +179 -0
  104. package/lib/Utils/logger.js +42 -0
  105. package/lib/Utils/lt-hash.js +73 -0
  106. package/lib/Utils/make-mutex.js +104 -0
  107. package/lib/Utils/message-inspector.js +110 -0
  108. package/lib/Utils/message-retry-manager.js +507 -2
  109. package/lib/Utils/messages-media.js +1931 -1
  110. package/lib/Utils/messages.js +3748 -0
  111. package/lib/Utils/noise-handler.js +423 -0
  112. package/lib/Utils/offline-node-processor.js +53 -1
  113. package/lib/Utils/pre-key-manager.js +255 -1
  114. package/lib/Utils/process-message.js +1592 -0
  115. package/lib/Utils/reporting-utils.js +272 -1
  116. package/lib/Utils/rich-message-utils.js +377 -0
  117. package/lib/Utils/rich-messages.js +1 -1
  118. package/lib/Utils/signal.js +437 -0
  119. package/lib/Utils/stanza-ack.js +51 -1
  120. package/lib/Utils/status-refresh.js +37 -0
  121. package/lib/Utils/status-tools.js +4 -2
  122. package/lib/Utils/sync-action-utils.js +63 -1
  123. package/lib/Utils/tc-token-utils.js +185 -1
  124. package/lib/Utils/terminal-banner.js +31 -0
  125. package/lib/Utils/use-multi-file-auth-state.js +271 -0
  126. package/lib/Utils/use-single-file-auth-state.js +108 -0
  127. package/lib/Utils/use-sqlite-auth-state.js +142 -0
  128. package/lib/Utils/validate-connection.js +497 -1
  129. package/lib/Utils/view-once-compat.js +13 -5
  130. package/lib/VoIP/audio-feeder.js +150 -0
  131. package/lib/VoIP/index.js +289 -0
  132. package/lib/VoIP/relay-transport.js +608 -0
  133. package/lib/VoIP/signaling.js +583 -0
  134. package/lib/VoIP/types.js +30 -0
  135. package/lib/VoIP/wasm-engine.js +1261 -2
  136. package/lib/VoIP/worker-bootstrap.js +1090 -2
  137. package/lib/WABinary/constants.js +2805 -0
  138. package/lib/WABinary/decode.js +649 -2
  139. package/lib/WABinary/encode.js +564 -2
  140. package/lib/WABinary/generic-utils.js +401 -0
  141. package/lib/WABinary/index.js +82 -0
  142. package/lib/WABinary/jid-utils.js +290 -0
  143. package/lib/WABinary/types.js +25 -0
  144. package/lib/WAM/BinaryInfo.js +50 -0
  145. package/lib/WAM/constants.js +45747 -0
  146. package/lib/WAM/encode.js +393 -0
  147. package/lib/WAM/index.js +78 -0
  148. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +135 -0
  149. package/lib/WAUSync/Protocols/USyncContactProtocol.js +110 -0
  150. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +142 -0
  151. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +85 -0
  152. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +69 -0
  153. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +107 -0
  154. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +38 -1
  155. package/lib/WAUSync/Protocols/index.js +83 -0
  156. package/lib/WAUSync/USyncQuery.js +241 -2
  157. package/lib/WAUSync/USyncUser.js +86 -0
  158. package/lib/WAUSync/index.js +78 -0
  159. package/lib/assets/wasm/loader.js +1875 -1
  160. package/lib/assets/wasm/worker-modules.js +11047 -1
  161. package/lib/index.d.ts +126 -0
  162. package/lib/index.js +53 -21
  163. package/lib/index.mjs +437 -0
  164. package/licenses/upstream-MIT.txt +22 -0
  165. package/maintenance/api-coverage.json +1642 -0
  166. package/maintenance/archivos-sha256.json +207 -0
  167. package/maintenance/consumer-validation.log +3 -0
  168. package/maintenance/dependency-audit.json +4 -4
  169. package/maintenance/extension-inventory.json +57 -0
  170. package/maintenance/protocol-update.json +1101 -0
  171. package/maintenance/protocol-wire-audit.json +4 -0
  172. package/maintenance/reference-socket-surface.json +209 -0
  173. package/maintenance/resumen-validacion.json +50 -0
  174. package/maintenance/validation.log +2725 -0
  175. package/maintenance/verification.json +566 -0
  176. package/package.json +107 -9
  177. package/scripts/check-imports.cjs +10 -0
  178. package/scripts/check-package.cjs +40 -0
  179. package/scripts/check-syntax.cjs +1 -1
  180. package/scripts/generate-protobuf.cjs +6 -1
  181. package/scripts/generate-public-types.cjs +9 -0
  182. package/scripts/kage-base64-fix.cjs +45 -0
  183. package/scripts/maintenance.cjs +10 -0
  184. package/test/advanced.test.cjs +24 -0
  185. package/test/base64-fix.test.cjs +49 -0
  186. package/test/maintenance.test.cjs +1 -1
  187. package/test/npm-publication.test.cjs +141 -0
  188. package/test/recepcion-reparada.test.cjs +98 -0
  189. package/test/resilience-binary.test.cjs +69 -0
  190. package/test/resilience-transport.test.cjs +34 -0
  191. package/test/types.test.ts +5 -0
  192. package/test/upgrade.test.cjs +104 -0
  193. package/tsconfig.json +20 -0
  194. package/WAProto/p.html +0 -1
  195. package/lib/KageExtra/Modded/message_builder.js +0 -2330
  196. package/lib/KageExtra/VoIP/audio-feeder.js +0 -136
  197. package/lib/KageExtra/VoIP/index.js +0 -282
  198. package/lib/KageExtra/VoIP/relay-transport.js +0 -559
  199. package/lib/KageExtra/VoIP/signaling.js +0 -603
  200. package/lib/KageExtra/VoIP/types.js +0 -17
  201. package/lib/KageExtra/VoIP/wasm-engine.js +0 -1214
  202. package/lib/KageExtra/VoIP/worker-bootstrap.js +0 -1042
  203. package/lib/KageExtra/assets/wasm/loader.js +0 -5
  204. package/lib/KageExtra/assets/wasm/whatsapp.wasm +0 -0
  205. package/lib/KageExtra/assets/wasm/worker-modules.js +0 -273
  206. package/lib/KageExtra/index.js +0 -3
  207. package/lib/KageExtra/package.json +0 -1
  208. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +0 -53
  209. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +0 -24
  210. package/maintenance/revision-full-check.txt +0 -1097
  211. package/maintenance/revision-library-tests.tap +0 -1088
  212. package/maintenance/revision-protobuf.txt +0 -5
  213. package/maintenance/revision-syntax.txt +0 -5
  214. package/maintenance/schema-coverage.json +0 -23
  215. package/maintenance/schema-merge.json +0 -30
  216. package/maintenance/validation-node20.txt +0 -191
  217. package/maintenance/validation-node22.txt +0 -237
@@ -871,4 +871,1934 @@ const MEDIA_RETRY_STATUS_MAP = {
871
871
  };
872
872
  function __importStar(arg0) {
873
873
  throw new Error('Function not implemented.');
874
- }
874
+ }
875
+ // Kage: compatibilidad de API.
876
+ const __kageModule73 = (() => {
877
+ const module = { exports: {} }; const exports = module.exports;
878
+ "use strict"
879
+
880
+ Object.defineProperty(exports, "__esModule", { value: true })
881
+
882
+ const { Boom } = require("@hapi/boom")
883
+ const { exec } = require("child_process")
884
+ const { once } = require("events")
885
+ const {
886
+ createHash,
887
+ randomBytes,
888
+ createHmac,
889
+ createCipheriv,
890
+ createDecipheriv
891
+ } = require("crypto")
892
+ const {
893
+ promises,
894
+ createReadStream,
895
+ createWriteStream
896
+ } = require("fs")
897
+ const {
898
+ parseBuffer,
899
+ parseFile,
900
+ parseStream
901
+ } = require('music-metadata')
902
+ const { tmpdir } = require("os")
903
+ const { join } = require("path")
904
+ const {
905
+ Readable,
906
+ Transform
907
+ } = require("stream")
908
+ const { proto } = require("../../WAProto")
909
+ const {
910
+ MEDIA_PATH_MAP,
911
+ MEDIA_HKDF_KEY_MAPPING
912
+ } = require("../Defaults/media")
913
+ const { DEFAULT_ORIGIN } = require("../Defaults/constants")
914
+ const {
915
+ getBinaryNodeChild,
916
+ getBinaryNodeChildBuffer,
917
+ jidNormalizedUser
918
+ } = require("../WABinary")
919
+ const {
920
+ aesDecryptGCM,
921
+ aesEncryptGCM,
922
+ hkdf
923
+ } = require("./crypto")
924
+ const { generateMessageID } = require("./generics")
925
+
926
+ const getTmpFilesDirectory = () => tmpdir()
927
+
928
+ const getImageProcessingLibrary = () => {
929
+ let sharp, jimp
930
+
931
+ try {
932
+ sharp = require('sharp')
933
+ } catch {}
934
+
935
+ if (sharp) {
936
+ return { sharp }
937
+ }
938
+
939
+ try {
940
+ jimp = require('jimp')
941
+ } catch {}
942
+
943
+ if (jimp) {
944
+ return { jimp }
945
+ }
946
+
947
+ throw new Boom('No image processing library available')
948
+ }
949
+
950
+ const hkdfInfoKey = (type) => {
951
+ const hkdfInfo = MEDIA_HKDF_KEY_MAPPING[type]
952
+ return `WhatsApp ${hkdfInfo} Keys`
953
+ }
954
+
955
+ const getRawMediaUploadData = async (media, mediaType, logger) => {
956
+ const { stream } = await getStream(media)
957
+
958
+ logger?.debug('got stream for raw upload')
959
+
960
+ const hasher = createHash('sha256')
961
+ const filePath = join(getTmpFilesDirectory(), mediaType + generateMessageID())
962
+ const fileWriteStream = createWriteStream(filePath)
963
+
964
+ let fileLength = 0
965
+
966
+ try {
967
+ for await (const data of stream) {
968
+ fileLength += data.length
969
+ hasher.update(data)
970
+
971
+ if (!fileWriteStream.write(data)) {
972
+ await once(fileWriteStream, 'drain')
973
+ }
974
+ }
975
+
976
+ fileWriteStream.end()
977
+ await once(fileWriteStream, 'finish')
978
+ stream.destroy()
979
+
980
+ const fileSha256 = hasher.digest()
981
+
982
+ logger?.debug('hashed data for raw upload')
983
+
984
+ return {
985
+ filePath: filePath,
986
+ fileSha256,
987
+ fileLength
988
+ }
989
+ }
990
+ catch (error) {
991
+ fileWriteStream.destroy()
992
+ stream.destroy()
993
+
994
+ try {
995
+ await promises.unlink(filePath)
996
+ }
997
+ catch {
998
+ //
999
+ }
1000
+ throw error
1001
+ }
1002
+ }
1003
+
1004
+ /** generates all the keys required to encrypt/decrypt & sign a media message */
1005
+ async function getMediaKeys(buffer, mediaType) {
1006
+ if (!buffer) {
1007
+ throw new Boom('Cannot derive from empty media key')
1008
+ }
1009
+ if (typeof buffer === 'string') {
1010
+ buffer = Buffer.from(buffer.replace('data:base64,', ''), 'base64')
1011
+ }
1012
+ // expand using HKDF to 112 bytes, also pass in the relevant app info
1013
+ const expandedMediaKey = await hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
1014
+ return {
1015
+ iv: expandedMediaKey.slice(0, 16),
1016
+ cipherKey: expandedMediaKey.slice(16, 48),
1017
+ macKey: expandedMediaKey.slice(48, 80)
1018
+ }
1019
+ }
1020
+
1021
+ /** Extracts video thumb using FFMPEG */
1022
+ const extractVideoThumb = async (path, destPath, time, size) => new Promise((resolve, reject) => {
1023
+ const cmd = `ffmpeg -ss ${time} -i ${path} -y -vf scale=${size.width}:-1 -vframes 1 -f image2 ${destPath}`
1024
+ exec(cmd, err => {
1025
+ if (err) {
1026
+ reject(err)
1027
+ }
1028
+ else {
1029
+ resolve()
1030
+ }
1031
+ })
1032
+ })
1033
+
1034
+ const extractImageThumb = async (bufferOrFilePath, width = 32, quality = 50) => {
1035
+ // TODO: Move entirely to sharp, removing jimp as it supports readable streams
1036
+ // This will have positive speed and performance impacts as well as minimizing RAM usage.
1037
+ if (bufferOrFilePath instanceof Readable) {
1038
+ bufferOrFilePath = await toBuffer(bufferOrFilePath)
1039
+ }
1040
+
1041
+ const lib = await getImageProcessingLibrary()
1042
+
1043
+ if ('sharp' in lib && typeof lib.sharp === 'function') {
1044
+ const img = lib.sharp(bufferOrFilePath)
1045
+ const dimensions = await img.metadata()
1046
+ const buffer = await img.resize(width).jpeg({ quality: 50 }).toBuffer()
1047
+ return {
1048
+ buffer,
1049
+ original: {
1050
+ width: dimensions.width,
1051
+ height: dimensions.height
1052
+ }
1053
+ }
1054
+ }
1055
+ else if ('jimp' in lib && typeof lib.jimp.read === 'function') {
1056
+ const { read, MIME_JPEG, RESIZE_BEZIER, AUTO } = lib.jimp
1057
+ const jimp = await read(bufferOrFilePath)
1058
+ const dimensions = {
1059
+ width: jimp.getWidth(),
1060
+ height: jimp.getHeight()
1061
+ }
1062
+ const buffer = await jimp
1063
+ .quality(quality)
1064
+ .resize(width, AUTO, RESIZE_BEZIER)
1065
+ .getBufferAsync(MIME_JPEG)
1066
+ return {
1067
+ buffer,
1068
+ original: dimensions
1069
+ }
1070
+ }
1071
+ else {
1072
+ throw new Boom('No image processing library available')
1073
+ }
1074
+ }
1075
+
1076
+ const encodeBase64EncodedStringForUpload = (b64) => (encodeURIComponent(b64
1077
+ .replace(/\+/g, '-')
1078
+ .replace(/\//g, '_')
1079
+ .replace(/\=+$/, '')))
1080
+
1081
+ const generateProfilePicture = async (mediaUpload, dimensions) => {
1082
+ let buffer
1083
+
1084
+ const { width: w = 640, height: h = 640 } = dimensions || {}
1085
+
1086
+ if (Buffer.isBuffer(mediaUpload)) {
1087
+ buffer = mediaUpload
1088
+ }
1089
+ else {
1090
+ // Use getStream to handle all WAMediaUpload types (Buffer, Stream, URL)
1091
+ const { stream } = await getStream(mediaUpload)
1092
+ // Convert the resulting stream to a buffer
1093
+ buffer = await toBuffer(stream)
1094
+ }
1095
+ const lib = await getImageProcessingLibrary()
1096
+
1097
+ let img
1098
+
1099
+ if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
1100
+ img = lib.sharp
1101
+ .default(buffer)
1102
+ .resize(w, h)
1103
+ .jpeg({
1104
+ quality: 50
1105
+ }).toBuffer()
1106
+ }
1107
+ else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
1108
+ const jimp = await lib.jimp.read(buffer)
1109
+ const min = Math.min(jimp.width, jimp.height)
1110
+ const cropped = jimp.crop({ x: 0, y: 0, w: min, h: min })
1111
+ img = cropped.resize({ w, h, mode: lib.jimp.ResizeStrategy.BILINEAR }).getBuffer('image/jpeg', { quality: 50 })
1112
+ }
1113
+ else {
1114
+ throw new Boom('No image processing library available');
1115
+ }
1116
+ return {
1117
+ img: await img
1118
+ }
1119
+ }
1120
+
1121
+ /** gets the SHA256 of the given media message */
1122
+ const mediaMessageSHA256B64 = (message) => {
1123
+ const media = Object.values(message)[0]
1124
+ return media?.fileSha256 && Buffer.from(media.fileSha256).toString('base64')
1125
+ }
1126
+
1127
+ async function getAudioDuration(buffer) {
1128
+ const options = {
1129
+ duration: true
1130
+ }
1131
+
1132
+ let metadata
1133
+
1134
+ if (Buffer.isBuffer(buffer)) {
1135
+ metadata = await parseBuffer(buffer, undefined, options)
1136
+ }
1137
+ else if (typeof buffer === 'string') {
1138
+ metadata = await parseFile(buffer, options)
1139
+ }
1140
+ else {
1141
+ metadata = await parseStream(buffer, undefined, options)
1142
+ }
1143
+ return metadata.format?.duration
1144
+ }
1145
+
1146
+ /**
1147
+ referenced from and modifying https://github.com/wppconnect-team/wa-js/blob/main/src/chat/functions/prepareAudioWaveform.ts
1148
+ */
1149
+ async function getAudioWaveform(buffer, logger) {
1150
+ try {
1151
+ const { default: decoder } = await eval('import(\'audio-decode\')')
1152
+
1153
+ let audioData
1154
+
1155
+ if (Buffer.isBuffer(buffer)) {
1156
+ audioData = buffer
1157
+ }
1158
+ else if (typeof buffer === 'string') {
1159
+ const rStream = createReadStream(buffer)
1160
+ audioData = await toBuffer(rStream)
1161
+ }
1162
+ else {
1163
+ audioData = await toBuffer(buffer)
1164
+ }
1165
+
1166
+ const audioBuffer = await decoder(audioData)
1167
+ const rawData = audioBuffer.getChannelData(0) // We only need to work with one channel of data
1168
+ const samples = 64 // Number of samples we want to have in our final data set
1169
+ const blockSize = Math.floor(rawData.length / samples) // the number of samples in each subdivision
1170
+ const filteredData = []
1171
+
1172
+ for (let i = 0; i < samples; i++) {
1173
+ const blockStart = blockSize * i // the location of the first sample in the block
1174
+ let sum = 0
1175
+ for (let j = 0; j < blockSize; j++) {
1176
+ sum = sum + Math.abs(rawData[blockStart + j]) // find the sum of all the samples in the block
1177
+ }
1178
+ filteredData.push(sum / blockSize) // divide the sum by the block size to get the average
1179
+ }
1180
+
1181
+ // This guarantees that the largest data point will be set to 1, and the rest of the data will scale proportionally.
1182
+ const multiplier = Math.pow(Math.max(...filteredData), -1)
1183
+ const normalizedData = filteredData.map((n) => n * multiplier)
1184
+
1185
+ // Generate waveform like WhatsApp
1186
+ const waveform = new Uint8Array(normalizedData.map((n) => Math.floor(100 * n)))
1187
+ return waveform
1188
+ }
1189
+ catch (e) {
1190
+ logger?.debug('Failed to generate waveform: ' + e)
1191
+ }
1192
+ }
1193
+
1194
+ const toReadable = (buffer) => {
1195
+ const readable = new Readable({ read: () => { } })
1196
+ readable.push(buffer)
1197
+ readable.push(null)
1198
+ return readable
1199
+ }
1200
+
1201
+ const toBuffer = async (stream) => {
1202
+ const chunks = []
1203
+ for await (const chunk of stream) {
1204
+ chunks.push(chunk)
1205
+ }
1206
+ stream.destroy()
1207
+ return Buffer.concat(chunks)
1208
+ }
1209
+
1210
+ const getStream = async (item, opts) => {
1211
+ if (Buffer.isBuffer(item)) {
1212
+ return { stream: toReadable(item), type: 'buffer' }
1213
+ }
1214
+
1215
+ if ('stream' in item) {
1216
+ return { stream: item.stream, type: 'readable' }
1217
+ }
1218
+
1219
+ const urlStr = item.url.toString()
1220
+
1221
+ if (urlStr.startsWith('data:')) {
1222
+ const buffer = Buffer.from(urlStr.split(',')[1], 'base64')
1223
+ return { stream: await toReadable(buffer), type: 'buffer' }
1224
+ }
1225
+
1226
+ if (urlStr.startsWith('http://') || urlStr.startsWith('https://')) {
1227
+ return { stream: await getHttpStream(item.url, opts), type: 'remote' }
1228
+ }
1229
+
1230
+ return { stream: createReadStream(item.url), type: 'file' }
1231
+ }
1232
+
1233
+ /** generates a thumbnail for a given media, if required */
1234
+ async function generateThumbnail(file, mediaType, options) {
1235
+ let thumbnail
1236
+ let originalImageDimensions
1237
+
1238
+ if (mediaType === 'image') {
1239
+ const { buffer, original } = await extractImageThumb(file)
1240
+
1241
+ thumbnail = buffer.toString('base64')
1242
+
1243
+ if (original.width && original.height) {
1244
+ originalImageDimensions = {
1245
+ width: original.width,
1246
+ height: original.height
1247
+ }
1248
+ }
1249
+ }
1250
+ else if (mediaType === 'video') {
1251
+ const imgFilename = join(getTmpFilesDirectory(), generateMessageID() + '.jpg')
1252
+ try {
1253
+ await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 })
1254
+ const buff = await promises.readFile(imgFilename)
1255
+
1256
+ thumbnail = buff.toString('base64')
1257
+
1258
+ await promises.unlink(imgFilename)
1259
+ }
1260
+ catch (err) {
1261
+ options.logger?.debug('could not generate video thumb: ' + err)
1262
+ }
1263
+ }
1264
+ return {
1265
+ thumbnail,
1266
+ originalImageDimensions
1267
+ }
1268
+ }
1269
+
1270
+ const getHttpStream = async (url, options = {}) => {
1271
+ const response = await fetch(url.toString(), {
1272
+ dispatcher: options.dispatcher,
1273
+ method: 'GET',
1274
+ headers: options.headers
1275
+ })
1276
+
1277
+ if (!response.ok) {
1278
+ throw new Boom(`Failed to fetch stream from ${url}`, { statusCode: response.status, data: { url } })
1279
+ }
1280
+
1281
+ return response.body instanceof Readable ? response.body : Readable.fromWeb(response.body)
1282
+ }
1283
+
1284
+ /*const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
1285
+ const { stream, type } = await getStream(media, opts)
1286
+ logger?.debug('fetched media stream')
1287
+
1288
+ const encFilePath = join(tmpdir(), mediaType + generateMessageID() + '-plain')
1289
+ const encFileWriteStream = createWriteStream(encFilePath)
1290
+
1291
+ let originalFilePath
1292
+ let originalFileStream
1293
+
1294
+ if (type === 'file') {
1295
+ originalFilePath = media.url.toString()
1296
+ } else if (saveOriginalFileIfRequired) {
1297
+ originalFilePath = join(tmpdir(), mediaType + generateMessageID() + '-original')
1298
+ originalFileStream = createWriteStream(originalFilePath)
1299
+ }
1300
+
1301
+ let fileLength = 0
1302
+ const sha256 = createHash('sha256')
1303
+
1304
+ try {
1305
+ for await (const data of stream) {
1306
+ fileLength += data.length
1307
+
1308
+ if (type === 'remote'
1309
+ && opts?.maxContentLength
1310
+ && fileLength + data.length > opts.maxContentLength) {
1311
+ throw new Boom(`content length exceeded when preparing "${type}"`, {
1312
+ data: { media, type }
1313
+ })
1314
+ }
1315
+
1316
+ sha256.update(data)
1317
+ encFileWriteStream.write(data)
1318
+
1319
+ if (originalFileStream && !originalFileStream.write(data)) {
1320
+ await once(originalFileStream, 'drain')
1321
+ }
1322
+ }
1323
+
1324
+ const fileSha256 = sha256.digest()
1325
+ encFileWriteStream.end()
1326
+ originalFileStream?.end?.call(originalFileStream)
1327
+ stream.destroy()
1328
+
1329
+ logger?.debug('prepared plain stream successfully')
1330
+
1331
+ return {
1332
+ mediaKey: undefined,
1333
+ originalFilePath,
1334
+ encFilePath,
1335
+ mac: undefined,
1336
+ fileEncSha256: undefined,
1337
+ fileSha256,
1338
+ fileLength
1339
+ }
1340
+ }
1341
+ catch (error) {
1342
+ encFileWriteStream.destroy()
1343
+ originalFileStream?.destroy?.call(originalFileStream)
1344
+ sha256.destroy()
1345
+ stream.destroy()
1346
+ try {
1347
+ await promises.unlink(encFilePath)
1348
+ if (originalFilePath && didSaveToTmpPath) {
1349
+ await promises.unlink(originalFilePath)
1350
+ }
1351
+ } catch (err) {
1352
+ logger?.error({ err }, 'failed deleting tmp files')
1353
+ }
1354
+ throw error
1355
+ }
1356
+ }*/
1357
+
1358
+ const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
1359
+ const { stream, type } = await getStream(media, opts)
1360
+
1361
+ logger?.debug('fetched media stream')
1362
+
1363
+ const mediaKey = randomBytes(32)
1364
+ const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType)
1365
+ const encFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '-enc')
1366
+ const encFileWriteStream = createWriteStream(encFilePath)
1367
+
1368
+ let originalFileStream;
1369
+ let originalFilePath
1370
+
1371
+ if (saveOriginalFileIfRequired) {
1372
+ originalFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '-original')
1373
+ originalFileStream = createWriteStream(originalFilePath)
1374
+ }
1375
+
1376
+ let fileLength = 0
1377
+
1378
+ const aes = createCipheriv('aes-256-cbc', cipherKey, iv)
1379
+ const hmac = createHmac('sha256', macKey).update(iv)
1380
+ const sha256Plain = createHash('sha256');
1381
+ const sha256Enc = createHash('sha256')
1382
+
1383
+ const onChunk = async (buff) => {
1384
+ sha256Enc.update(buff)
1385
+ hmac.update(buff)
1386
+
1387
+ // Handle backpressure: if write returns false, wait for drain
1388
+ if (!encFileWriteStream.write(buff)) {
1389
+ await once(encFileWriteStream, 'drain')
1390
+ }
1391
+ }
1392
+
1393
+ try {
1394
+ for await (const data of stream) {
1395
+ fileLength += data.length
1396
+
1397
+ if (type === 'remote' &&
1398
+ opts?.maxContentLength &&
1399
+ fileLength + data.length > opts.maxContentLength) {
1400
+ throw new Boom(`content length exceeded when encrypting "${type}"`, {
1401
+ data: { media, type }
1402
+ })
1403
+ }
1404
+
1405
+ if (originalFileStream) {
1406
+ if (!originalFileStream.write(data)) {
1407
+ await once(originalFileStream, 'drain')
1408
+ }
1409
+ }
1410
+
1411
+ sha256Plain.update(data)
1412
+
1413
+ await onChunk(aes.update(data))
1414
+ }
1415
+
1416
+ await onChunk(aes.final())
1417
+ const mac = hmac.digest().slice(0, 10)
1418
+
1419
+ sha256Enc.update(mac)
1420
+
1421
+ const fileSha256 = sha256Plain.digest()
1422
+ const fileEncSha256 = sha256Enc.digest()
1423
+
1424
+ encFileWriteStream.write(mac)
1425
+
1426
+ const encFinishPromise = once(encFileWriteStream, 'finish')
1427
+ const originalFinishPromise = originalFileStream ? once(originalFileStream, 'finish') : Promise.resolve()
1428
+
1429
+ encFileWriteStream.end()
1430
+ originalFileStream?.end?.()
1431
+ stream.destroy()
1432
+
1433
+ // Wait for write streams to fully flush to disk
1434
+ // This helps reduce memory pressure by allowing OS to release buffers
1435
+ await encFinishPromise
1436
+ await originalFinishPromise
1437
+
1438
+ logger?.debug('encrypted data successfully')
1439
+
1440
+ return {
1441
+ mediaKey,
1442
+ originalFilePath,
1443
+ encFilePath,
1444
+ mac,
1445
+ fileEncSha256,
1446
+ fileSha256,
1447
+ fileLength
1448
+ }
1449
+ }
1450
+ catch (error) {
1451
+ // destroy all streams with error
1452
+ encFileWriteStream.destroy()
1453
+ originalFileStream?.destroy?.()
1454
+ aes.destroy()
1455
+ hmac.destroy()
1456
+ sha256Plain.destroy()
1457
+ sha256Enc.destroy()
1458
+ stream.destroy()
1459
+
1460
+ try {
1461
+ await promises.unlink(encFilePath)
1462
+
1463
+ if (originalFilePath) {
1464
+ await promises.unlink(originalFilePath)
1465
+ }
1466
+ }
1467
+ catch (err) {
1468
+ logger?.error({ err }, 'failed deleting tmp files')
1469
+ }
1470
+ throw error
1471
+ }
1472
+ }
1473
+
1474
+ const DEF_HOST = 'mmg.whatsapp.net'
1475
+
1476
+ const AES_CHUNK_SIZE = 16
1477
+
1478
+ const toSmallestChunkSize = (num) => {
1479
+ return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE
1480
+ }
1481
+ const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`
1482
+
1483
+ const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
1484
+ const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/')
1485
+ const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath)
1486
+
1487
+ if (!downloadUrl) {
1488
+ throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 })
1489
+ }
1490
+
1491
+ const keys = await getMediaKeys(mediaKey, type)
1492
+ return downloadEncryptedContent(downloadUrl, keys, opts)
1493
+ }
1494
+
1495
+ /**
1496
+ * Decrypts and downloads an AES256-CBC encrypted file given the keys.
1497
+ * Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
1498
+ * */
1499
+ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
1500
+ let bytesFetched = 0
1501
+ let startChunk = 0
1502
+ let firstBlockIsIV = false
1503
+
1504
+ // if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
1505
+ if (startByte) {
1506
+ const chunk = toSmallestChunkSize(startByte || 0)
1507
+
1508
+ if (chunk) {
1509
+ startChunk = chunk - AES_CHUNK_SIZE
1510
+ bytesFetched = chunk
1511
+ firstBlockIsIV = true
1512
+ }
1513
+ }
1514
+
1515
+ const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined
1516
+ const headersInit = options?.headers ? options.headers : undefined
1517
+ const headers = {
1518
+ ...(headersInit
1519
+ ? Array.isArray(headersInit)
1520
+ ? Object.fromEntries(headersInit)
1521
+ : headersInit
1522
+ : {}),
1523
+ Origin: DEFAULT_ORIGIN
1524
+ }
1525
+
1526
+ if (startChunk || endChunk) {
1527
+ headers.Range = `bytes=${startChunk}-`
1528
+
1529
+ if (endChunk) {
1530
+ headers.Range += endChunk
1531
+ }
1532
+ }
1533
+
1534
+ // download the message
1535
+ const fetched = await getHttpStream(downloadUrl, {
1536
+ ...(options || {}),
1537
+ headers
1538
+ })
1539
+
1540
+ let remainingBytes = Buffer.from([])
1541
+ let aes
1542
+
1543
+ const pushBytes = (bytes, push) => {
1544
+ if (startByte || endByte) {
1545
+ const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0)
1546
+ const end = bytesFetched + bytes.length < endByte ? undefined : Math.max(endByte - bytesFetched, 0)
1547
+
1548
+ push(bytes.slice(start, end))
1549
+ bytesFetched += bytes.length
1550
+ }
1551
+ else {
1552
+ push(bytes)
1553
+ }
1554
+ }
1555
+
1556
+ const output = new Transform({
1557
+ transform(chunk, _, callback) {
1558
+ let data = Buffer.concat([remainingBytes, chunk])
1559
+
1560
+ const decryptLength = toSmallestChunkSize(data.length)
1561
+
1562
+ remainingBytes = data.slice(decryptLength)
1563
+ data = data.slice(0, decryptLength)
1564
+
1565
+ if (!aes) {
1566
+ let ivValue = iv
1567
+
1568
+ if (firstBlockIsIV) {
1569
+ ivValue = data.slice(0, AES_CHUNK_SIZE)
1570
+ data = data.slice(AES_CHUNK_SIZE)
1571
+ }
1572
+
1573
+ aes = createDecipheriv('aes-256-cbc', cipherKey, ivValue)
1574
+
1575
+ // if an end byte that is not EOF is specified
1576
+ // stop auto padding (PKCS7) -- otherwise throws an error for decryption
1577
+ if (endByte) {
1578
+ aes.setAutoPadding(false)
1579
+ }
1580
+ }
1581
+ try {
1582
+ pushBytes(aes.update(data), b => this.push(b))
1583
+ callback()
1584
+ }
1585
+ catch (error) {
1586
+ callback(error)
1587
+ }
1588
+ },
1589
+ final(callback) {
1590
+ try {
1591
+ pushBytes(aes.final(), b => this.push(b))
1592
+ callback()
1593
+ }
1594
+ catch (error) {
1595
+ callback(error)
1596
+ }
1597
+ }
1598
+ })
1599
+
1600
+ return fetched.pipe(output, { end: true })
1601
+ }
1602
+
1603
+ function extensionForMediaMessage(message) {
1604
+ const getExtension = (mimetype) => mimetype.split('')[0].split('/')[1]
1605
+ const type = Object.keys(message)[0]
1606
+ let extension
1607
+ if (type === 'locationMessage' ||
1608
+ type === 'liveLocationMessage' ||
1609
+ type === 'productMessage') {
1610
+ extension = '.jpeg'
1611
+ }
1612
+ else {
1613
+ const messageContent = message[type]
1614
+ extension = getExtension(messageContent.mimetype)
1615
+ }
1616
+ return extension
1617
+ }
1618
+
1619
+ const isNodeRuntime = () => {
1620
+ return (typeof process !== 'undefined' &&
1621
+ process.versions?.node !== null &&
1622
+ typeof process.versions.bun === 'undefined' &&
1623
+ typeof globalThis.Deno === 'undefined')
1624
+ }
1625
+
1626
+ const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent }, redirectCount = 0) => {
1627
+ if (redirectCount > 5) {
1628
+ throw new Error('Too many redirects')
1629
+ }
1630
+
1631
+ const parsedUrl = new URL(url)
1632
+ const httpModule = parsedUrl.protocol === 'https:' ? require('https') : require('http')
1633
+
1634
+ // Get file size for Content-Length header (required for Node.js streaming)
1635
+ const fileStats = await promises.stat(filePath)
1636
+ const fileSize = fileStats.size
1637
+
1638
+ return new Promise((resolve, reject) => {
1639
+ const req = httpModule.request({
1640
+ hostname: parsedUrl.hostname,
1641
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
1642
+ path: parsedUrl.pathname + parsedUrl.search,
1643
+ method: 'POST',
1644
+ headers: {
1645
+ ...headers,
1646
+ 'Content-Length': fileSize
1647
+ },
1648
+ agent,
1649
+ timeout: timeoutMs
1650
+ }, res => {
1651
+ // Handle redirects (3xx)
1652
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
1653
+ res.resume() // Consume response to free resources
1654
+
1655
+ const newUrl = new URL(res.headers.location, url).toString()
1656
+
1657
+ resolve(uploadWithNodeHttp({
1658
+ url: newUrl,
1659
+ filePath,
1660
+ headers,
1661
+ timeoutMs,
1662
+ agent
1663
+ }, redirectCount + 1))
1664
+ return
1665
+ }
1666
+
1667
+ let body = ''
1668
+
1669
+ res.on('data', chunk => (body += chunk))
1670
+ res.on('end', () => {
1671
+ try {
1672
+ resolve(JSON.parse(body))
1673
+ }
1674
+ catch {
1675
+ resolve(undefined)
1676
+ }
1677
+ })
1678
+ })
1679
+
1680
+ req.on('error', reject)
1681
+ req.on('timeout', () => {
1682
+ req.destroy()
1683
+ reject(new Error('Upload timeout'))
1684
+ })
1685
+
1686
+ const stream = createReadStream(filePath)
1687
+
1688
+ stream.pipe(req)
1689
+ stream.on('error', err => {
1690
+ req.destroy()
1691
+ reject(err)
1692
+ })
1693
+ })
1694
+ }
1695
+
1696
+ const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {
1697
+ // Convert Node.js Readable to Web ReadableStream
1698
+ const nodeStream = createReadStream(filePath)
1699
+ const webStream = Readable.toWeb(nodeStream)
1700
+ const response = await fetch(url, {
1701
+ dispatcher: agent,
1702
+ method: 'POST',
1703
+ body: webStream,
1704
+ headers,
1705
+ duplex: 'half',
1706
+ signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
1707
+ })
1708
+
1709
+ try {
1710
+ return (await response.json())
1711
+ }
1712
+ catch {
1713
+ return undefined
1714
+ }
1715
+ }
1716
+
1717
+ /**
1718
+ * Uploads media to WhatsApp servers.
1719
+ *
1720
+ * ## Why we have two upload implementations:
1721
+ *
1722
+ * Node.js's native `fetch` (powered by undici) has a known bug where it buffers
1723
+ * the entire request body in memory before sending, even when using streams.
1724
+ * This causes memory issues with large files (e.g., 1GB file = 1GB+ memory usage).
1725
+ * See: https://github.com/nodejs/undici/issues/4058
1726
+ *
1727
+ * Other runtimes (Bun, Deno, browsers) correctly stream the request body without
1728
+ * buffering, so we can use the web-standard Fetch API there.
1729
+ *
1730
+ * ## Future considerations:
1731
+ * Once the undici bug is fixed, we can simplify this to use only the Fetch API
1732
+ * across all runtimes. Monitor the GitHub issue for updates.
1733
+ */
1734
+ const uploadMedia = async (params, logger) => {
1735
+ if (isNodeRuntime()) {
1736
+ logger?.debug('Using Node.js https module for upload (avoids undici buffering bug)')
1737
+ return uploadWithNodeHttp(params)
1738
+ }
1739
+ else {
1740
+ logger?.debug('Using web-standard Fetch API for upload');
1741
+ return uploadWithFetch(params)
1742
+ }
1743
+ }
1744
+
1745
+ const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
1746
+ return async (filePath, { mediaType, fileEncSha256B64, timeoutMs }) => {
1747
+ // send a query JSON to obtain the url & auth token to upload our media
1748
+ let uploadInfo = await refreshMediaConn(false)
1749
+ let urls
1750
+
1751
+ const hosts = [...customUploadHosts, ...uploadInfo.hosts]
1752
+
1753
+ fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
1754
+
1755
+ // Prepare common headers
1756
+ const customHeaders = (() => {
1757
+ const hdrs = options?.headers;
1758
+ if (!hdrs)
1759
+ return {};
1760
+ return Array.isArray(hdrs) ? Object.fromEntries(hdrs) : hdrs
1761
+ })()
1762
+
1763
+ const headers = {
1764
+ ...customHeaders,
1765
+ 'Content-Type': 'application/octet-stream',
1766
+ Origin: DEFAULT_ORIGIN
1767
+ }
1768
+
1769
+ for (const { hostname } of hosts) {
1770
+ logger.debug(`uploading to "${hostname}"`)
1771
+
1772
+ const auth = encodeURIComponent(uploadInfo.auth)
1773
+ const url = `https://${hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
1774
+
1775
+ let result
1776
+
1777
+ try {
1778
+ result = await uploadMedia({
1779
+ url,
1780
+ filePath,
1781
+ headers,
1782
+ timeoutMs,
1783
+ agent: fetchAgent
1784
+ }, logger);
1785
+ if (result?.url || result?.direct_path) {
1786
+ urls = {
1787
+ mediaUrl: result.url,
1788
+ directPath: result.direct_path,
1789
+ meta_hmac: result.meta_hmac,
1790
+ fbid: result.fbid,
1791
+ ts: result.ts
1792
+ }
1793
+ break
1794
+ }
1795
+ else {
1796
+ uploadInfo = await refreshMediaConn(true)
1797
+ throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
1798
+ }
1799
+ }
1800
+ catch (error) {
1801
+ const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname
1802
+ logger.warn({ trace: error?.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`)
1803
+ }
1804
+ }
1805
+
1806
+ if (!urls) {
1807
+ throw new Boom('Media upload failed on all hosts', { statusCode: 500 })
1808
+ }
1809
+
1810
+ return urls
1811
+ }
1812
+ }
1813
+
1814
+ const getMediaRetryKey = (mediaKey) => {
1815
+ return hkdf(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' })
1816
+ }
1817
+ /**
1818
+ * Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
1819
+ */
1820
+ const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
1821
+ const recp = { stanzaId: key.id }
1822
+ const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
1823
+ const iv = randomBytes(12)
1824
+ const retryKey = await getMediaRetryKey(mediaKey)
1825
+ const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id))
1826
+ const req = {
1827
+ tag: 'receipt',
1828
+ attrs: {
1829
+ id: key.id,
1830
+ to: jidNormalizedUser(meId),
1831
+ type: 'server-error'
1832
+ },
1833
+ content: [
1834
+ // this encrypt node is actually pretty useless
1835
+ // the media is returned even without this node
1836
+ // keeping it here to maintain parity with WA Web
1837
+ {
1838
+ tag: 'encrypt',
1839
+ attrs: {},
1840
+ content: [
1841
+ { tag: 'enc_p', attrs: {}, content: ciphertext },
1842
+ { tag: 'enc_iv', attrs: {}, content: iv }
1843
+ ]
1844
+ },
1845
+ {
1846
+ tag: 'rmr',
1847
+ attrs: {
1848
+ jid: key.remoteJid,
1849
+ from_me: (!!key.fromMe).toString(),
1850
+ participant: key.participant || undefined
1851
+ }
1852
+ }
1853
+ ]
1854
+ }
1855
+ return req
1856
+ }
1857
+
1858
+ const decodeMediaRetryNode = (node) => {
1859
+ const rmrNode = getBinaryNodeChild(node, 'rmr')
1860
+ const event = {
1861
+ key: {
1862
+ id: node.attrs.id,
1863
+ remoteJid: rmrNode.attrs.jid,
1864
+ fromMe: rmrNode.attrs.from_me === 'true',
1865
+ participant: rmrNode.attrs.participant
1866
+ }
1867
+ }
1868
+ const errorNode = getBinaryNodeChild(node, 'error')
1869
+ if (errorNode) {
1870
+ const errorCode = +errorNode.attrs.code
1871
+ event.error = new Boom(`Failed to re-upload media (${errorCode})`, { data: errorNode.attrs, statusCode: getStatusCodeForMediaRetry(errorCode) })
1872
+ }
1873
+ else {
1874
+ const encryptedInfoNode = getBinaryNodeChild(node, 'encrypt')
1875
+ const ciphertext = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_p')
1876
+ const iv = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_iv')
1877
+ if (ciphertext && iv) {
1878
+ event.media = { ciphertext, iv }
1879
+ }
1880
+ else {
1881
+ event.error = new Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 })
1882
+ }
1883
+ }
1884
+ return event
1885
+ }
1886
+
1887
+ const decryptMediaRetryData = async ({ ciphertext, iv }, mediaKey, msgId) => {
1888
+ const retryKey = await getMediaRetryKey(mediaKey)
1889
+ const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId))
1890
+ return proto.MediaRetryNotification.decode(plaintext)
1891
+ }
1892
+
1893
+ const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code]
1894
+
1895
+ const MEDIA_RETRY_STATUS_MAP = {
1896
+ [proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
1897
+ [proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
1898
+ [proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
1899
+ [proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418,
1900
+ }
1901
+
1902
+ module.exports = {
1903
+ hkdfInfoKey,
1904
+ getMediaKeys,
1905
+ extractVideoThumb,
1906
+ extractImageThumb,
1907
+ encodeBase64EncodedStringForUpload,
1908
+ generateProfilePicture,
1909
+ mediaMessageSHA256B64,
1910
+ getAudioDuration,
1911
+ getAudioWaveform,
1912
+ toReadable,
1913
+ toBuffer,
1914
+ getStream,
1915
+ generateThumbnail,
1916
+ getHttpStream,
1917
+ //prepareStream,
1918
+ encryptedStream,
1919
+ getUrlFromDirectPath,
1920
+ downloadContentFromMessage,
1921
+ downloadEncryptedContent,
1922
+ extensionForMediaMessage,
1923
+ uploadWithNodeHttp,
1924
+ getRawMediaUploadData,
1925
+ getWAUploadToServer,
1926
+ getMediaRetryKey,
1927
+ encryptMediaRetryRequest,
1928
+ decodeMediaRetryNode,
1929
+ decryptMediaRetryData,
1930
+ getStatusCodeForMediaRetry,
1931
+ MEDIA_RETRY_STATUS_MAP
1932
+ }
1933
+ return module.exports;
1934
+ })();
1935
+ for (const key of Object.keys(__kageModule73)) {
1936
+ if (key !== '__esModule' && !Object.prototype.hasOwnProperty.call(exports,key)) Object.defineProperty(exports,key,{enumerable:true,configurable:true,get:()=>__kageModule73[key]});
1937
+ }
1938
+
1939
+ // Kage: funciones avanzadas.
1940
+ const __kageModule177 = (() => {
1941
+ const module = { exports: {} }; const exports = module.exports;
1942
+ "use strict";
1943
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
1944
+ if (k2 === undefined) k2 = k;
1945
+ var desc = Object.getOwnPropertyDescriptor(m, k);
1946
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1947
+ desc = { enumerable: true, get: function() { return m[k]; } };
1948
+ }
1949
+ Object.defineProperty(o, k2, desc);
1950
+ }) : (function(o, m, k, k2) {
1951
+ if (k2 === undefined) k2 = k;
1952
+ o[k2] = m[k];
1953
+ }));
1954
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
1955
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
1956
+ }) : function(o, v) {
1957
+ o["default"] = v;
1958
+ });
1959
+ var __importStar = (this && this.__importStar) || (function () {
1960
+ var ownKeys = function(o) {
1961
+ ownKeys = Object.getOwnPropertyNames || function (o) {
1962
+ var ar = [];
1963
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
1964
+ return ar;
1965
+ };
1966
+ return ownKeys(o);
1967
+ };
1968
+ return function (mod) {
1969
+ if (mod && mod.__esModule) return mod;
1970
+ var result = {};
1971
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
1972
+ __setModuleDefault(result, mod);
1973
+ return result;
1974
+ };
1975
+ })();
1976
+ Object.defineProperty(exports, "__esModule", { value: true });
1977
+ exports.getStatusCodeForMediaRetry = exports.decryptMediaRetryData = exports.decodeMediaRetryNode = exports.encryptMediaRetryRequest = exports.getWAUploadToServer = exports.uploadWithNodeHttp = exports.downloadEncryptedContent = exports.downloadContentFromMessage = exports.getUrlFromDirectPath = exports.DEF_MEDIA_HOST = exports.encryptedStream = exports.getHttpStream = exports.getStream = exports.toBuffer = exports.toReadable = exports.mediaMessageSHA256B64 = exports.generateProfilePicture = exports.encodeBase64EncodedStringForUpload = exports.extractImageThumb = exports.extractVideoThumb = exports.getRawMediaUploadData = exports.hkdfInfoKey = exports.getImageProcessingLibrary = void 0;
1978
+ exports.getMediaKeys = getMediaKeys;
1979
+ exports.getAudioData = getAudioData;
1980
+ exports.generateThumbnail = generateThumbnail;
1981
+ exports.extensionForMediaMessage = extensionForMediaMessage;
1982
+ const node_child_process_1 = require("node:child_process");
1983
+ const Crypto = __importStar(require("node:crypto"));
1984
+ const node_events_1 = require("node:events");
1985
+ const node_fs_1 = require("node:fs");
1986
+ const node_os_1 = require("node:os");
1987
+ const node_path_1 = require("node:path");
1988
+ const node_stream_1 = require("node:stream");
1989
+ const node_url_1 = require("node:url");
1990
+ const index_js_1 = require("../../WAProto/index.js");
1991
+ const index_js_2 = require("../Addon/index.js");
1992
+ const index_js_3 = require("../Defaults/index.js");
1993
+ const index_js_4 = require("../WABinary/index.js");
1994
+ const crypto_js_1 = require("./crypto.js");
1995
+ const generics_js_1 = require("./generics.js");
1996
+ const getTmpFilesDirectory = () => (0, node_os_1.tmpdir)();
1997
+ let imageProcessingLibrary;
1998
+ const getImageProcessingLibrary = async () => {
1999
+ if (imageProcessingLibrary) {
2000
+ return imageProcessingLibrary;
2001
+ }
2002
+ //@ts-ignore
2003
+ const [sharp, image, jimp] = await Promise.all([
2004
+ Promise.resolve().then(() => __importStar(require('sharp'))).catch(() => { }),
2005
+ Promise.resolve().then(() => __importStar(require('@napi-rs/image'))).catch(() => { }),
2006
+ Promise.resolve().then(() => __importStar(require('jimp'))).catch(() => { })
2007
+ ]);
2008
+ if (sharp) {
2009
+ imageProcessingLibrary = { sharp };
2010
+ }
2011
+ else if (image) {
2012
+ imageProcessingLibrary = { image };
2013
+ }
2014
+ else if (jimp) {
2015
+ imageProcessingLibrary = { jimp };
2016
+ }
2017
+ else {
2018
+ throw new index_js_2.HttpError('No image processing library available');
2019
+ }
2020
+ return imageProcessingLibrary;
2021
+ };
2022
+ exports.getImageProcessingLibrary = getImageProcessingLibrary;
2023
+ const hkdfInfoKey = (type) => {
2024
+ const hkdfInfo = index_js_3.MEDIA_HKDF_KEY_MAPPING[type];
2025
+ return `WhatsApp ${hkdfInfo} Keys`;
2026
+ };
2027
+ exports.hkdfInfoKey = hkdfInfoKey;
2028
+ const getRawMediaUploadData = async (media, mediaType, logger) => {
2029
+ const { stream } = await (0, exports.getStream)(media);
2030
+ logger?.debug('got stream for raw upload');
2031
+ const hasher = Crypto.createHash('sha256');
2032
+ const filePath = (0, node_path_1.join)((0, node_os_1.tmpdir)(), mediaType + (0, generics_js_1.generateMessageIDV2)());
2033
+ const fileWriteStream = (0, node_fs_1.createWriteStream)(filePath);
2034
+ let fileLength = 0;
2035
+ try {
2036
+ for await (const data of stream) {
2037
+ fileLength += data.length;
2038
+ hasher.update(data);
2039
+ if (!fileWriteStream.write(data)) {
2040
+ await (0, node_events_1.once)(fileWriteStream, 'drain');
2041
+ }
2042
+ }
2043
+ fileWriteStream.end();
2044
+ await (0, node_events_1.once)(fileWriteStream, 'finish');
2045
+ stream.destroy();
2046
+ const fileSha256 = hasher.digest();
2047
+ logger?.debug('hashed data for raw upload');
2048
+ return {
2049
+ filePath: filePath,
2050
+ fileSha256,
2051
+ fileLength
2052
+ };
2053
+ }
2054
+ catch (error) {
2055
+ fileWriteStream.destroy();
2056
+ stream.destroy();
2057
+ try {
2058
+ await node_fs_1.promises.unlink(filePath);
2059
+ }
2060
+ catch {
2061
+ //
2062
+ }
2063
+ throw error;
2064
+ }
2065
+ };
2066
+ exports.getRawMediaUploadData = getRawMediaUploadData;
2067
+ /** generates all the keys required to encrypt/decrypt & sign a media message */
2068
+ async function getMediaKeys(buffer, mediaType) {
2069
+ if (!buffer) {
2070
+ throw new index_js_2.HttpError('Cannot derive from empty media key');
2071
+ }
2072
+ if (typeof buffer === 'string') {
2073
+ buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64');
2074
+ }
2075
+ // expand using HKDF to 112 bytes, also pass in the relevant app info
2076
+ const expandedMediaKey = (0, crypto_js_1.hkdf)(buffer, 112, { info: (0, exports.hkdfInfoKey)(mediaType) });
2077
+ return {
2078
+ iv: expandedMediaKey.slice(0, 16),
2079
+ cipherKey: expandedMediaKey.slice(16, 48),
2080
+ macKey: expandedMediaKey.slice(48, 80)
2081
+ };
2082
+ }
2083
+ /** Extracts video thumb using FFMPEG */
2084
+ const extractVideoThumb = async (path, time, size) => {
2085
+ const ffmpeg = (0, node_child_process_1.spawn)('ffmpeg', [
2086
+ '-loglevel', 'error',
2087
+ '-ss', String(time),
2088
+ '-i', path,
2089
+ '-an', '-sn', '-dn',
2090
+ '-map_metadata', '-1',
2091
+ '-vf', `scale=${size.width}:-1`,
2092
+ '-frames:v', '1',
2093
+ '-c:v', 'mjpeg',
2094
+ '-f', 'image2pipe',
2095
+ 'pipe:1'
2096
+ ], {
2097
+ stdio: ['ignore', 'pipe', 'pipe']
2098
+ });
2099
+ let buffer = Buffer.alloc(0);
2100
+ const stderrChunks = [];
2101
+ ffmpeg.stdout.on('data', (chunk) => {
2102
+ buffer = Buffer.concat([buffer, chunk]);
2103
+ });
2104
+ ffmpeg.stderr.on('data', (chunk) => stderrChunks.push(chunk));
2105
+ const [code] = await (0, node_events_1.once)(ffmpeg, 'close');
2106
+ if (code !== 0) {
2107
+ throw new index_js_2.HttpError(`FFmpeg failed (code ${code}):\n` +
2108
+ Buffer.concat(stderrChunks).toString('utf8'));
2109
+ }
2110
+ return buffer;
2111
+ };
2112
+ exports.extractVideoThumb = extractVideoThumb;
2113
+ const extractImageThumb = async (bufferOrFilePath, width = 32) => {
2114
+ // TODO: Move entirely to sharp, removing jimp as it supports readable streams
2115
+ // This will have positive speed and performance impacts as well as minimizing RAM usage.
2116
+ if (bufferOrFilePath instanceof node_stream_1.Readable) {
2117
+ bufferOrFilePath = await (0, exports.toBuffer)(bufferOrFilePath);
2118
+ }
2119
+ const lib = await (0, exports.getImageProcessingLibrary)();
2120
+ if ('sharp' in lib && lib.sharp?.default) {
2121
+ const img = lib.sharp.default(bufferOrFilePath);
2122
+ const dimensions = await img.metadata();
2123
+ const buffer = await img.resize(width).jpeg({ quality: 50 }).toBuffer();
2124
+ return {
2125
+ buffer,
2126
+ original: {
2127
+ width: dimensions.width,
2128
+ height: dimensions.height
2129
+ }
2130
+ };
2131
+ }
2132
+ else if ('image' in lib && lib.image?.Transformer) {
2133
+ if (!Buffer.isBuffer(bufferOrFilePath)) {
2134
+ bufferOrFilePath = await node_fs_1.promises.readFile(bufferOrFilePath);
2135
+ }
2136
+ const img = new lib.image.Transformer(bufferOrFilePath);
2137
+ const dimensions = await img.metadata();
2138
+ const buffer = await img.resize(width, undefined, 0).jpeg(50);
2139
+ return {
2140
+ buffer,
2141
+ original: {
2142
+ width: dimensions.width,
2143
+ height: dimensions.height
2144
+ }
2145
+ };
2146
+ }
2147
+ else if ('jimp' in lib && lib.jimp?.Jimp) {
2148
+ const jimp = await lib.jimp.Jimp.read(bufferOrFilePath);
2149
+ const dimensions = {
2150
+ width: jimp.width,
2151
+ height: jimp.height
2152
+ };
2153
+ const buffer = await jimp
2154
+ .resize({ w: width, mode: lib.jimp.ResizeStrategy.BILINEAR })
2155
+ .getBuffer('image/jpeg', { quality: 50 });
2156
+ return {
2157
+ buffer,
2158
+ original: dimensions
2159
+ };
2160
+ }
2161
+ else {
2162
+ throw new index_js_2.HttpError('No image processing library available');
2163
+ }
2164
+ };
2165
+ exports.extractImageThumb = extractImageThumb;
2166
+ const encodeBase64EncodedStringForUpload = (b64) => encodeURIComponent(b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''));
2167
+ exports.encodeBase64EncodedStringForUpload = encodeBase64EncodedStringForUpload;
2168
+ const generateProfilePicture = async (mediaUpload, dimensions) => {
2169
+ let buffer;
2170
+ const { width: w = 720, height: h = 720 } = dimensions || {};
2171
+ if (Buffer.isBuffer(mediaUpload)) {
2172
+ buffer = mediaUpload;
2173
+ }
2174
+ else {
2175
+ // Use getStream to handle all WAMediaUpload types (Buffer, Stream, URL)
2176
+ const { stream } = await (0, exports.getStream)(mediaUpload);
2177
+ // Convert the resulting stream to a buffer
2178
+ buffer = await (0, exports.toBuffer)(stream);
2179
+ }
2180
+ const lib = await (0, exports.getImageProcessingLibrary)();
2181
+ let img;
2182
+ if ('sharp' in lib && lib.sharp?.default) {
2183
+ img = lib.sharp
2184
+ .default(buffer)
2185
+ .resize(w, h)
2186
+ .jpeg({
2187
+ quality: 80
2188
+ })
2189
+ .toBuffer();
2190
+ }
2191
+ else if ('image' in lib && lib.image?.Transformer) {
2192
+ img = new lib.image
2193
+ .Transformer(buffer)
2194
+ .resize(w, h, 0)
2195
+ .jpeg(80);
2196
+ }
2197
+ else if ('jimp' in lib && lib.jimp?.Jimp) {
2198
+ const jimp = await lib.jimp.Jimp.read(buffer);
2199
+ const min = Math.min(jimp.width, jimp.height);
2200
+ const cropped = jimp.crop({ x: 0, y: 0, w: min, h: min });
2201
+ img = cropped.resize({ w, h, mode: lib.jimp.ResizeStrategy.BILINEAR }).getBuffer('image/jpeg', { quality: 80 });
2202
+ }
2203
+ else {
2204
+ throw new index_js_2.HttpError('No image processing library available');
2205
+ }
2206
+ return {
2207
+ img: await img
2208
+ };
2209
+ };
2210
+ exports.generateProfilePicture = generateProfilePicture;
2211
+ /** gets the SHA256 of the given media message */
2212
+ const mediaMessageSHA256B64 = (message) => {
2213
+ const media = Object.values(message)[0];
2214
+ return media?.fileSha256 && Buffer.from(media.fileSha256).toString('base64');
2215
+ };
2216
+ exports.mediaMessageSHA256B64 = mediaMessageSHA256B64;
2217
+ async function getAudioData(buffer, logger) {
2218
+ try {
2219
+ const filePath = await (0, index_js_2.getFile)(buffer);
2220
+ const result = await (0, index_js_2.getAudioWaveform)(filePath);
2221
+ await node_fs_1.promises.unlink(filePath);
2222
+ return result;
2223
+ }
2224
+ catch (e) {
2225
+ logger?.debug('Failed to get audio data: ' + e);
2226
+ }
2227
+ }
2228
+ const toReadable = (buffer) => {
2229
+ const readable = new node_stream_1.Readable({ read: () => { } });
2230
+ readable.push(buffer);
2231
+ readable.push(null);
2232
+ return readable;
2233
+ };
2234
+ exports.toReadable = toReadable;
2235
+ const toBuffer = async (stream) => {
2236
+ const chunks = [];
2237
+ for await (const chunk of stream) {
2238
+ chunks.push(chunk);
2239
+ }
2240
+ stream.destroy();
2241
+ return Buffer.concat(chunks);
2242
+ };
2243
+ exports.toBuffer = toBuffer;
2244
+ const getStream = async (item, opts) => {
2245
+ if (Buffer.isBuffer(item)) {
2246
+ return { stream: (0, exports.toReadable)(item), type: 'buffer' };
2247
+ }
2248
+ if ('stream' in item) {
2249
+ return { stream: item.stream, type: 'readable' };
2250
+ }
2251
+ const urlStr = item.url.toString();
2252
+ if (urlStr.startsWith('data:')) {
2253
+ const buffer = Buffer.from(urlStr.split(',')[1], 'base64');
2254
+ return { stream: (0, exports.toReadable)(buffer), type: 'buffer' };
2255
+ }
2256
+ if (urlStr.startsWith('http://') || urlStr.startsWith('https://')) {
2257
+ return { stream: await (0, exports.getHttpStream)(item.url, opts), type: 'remote' };
2258
+ }
2259
+ return { stream: (0, node_fs_1.createReadStream)(item.url), type: 'file' };
2260
+ };
2261
+ exports.getStream = getStream;
2262
+ /** generates a thumbnail for a given media, if required */
2263
+ async function generateThumbnail(file, mediaType, options) {
2264
+ let thumbnail;
2265
+ let originalImageDimensions;
2266
+ if (mediaType === 'image') {
2267
+ const { buffer, original } = await (0, exports.extractImageThumb)(file);
2268
+ thumbnail = buffer.toString('base64');
2269
+ if (original.width && original.height) {
2270
+ originalImageDimensions = {
2271
+ width: original.width,
2272
+ height: original.height
2273
+ };
2274
+ }
2275
+ }
2276
+ else if (mediaType === 'video') {
2277
+ try {
2278
+ const buff = await (0, exports.extractVideoThumb)(file, '00:00:00', { width: 32, height: 32 });
2279
+ thumbnail = buff.toString('base64');
2280
+ }
2281
+ catch (err) {
2282
+ options.logger?.debug('could not generate video thumb: ' + err);
2283
+ }
2284
+ }
2285
+ return {
2286
+ thumbnail,
2287
+ originalImageDimensions
2288
+ };
2289
+ }
2290
+ const getHttpStream = async (url, options = {}) => {
2291
+ const response = await fetch(url.toString(), {
2292
+ dispatcher: options.dispatcher,
2293
+ method: 'GET',
2294
+ headers: options.headers
2295
+ });
2296
+ if (!response.ok) {
2297
+ throw new index_js_2.HttpError(`Failed to fetch stream from ${url}`, { statusCode: response.status, data: { url } });
2298
+ }
2299
+ // @ts-ignore Node18+ Readable.fromWeb exists
2300
+ return response.body instanceof node_stream_1.Readable ? response.body : node_stream_1.Readable.fromWeb(response.body);
2301
+ };
2302
+ exports.getHttpStream = getHttpStream;
2303
+ const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
2304
+ const { stream, type } = await (0, exports.getStream)(media, opts);
2305
+ logger?.debug('fetched media stream');
2306
+ const mediaKey = Crypto.randomBytes(32);
2307
+ const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType);
2308
+ const encFilePath = (0, node_path_1.join)(getTmpFilesDirectory(), mediaType + (0, generics_js_1.generateMessageIDV2)() + '-enc');
2309
+ const encFileWriteStream = (0, node_fs_1.createWriteStream)(encFilePath);
2310
+ let originalFileStream;
2311
+ let originalFilePath;
2312
+ if (saveOriginalFileIfRequired) {
2313
+ originalFilePath = (0, node_path_1.join)(getTmpFilesDirectory(), mediaType + (0, generics_js_1.generateMessageIDV2)() + '-original');
2314
+ originalFileStream = (0, node_fs_1.createWriteStream)(originalFilePath);
2315
+ }
2316
+ let fileLength = 0;
2317
+ const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv);
2318
+ const hmac = Crypto.createHmac('sha256', macKey).update(iv);
2319
+ const sha256Plain = Crypto.createHash('sha256');
2320
+ const sha256Enc = Crypto.createHash('sha256');
2321
+ const onChunk = async (buff) => {
2322
+ sha256Enc.update(buff);
2323
+ hmac.update(buff);
2324
+ // Handle backpressure: if write returns false, wait for drain
2325
+ if (!encFileWriteStream.write(buff)) {
2326
+ await (0, node_events_1.once)(encFileWriteStream, 'drain');
2327
+ }
2328
+ };
2329
+ try {
2330
+ for await (const data of stream) {
2331
+ fileLength += data.length;
2332
+ if (type === 'remote' &&
2333
+ opts?.maxContentLength &&
2334
+ fileLength + data.length > opts.maxContentLength) {
2335
+ throw new index_js_2.HttpError(`content length exceeded when encrypting "${type}"`, {
2336
+ data: { media, type }
2337
+ });
2338
+ }
2339
+ if (originalFileStream) {
2340
+ if (!originalFileStream.write(data)) {
2341
+ await (0, node_events_1.once)(originalFileStream, 'drain');
2342
+ }
2343
+ }
2344
+ sha256Plain.update(data);
2345
+ await onChunk(aes.update(data));
2346
+ }
2347
+ await onChunk(aes.final());
2348
+ const mac = hmac.digest().slice(0, 10);
2349
+ sha256Enc.update(mac);
2350
+ const fileSha256 = sha256Plain.digest();
2351
+ const fileEncSha256 = sha256Enc.digest();
2352
+ encFileWriteStream.write(mac);
2353
+ const encFinishPromise = (0, node_events_1.once)(encFileWriteStream, 'finish');
2354
+ const originalFinishPromise = originalFileStream ? (0, node_events_1.once)(originalFileStream, 'finish') : Promise.resolve();
2355
+ encFileWriteStream.end();
2356
+ originalFileStream?.end?.();
2357
+ stream.destroy();
2358
+ // Wait for write streams to fully flush to disk
2359
+ // This helps reduce memory pressure by allowing OS to release buffers
2360
+ await encFinishPromise;
2361
+ await originalFinishPromise;
2362
+ logger?.debug('encrypted data successfully');
2363
+ return {
2364
+ mediaKey,
2365
+ originalFilePath,
2366
+ encFilePath,
2367
+ mac,
2368
+ fileEncSha256,
2369
+ fileSha256,
2370
+ fileLength
2371
+ };
2372
+ }
2373
+ catch (error) {
2374
+ // destroy all streams with error
2375
+ encFileWriteStream.destroy();
2376
+ originalFileStream?.destroy?.();
2377
+ aes.destroy();
2378
+ hmac.destroy();
2379
+ sha256Plain.destroy();
2380
+ sha256Enc.destroy();
2381
+ stream.destroy();
2382
+ try {
2383
+ await node_fs_1.promises.unlink(encFilePath);
2384
+ if (originalFilePath) {
2385
+ await node_fs_1.promises.unlink(originalFilePath);
2386
+ }
2387
+ }
2388
+ catch (err) {
2389
+ logger?.error({ err }, 'failed deleting tmp files');
2390
+ }
2391
+ throw error;
2392
+ }
2393
+ };
2394
+ exports.encryptedStream = encryptedStream;
2395
+ exports.DEF_MEDIA_HOST = 'mmg.whatsapp.net';
2396
+ const AES_CHUNK_SIZE = 16;
2397
+ const toSmallestChunkSize = (num) => {
2398
+ return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE;
2399
+ };
2400
+ const getUrlFromDirectPath = (directPath, host = exports.DEF_MEDIA_HOST) => `https://${host}${directPath}`;
2401
+ exports.getUrlFromDirectPath = getUrlFromDirectPath;
2402
+ const extractHost = (url) => {
2403
+ if (!url)
2404
+ return undefined;
2405
+ try {
2406
+ return new node_url_1.URL(url).host;
2407
+ }
2408
+ catch {
2409
+ return undefined;
2410
+ }
2411
+ };
2412
+ const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
2413
+ // Fallback host: explicit opt > host parsed from `url` > DEF_MEDIA_HOST.
2414
+ // Lets us honor a non-default host carried by the proto without forcing callers to thread it through.
2415
+ const fallbackHost = opts.host ?? extractHost(url);
2416
+ const downloadUrl = directPath ? (0, exports.getUrlFromDirectPath)(directPath, fallbackHost) : url;
2417
+ if (!downloadUrl) {
2418
+ throw new index_js_2.HttpError('No valid media URL or directPath present in message', { statusCode: 400 });
2419
+ }
2420
+ const keys = await getMediaKeys(mediaKey, type);
2421
+ return (0, exports.downloadEncryptedContent)(downloadUrl, keys, opts);
2422
+ };
2423
+ exports.downloadContentFromMessage = downloadContentFromMessage;
2424
+ /**
2425
+ * Decrypts and downloads an AES256-CBC encrypted file given the keys.
2426
+ * Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
2427
+ * */
2428
+ const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
2429
+ let bytesFetched = 0;
2430
+ let startChunk = 0;
2431
+ let firstBlockIsIV = false;
2432
+ // if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
2433
+ if (startByte) {
2434
+ const chunk = toSmallestChunkSize(startByte || 0);
2435
+ if (chunk) {
2436
+ startChunk = chunk - AES_CHUNK_SIZE;
2437
+ bytesFetched = chunk;
2438
+ firstBlockIsIV = true;
2439
+ }
2440
+ }
2441
+ const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined;
2442
+ const headersInit = options?.headers ? options.headers : undefined;
2443
+ const headers = {
2444
+ ...(headersInit
2445
+ ? Array.isArray(headersInit)
2446
+ ? Object.fromEntries(headersInit)
2447
+ : headersInit
2448
+ : {}),
2449
+ Origin: index_js_3.DEFAULT_ORIGIN
2450
+ };
2451
+ if (startChunk || endChunk) {
2452
+ headers.Range = `bytes=${startChunk}-`;
2453
+ if (endChunk) {
2454
+ headers.Range += endChunk;
2455
+ }
2456
+ }
2457
+ // download the message
2458
+ const fetched = await (0, exports.getHttpStream)(downloadUrl, {
2459
+ ...(options || {}),
2460
+ headers
2461
+ });
2462
+ let remainingBytes = Buffer.from([]);
2463
+ let aes;
2464
+ const pushBytes = (bytes, push) => {
2465
+ if (startByte || endByte) {
2466
+ const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0);
2467
+ const end = bytesFetched + bytes.length < endByte ? undefined : Math.max(endByte - bytesFetched, 0);
2468
+ push(bytes.slice(start, end));
2469
+ bytesFetched += bytes.length;
2470
+ }
2471
+ else {
2472
+ push(bytes);
2473
+ }
2474
+ };
2475
+ const output = new node_stream_1.Transform({
2476
+ transform(chunk, _, callback) {
2477
+ let data = remainingBytes.length ? Buffer.concat([remainingBytes, chunk]) : chunk;
2478
+ const decryptLength = toSmallestChunkSize(data.length);
2479
+ remainingBytes = data.slice(decryptLength);
2480
+ data = data.slice(0, decryptLength);
2481
+ if (!aes) {
2482
+ let ivValue = iv;
2483
+ if (firstBlockIsIV) {
2484
+ ivValue = data.slice(0, AES_CHUNK_SIZE);
2485
+ data = data.slice(AES_CHUNK_SIZE);
2486
+ }
2487
+ aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue);
2488
+ // if an end byte that is not EOF is specified
2489
+ // stop auto padding (PKCS7) -- otherwise throws an error for decryption
2490
+ if (endByte) {
2491
+ aes.setAutoPadding(false);
2492
+ }
2493
+ }
2494
+ try {
2495
+ pushBytes(aes.update(data), b => this.push(b));
2496
+ callback();
2497
+ }
2498
+ catch (error) {
2499
+ callback(error);
2500
+ }
2501
+ },
2502
+ final(callback) {
2503
+ try {
2504
+ pushBytes(aes.final(), b => this.push(b));
2505
+ callback();
2506
+ }
2507
+ catch (error) {
2508
+ callback(error);
2509
+ }
2510
+ }
2511
+ });
2512
+ return fetched.pipe(output, { end: true });
2513
+ };
2514
+ exports.downloadEncryptedContent = downloadEncryptedContent;
2515
+ function extensionForMediaMessage(message) {
2516
+ const getExtension = (mimetype) => mimetype.split(';')[0]?.split('/')[1];
2517
+ const type = Object.keys(message)[0];
2518
+ let extension;
2519
+ if (type === 'locationMessage' || type === 'liveLocationMessage' || type === 'productMessage') {
2520
+ extension = '.jpeg';
2521
+ }
2522
+ else {
2523
+ const messageContent = message[type];
2524
+ extension = getExtension(messageContent.mimetype);
2525
+ }
2526
+ return extension;
2527
+ }
2528
+ const isNodeRuntime = () => {
2529
+ return (typeof process !== 'undefined' &&
2530
+ process.versions?.node !== null &&
2531
+ typeof process.versions.bun === 'undefined' &&
2532
+ typeof globalThis.Deno === 'undefined');
2533
+ };
2534
+ const uploadWithNodeHttp = async ({ url, filePath, headers, timeoutMs, agent }, redirectCount = 0) => {
2535
+ if (redirectCount > 5) {
2536
+ throw new Error('Too many redirects');
2537
+ }
2538
+ const parsedUrl = new node_url_1.URL(url);
2539
+ const httpModule = parsedUrl.protocol === 'https:' ? await Promise.resolve().then(() => __importStar(require('https'))) : await Promise.resolve().then(() => __importStar(require('http')));
2540
+ // Get file size for Content-Length header (required for Node.js streaming)
2541
+ const fileStats = await node_fs_1.promises.stat(filePath);
2542
+ const fileSize = fileStats.size;
2543
+ return new Promise((resolve, reject) => {
2544
+ const req = httpModule.request({
2545
+ hostname: parsedUrl.hostname,
2546
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
2547
+ path: parsedUrl.pathname + parsedUrl.search,
2548
+ method: 'POST',
2549
+ headers: {
2550
+ ...headers,
2551
+ 'Content-Length': fileSize
2552
+ },
2553
+ agent,
2554
+ timeout: timeoutMs
2555
+ }, res => {
2556
+ // Handle redirects (3xx)
2557
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
2558
+ res.resume(); // Consume response to free resources
2559
+ const newUrl = new node_url_1.URL(res.headers.location, url).toString();
2560
+ resolve((0, exports.uploadWithNodeHttp)({
2561
+ url: newUrl,
2562
+ filePath,
2563
+ headers,
2564
+ timeoutMs,
2565
+ agent
2566
+ }, redirectCount + 1));
2567
+ return;
2568
+ }
2569
+ let body = '';
2570
+ res.on('data', chunk => (body += chunk));
2571
+ res.on('end', () => {
2572
+ try {
2573
+ resolve(JSON.parse(body));
2574
+ }
2575
+ catch {
2576
+ resolve(undefined);
2577
+ }
2578
+ });
2579
+ });
2580
+ req.on('error', reject);
2581
+ req.on('timeout', () => {
2582
+ req.destroy();
2583
+ reject(new Error('Upload timeout'));
2584
+ });
2585
+ const stream = (0, node_fs_1.createReadStream)(filePath);
2586
+ stream.pipe(req);
2587
+ stream.on('error', err => {
2588
+ req.destroy();
2589
+ reject(err);
2590
+ });
2591
+ });
2592
+ };
2593
+ exports.uploadWithNodeHttp = uploadWithNodeHttp;
2594
+ const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) => {
2595
+ // Convert Node.js Readable to Web ReadableStream
2596
+ const nodeStream = (0, node_fs_1.createReadStream)(filePath);
2597
+ const webStream = node_stream_1.Readable.toWeb(nodeStream);
2598
+ // Native fetch only accepts Undici-style dispatchers, not generic https Agents.
2599
+ const dispatcher = typeof agent?.dispatch === 'function' ? agent : undefined;
2600
+ const response = await fetch(url, {
2601
+ ...(dispatcher ? { dispatcher } : {}),
2602
+ method: 'POST',
2603
+ body: webStream,
2604
+ headers,
2605
+ duplex: 'half',
2606
+ signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
2607
+ });
2608
+ try {
2609
+ return (await response.json());
2610
+ }
2611
+ catch {
2612
+ return undefined;
2613
+ }
2614
+ };
2615
+ /**
2616
+ * Uploads media to WhatsApp servers.
2617
+ *
2618
+ * ## Why we have two upload implementations:
2619
+ *
2620
+ * Node.js's native `fetch` (powered by undici) has a known bug where it buffers
2621
+ * the entire request body in memory before sending, even when using streams.
2622
+ * This causes memory issues with large files (e.g., 1GB file = 1GB+ memory usage).
2623
+ * See: https://github.com/nodejs/undici/issues/4058
2624
+ *
2625
+ * Other runtimes (Bun, Deno, browsers) correctly stream the request body without
2626
+ * buffering, so we can use the web-standard Fetch API there.
2627
+ *
2628
+ * ## Future considerations:
2629
+ * Once the undici bug is fixed, we can simplify this to use only the Fetch API
2630
+ * across all runtimes. Monitor the GitHub issue for updates.
2631
+ */
2632
+ const uploadMedia = async (params, logger) => {
2633
+ if (isNodeRuntime()) {
2634
+ logger?.debug('Using Node.js https module for upload (avoids undici buffering bug)');
2635
+ return (0, exports.uploadWithNodeHttp)(params);
2636
+ }
2637
+ else {
2638
+ logger?.debug('Using web-standard Fetch API for upload');
2639
+ return uploadWithFetch(params);
2640
+ }
2641
+ };
2642
+ const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
2643
+ return async (filePath, { mediaType, fileEncSha256B64, timeoutMs, newsletter }) => {
2644
+ // send a query JSON to obtain the url & auth token to upload our media
2645
+ let uploadInfo = await refreshMediaConn(false);
2646
+ let urls;
2647
+ const hosts = [...customUploadHosts, ...uploadInfo.hosts];
2648
+ fileEncSha256B64 = (0, exports.encodeBase64EncodedStringForUpload)(fileEncSha256B64);
2649
+ // Prepare common headers
2650
+ const customHeaders = (() => {
2651
+ const hdrs = options?.headers;
2652
+ if (!hdrs)
2653
+ return {};
2654
+ return Array.isArray(hdrs) ? Object.fromEntries(hdrs) : hdrs;
2655
+ })();
2656
+ const headers = {
2657
+ ...customHeaders,
2658
+ 'Content-Type': 'application/octet-stream',
2659
+ Origin: index_js_3.DEFAULT_ORIGIN
2660
+ };
2661
+ for (const { hostname } of hosts) {
2662
+ logger.debug(`uploading to "${hostname}"`);
2663
+ const auth = encodeURIComponent(uploadInfo.auth);
2664
+ // Kage: actualización de compatibilidad.
2665
+ const mediaPathMap = newsletter ? index_js_3.NEWSLETTER_MEDIA_PATH_MAP : index_js_3.MEDIA_PATH_MAP;
2666
+ // Kage: actualización de compatibilidad.
2667
+ const serverThumb = newsletter ? '&server_thumb_gen=1' : '';
2668
+ const url = `https://${hostname}${mediaPathMap[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}${serverThumb}`;
2669
+ let result;
2670
+ try {
2671
+ result = await uploadMedia({
2672
+ url,
2673
+ filePath,
2674
+ headers,
2675
+ timeoutMs,
2676
+ agent: fetchAgent
2677
+ }, logger);
2678
+ if (result?.url || result?.direct_path) {
2679
+ urls = {
2680
+ mediaUrl: result.url,
2681
+ directPath: result.direct_path,
2682
+ meta_hmac: result.meta_hmac,
2683
+ fbid: result.fbid,
2684
+ ts: result.ts,
2685
+ thumbnailDirectPath: result.thumbnail_info?.thumbnail_direct_path,
2686
+ thumbnailSha256: result.thumbnail_info?.thumbnail_sha256
2687
+ };
2688
+ break;
2689
+ }
2690
+ else {
2691
+ uploadInfo = await refreshMediaConn(true);
2692
+ throw new Error(`upload failed, reason: ${JSON.stringify(result)}`);
2693
+ }
2694
+ }
2695
+ catch (error) {
2696
+ const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname;
2697
+ logger.warn({ trace: error?.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`);
2698
+ }
2699
+ }
2700
+ if (!urls) {
2701
+ throw new index_js_2.HttpError('Media upload failed on all hosts', { statusCode: 500 });
2702
+ }
2703
+ return urls;
2704
+ };
2705
+ };
2706
+ exports.getWAUploadToServer = getWAUploadToServer;
2707
+ const getMediaRetryKey = (mediaKey) => {
2708
+ return (0, crypto_js_1.hkdf)(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' });
2709
+ };
2710
+ /**
2711
+ * Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
2712
+ */
2713
+ const encryptMediaRetryRequest = (key, mediaKey, meId) => {
2714
+ const recp = { stanzaId: key.id };
2715
+ const recpBuffer = index_js_1.proto.ServerErrorReceipt.encode(recp).finish();
2716
+ const iv = Crypto.randomBytes(12);
2717
+ const retryKey = getMediaRetryKey(mediaKey);
2718
+ const ciphertext = (0, crypto_js_1.aesEncryptGCM)(recpBuffer, retryKey, iv, Buffer.from(key.id));
2719
+ const req = {
2720
+ tag: 'receipt',
2721
+ attrs: {
2722
+ id: key.id,
2723
+ to: (0, index_js_4.jidNormalizedUser)(meId),
2724
+ type: 'server-error'
2725
+ },
2726
+ content: [
2727
+ // this encrypt node is actually pretty useless
2728
+ // the media is returned even without this node
2729
+ // keeping it here to maintain parity with WA Web
2730
+ {
2731
+ tag: 'encrypt',
2732
+ attrs: {},
2733
+ content: [
2734
+ { tag: 'enc_p', attrs: {}, content: ciphertext },
2735
+ { tag: 'enc_iv', attrs: {}, content: iv }
2736
+ ]
2737
+ },
2738
+ {
2739
+ tag: 'rmr',
2740
+ attrs: {
2741
+ jid: key.remoteJid,
2742
+ from_me: (!!key.fromMe).toString(),
2743
+ // @ts-ignore
2744
+ participant: key.participant || undefined
2745
+ }
2746
+ }
2747
+ ]
2748
+ };
2749
+ return req;
2750
+ };
2751
+ exports.encryptMediaRetryRequest = encryptMediaRetryRequest;
2752
+ const decodeMediaRetryNode = (node) => {
2753
+ const rmrNode = (0, index_js_4.getBinaryNodeChild)(node, 'rmr');
2754
+ const event = {
2755
+ key: {
2756
+ id: node.attrs.id,
2757
+ remoteJid: rmrNode.attrs.jid,
2758
+ fromMe: rmrNode.attrs.from_me === 'true',
2759
+ participant: rmrNode.attrs.participant
2760
+ }
2761
+ };
2762
+ const errorNode = (0, index_js_4.getBinaryNodeChild)(node, 'error');
2763
+ if (errorNode) {
2764
+ const errorCode = +errorNode.attrs.code;
2765
+ event.error = new index_js_2.HttpError(`Failed to re-upload media (${errorCode})`, {
2766
+ data: errorNode.attrs,
2767
+ statusCode: (0, exports.getStatusCodeForMediaRetry)(errorCode)
2768
+ });
2769
+ }
2770
+ else {
2771
+ const encryptedInfoNode = (0, index_js_4.getBinaryNodeChild)(node, 'encrypt');
2772
+ const ciphertext = (0, index_js_4.getBinaryNodeChildBuffer)(encryptedInfoNode, 'enc_p');
2773
+ const iv = (0, index_js_4.getBinaryNodeChildBuffer)(encryptedInfoNode, 'enc_iv');
2774
+ if (ciphertext && iv) {
2775
+ event.media = { ciphertext, iv };
2776
+ }
2777
+ else {
2778
+ event.error = new index_js_2.HttpError('Failed to re-upload media (missing ciphertext)', { statusCode: 404 });
2779
+ }
2780
+ }
2781
+ return event;
2782
+ };
2783
+ exports.decodeMediaRetryNode = decodeMediaRetryNode;
2784
+ const decryptMediaRetryData = ({ ciphertext, iv }, mediaKey, msgId) => {
2785
+ const retryKey = getMediaRetryKey(mediaKey);
2786
+ const plaintext = (0, crypto_js_1.aesDecryptGCM)(ciphertext, retryKey, iv, Buffer.from(msgId));
2787
+ return index_js_1.proto.MediaRetryNotification.decode(plaintext);
2788
+ };
2789
+ exports.decryptMediaRetryData = decryptMediaRetryData;
2790
+ const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code];
2791
+ exports.getStatusCodeForMediaRetry = getStatusCodeForMediaRetry;
2792
+ const MEDIA_RETRY_STATUS_MAP = {
2793
+ [index_js_1.proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
2794
+ [index_js_1.proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
2795
+ [index_js_1.proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
2796
+ [index_js_1.proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
2797
+ };
2798
+
2799
+ return module.exports;
2800
+ })();
2801
+ for (const key of Object.keys(__kageModule177)) {
2802
+ if (key !== '__esModule' && !Object.prototype.hasOwnProperty.call(exports,key)) Object.defineProperty(exports,key,{enumerable:true,configurable:true,get:()=>__kageModule177[key]});
2803
+ }
2804
+ Object.defineProperty(exports, Symbol.for('kage.advanced'), {value:__kageModule177, configurable:true});