@pontalabs/baileys 1.0.6 → 1.0.8

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