@kangwifi-pro/waliwa 1.0.0

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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +600 -0
  3. package/dist/advanced/features.d.ts +211 -0
  4. package/dist/advanced/features.d.ts.map +1 -0
  5. package/dist/advanced/features.js +671 -0
  6. package/dist/advanced/features.js.map +1 -0
  7. package/dist/auth/auth-state.d.ts +89 -0
  8. package/dist/auth/auth-state.d.ts.map +1 -0
  9. package/dist/auth/auth-state.js +330 -0
  10. package/dist/auth/auth-state.js.map +1 -0
  11. package/dist/auth/pairing-code.d.ts +78 -0
  12. package/dist/auth/pairing-code.d.ts.map +1 -0
  13. package/dist/auth/pairing-code.js +191 -0
  14. package/dist/auth/pairing-code.js.map +1 -0
  15. package/dist/auth/qr-code.d.ts +60 -0
  16. package/dist/auth/qr-code.d.ts.map +1 -0
  17. package/dist/auth/qr-code.js +151 -0
  18. package/dist/auth/qr-code.js.map +1 -0
  19. package/dist/calls/handler.d.ts +61 -0
  20. package/dist/calls/handler.d.ts.map +1 -0
  21. package/dist/calls/handler.js +117 -0
  22. package/dist/calls/handler.js.map +1 -0
  23. package/dist/core/binary.d.ts +74 -0
  24. package/dist/core/binary.d.ts.map +1 -0
  25. package/dist/core/binary.js +448 -0
  26. package/dist/core/binary.js.map +1 -0
  27. package/dist/core/crypto.d.ts +60 -0
  28. package/dist/core/crypto.d.ts.map +1 -0
  29. package/dist/core/crypto.js +170 -0
  30. package/dist/core/crypto.js.map +1 -0
  31. package/dist/core/noise.d.ts +106 -0
  32. package/dist/core/noise.d.ts.map +1 -0
  33. package/dist/core/noise.js +307 -0
  34. package/dist/core/noise.js.map +1 -0
  35. package/dist/events/emitter.d.ts +44 -0
  36. package/dist/events/emitter.d.ts.map +1 -0
  37. package/dist/events/emitter.js +79 -0
  38. package/dist/events/emitter.js.map +1 -0
  39. package/dist/features/index.d.ts +342 -0
  40. package/dist/features/index.d.ts.map +1 -0
  41. package/dist/features/index.js +755 -0
  42. package/dist/features/index.js.map +1 -0
  43. package/dist/fixes/index.d.ts +333 -0
  44. package/dist/fixes/index.d.ts.map +1 -0
  45. package/dist/fixes/index.js +762 -0
  46. package/dist/fixes/index.js.map +1 -0
  47. package/dist/groups/management.d.ts +86 -0
  48. package/dist/groups/management.d.ts.map +1 -0
  49. package/dist/groups/management.js +443 -0
  50. package/dist/groups/management.js.map +1 -0
  51. package/dist/index.d.ts +65 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +236 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/messages/media.d.ts +93 -0
  56. package/dist/messages/media.d.ts.map +1 -0
  57. package/dist/messages/media.js +252 -0
  58. package/dist/messages/media.js.map +1 -0
  59. package/dist/messages/message-encoder.d.ts +70 -0
  60. package/dist/messages/message-encoder.d.ts.map +1 -0
  61. package/dist/messages/message-encoder.js +453 -0
  62. package/dist/messages/message-encoder.js.map +1 -0
  63. package/dist/messages/send.d.ts +101 -0
  64. package/dist/messages/send.d.ts.map +1 -0
  65. package/dist/messages/send.js +409 -0
  66. package/dist/messages/send.js.map +1 -0
  67. package/dist/recovery/index.d.ts +125 -0
  68. package/dist/recovery/index.d.ts.map +1 -0
  69. package/dist/recovery/index.js +584 -0
  70. package/dist/recovery/index.js.map +1 -0
  71. package/dist/skdm/index.d.ts +220 -0
  72. package/dist/skdm/index.d.ts.map +1 -0
  73. package/dist/skdm/index.js +600 -0
  74. package/dist/skdm/index.js.map +1 -0
  75. package/dist/socket/hybrid.d.ts +118 -0
  76. package/dist/socket/hybrid.d.ts.map +1 -0
  77. package/dist/socket/hybrid.js +352 -0
  78. package/dist/socket/hybrid.js.map +1 -0
  79. package/dist/socket/wa-socket.d.ts +300 -0
  80. package/dist/socket/wa-socket.d.ts.map +1 -0
  81. package/dist/socket/wa-socket.js +1094 -0
  82. package/dist/socket/wa-socket.js.map +1 -0
  83. package/dist/socket/ws-socket.d.ts +96 -0
  84. package/dist/socket/ws-socket.d.ts.map +1 -0
  85. package/dist/socket/ws-socket.js +302 -0
  86. package/dist/socket/ws-socket.js.map +1 -0
  87. package/dist/types/index.d.ts +444 -0
  88. package/dist/types/index.d.ts.map +1 -0
  89. package/dist/types/index.js +7 -0
  90. package/dist/types/index.js.map +1 -0
  91. package/dist/utils/jid.d.ts +46 -0
  92. package/dist/utils/jid.d.ts.map +1 -0
  93. package/dist/utils/jid.js +152 -0
  94. package/dist/utils/jid.js.map +1 -0
  95. package/dist/utils/logger.d.ts +45 -0
  96. package/dist/utils/logger.d.ts.map +1 -0
  97. package/dist/utils/logger.js +79 -0
  98. package/dist/utils/logger.js.map +1 -0
  99. package/dist/utils/retry.d.ts +43 -0
  100. package/dist/utils/retry.d.ts.map +1 -0
  101. package/dist/utils/retry.js +175 -0
  102. package/dist/utils/retry.js.map +1 -0
  103. package/docs/API.md +745 -0
  104. package/docs/ARCHITECTURE.md +307 -0
  105. package/docs/BAILEYS_FIXES.md +360 -0
  106. package/docs/FEATURES.md +532 -0
  107. package/docs/PROTOCOL.md +489 -0
  108. package/docs/RECOVERY.md +409 -0
  109. package/docs/SKDM.md +233 -0
  110. package/examples/ai-bot/index.ts +479 -0
  111. package/examples/ai-bot/package.json +17 -0
  112. package/examples/echo-bot/index.ts +171 -0
  113. package/examples/echo-bot/package.json +18 -0
  114. package/examples/group-bot/index.ts +557 -0
  115. package/examples/group-bot/package.json +17 -0
  116. package/examples/rest-gateway/index.ts +499 -0
  117. package/examples/rest-gateway/package.json +19 -0
  118. package/package.json +75 -0
  119. package/src/advanced/features.ts +817 -0
  120. package/src/auth/auth-state.ts +342 -0
  121. package/src/auth/pairing-code.ts +246 -0
  122. package/src/auth/qr-code.ts +191 -0
  123. package/src/calls/handler.ts +153 -0
  124. package/src/core/binary.ts +464 -0
  125. package/src/core/crypto.ts +189 -0
  126. package/src/core/noise.ts +406 -0
  127. package/src/events/emitter.ts +88 -0
  128. package/src/features/index.ts +921 -0
  129. package/src/fixes/index.ts +882 -0
  130. package/src/groups/management.ts +497 -0
  131. package/src/index.ts +274 -0
  132. package/src/messages/media.ts +372 -0
  133. package/src/messages/message-encoder.ts +520 -0
  134. package/src/messages/send.ts +521 -0
  135. package/src/recovery/index.ts +704 -0
  136. package/src/skdm/index.ts +693 -0
  137. package/src/socket/hybrid.ts +414 -0
  138. package/src/socket/wa-socket.ts +1347 -0
  139. package/src/socket/ws-socket.ts +355 -0
  140. package/src/types/index.ts +457 -0
  141. package/src/utils/jid.ts +156 -0
  142. package/src/utils/logger.ts +84 -0
  143. package/src/utils/retry.ts +205 -0
  144. package/tsconfig.json +30 -0
package/src/index.ts ADDED
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Waliwa - Lightweight WhatsApp library dengan hybrid protocol
3
+ *
4
+ * Lebih lengkap dari Baileys, lebih ringan dari ZapoJS, lebih hemat RAM.
5
+ * Menggunakan WhatsApp Web Multi-Device + Mobile API pairing.
6
+ *
7
+ * @example Basic usage
8
+ * ```ts
9
+ * import { makeWASocket, useFileAuthState } from 'waliwa';
10
+ *
11
+ * const { state, saveCreds } = await useFileAuthState('./auth');
12
+ * const sock = makeWASocket({ authState: state, printQRInTerminal: true });
13
+ *
14
+ * sock.ev.on('connection.update', ({ connection }) => {
15
+ * if (connection === 'open') console.log('Connected!');
16
+ * });
17
+ *
18
+ * sock.ev.on('message.upsert', async ({ messages }) => {
19
+ * for (const msg of messages) {
20
+ * if (!msg.key.fromMe && msg.message?.text) {
21
+ * await sock.sendMessage(msg.key.remoteJid, { text: 'Hello!' });
22
+ * }
23
+ * }
24
+ * });
25
+ * ```
26
+ */
27
+
28
+ // ============== Main Socket ==============
29
+ export { makeWASocket, WASocketImpl, WASocket } from './socket/wa-socket.js';
30
+ export type { WASocket as WASocketType } from './socket/wa-socket.js';
31
+
32
+ // ============== Hybrid Socket ==============
33
+ export { HybridSocketManager, MobileAPISocket } from './socket/hybrid.js';
34
+
35
+ // ============== Auth State ==============
36
+ export { FileAuthState, useFileAuthState, createDefaultCreds } from './auth/auth-state.js';
37
+
38
+ // ============== QR Code ==============
39
+ export {
40
+ generateQRCodeData,
41
+ buildQRString,
42
+ parseQRString,
43
+ QRRefreshManager
44
+ } from './auth/qr-code.js';
45
+
46
+ // ============== Pairing Code ==============
47
+ export {
48
+ generatePairingCode,
49
+ normalizePairingCode,
50
+ isValidPairingCodeFormat,
51
+ normalizePhoneNumber,
52
+ buildPairingRequestNode,
53
+ parsePairingResult,
54
+ PairingCodeExpiry
55
+ } from './auth/pairing-code.js';
56
+
57
+ // ============== Core Crypto ==============
58
+ export {
59
+ generateCurveKeyPair,
60
+ curveSharedSecret,
61
+ curveSign,
62
+ curveVerify,
63
+ sha256Hash,
64
+ sha512Hash,
65
+ sha1Hash,
66
+ hmacSha256,
67
+ hmacSha512,
68
+ hkdfDerive,
69
+ createAESCipher,
70
+ aesEncryptCBC,
71
+ aesDecryptCBC,
72
+ getRandomBytes,
73
+ concatBytes,
74
+ areBytesEqual,
75
+ constantTimeEqual,
76
+ type KeyPair,
77
+ type AESCipher
78
+ } from './core/crypto.js';
79
+
80
+ // ============== Noise Protocol ==============
81
+ export {
82
+ NoiseHandshake,
83
+ TrafficCipher,
84
+ buildClientPayload
85
+ } from './core/noise.js';
86
+
87
+ // ============== Binary Protocol ==============
88
+ export {
89
+ BinaryEncoder,
90
+ BinaryDecoder,
91
+ encodeBinaryNode,
92
+ decodeBinaryNode,
93
+ binaryNodeToString,
94
+ packNibbles,
95
+ unpackNibbles,
96
+ TAG_LIST,
97
+ type BinaryNode
98
+ } from './core/binary.js';
99
+
100
+ // ============== WebSocket Layer ==============
101
+ export { WAWebSocket } from './socket/ws-socket.js';
102
+ export type { WAWebSocketOptions, WAWebSocketEvent } from './socket/ws-socket.js';
103
+
104
+ // ============== Messages ==============
105
+ export {
106
+ MessageSender,
107
+ MessageReceiver,
108
+ PendingMessageTracker,
109
+ buildReadReceiptNode,
110
+ buildChatPresenceNode
111
+ } from './messages/send.js';
112
+ export {
113
+ encodeMessageNode,
114
+ decodeMessageNode,
115
+ generateMessageId,
116
+ generateStanzaId,
117
+ extractMessageText,
118
+ hasMedia,
119
+ getMediaInfo,
120
+ type ReceiptType,
121
+ type MessageStatusUpdateType
122
+ } from './messages/message-encoder.js';
123
+ export {
124
+ MediaUploader,
125
+ MediaDownloader,
126
+ encryptMedia,
127
+ decryptMedia,
128
+ deriveMediaKeys,
129
+ streamToBuffer,
130
+ bufferToStream,
131
+ type EncryptedMedia
132
+ } from './messages/media.js';
133
+
134
+ // ============== Groups ==============
135
+ export {
136
+ GroupManager,
137
+ buildInviteLink,
138
+ parseInviteLink
139
+ } from './groups/management.js';
140
+
141
+ // ============== Calls ==============
142
+ export { CallHandler } from './calls/handler.js';
143
+ export type { VoiceTranscriptionFn, VoiceTranscriptionResult } from './calls/handler.js';
144
+
145
+ // ============== Advanced Features ==============
146
+ export {
147
+ PollManager,
148
+ NewsletterManager,
149
+ StatusManager,
150
+ BusinessProfileManager,
151
+ CatalogManager,
152
+ LabelsManager,
153
+ type PollOptions,
154
+ type PollVote,
155
+ type NewsletterMetadata,
156
+ type StatusOptions,
157
+ type BusinessProfileOptions,
158
+ type BusinessHours,
159
+ type CatalogItem,
160
+ type Label
161
+ } from './advanced/features.js';
162
+
163
+ // ============== Event Emitter ==============
164
+ export { WAEventEmitter, StrictEventEmitter } from './events/emitter.js';
165
+
166
+ // ============== Utils ==============
167
+ export {
168
+ parseJid,
169
+ jidToString,
170
+ jidUser,
171
+ jidServer,
172
+ isGroupJid,
173
+ isUserJid,
174
+ isBroadcastJid,
175
+ isCallJid,
176
+ isStatusJid,
177
+ isNewsletter,
178
+ isLidJid,
179
+ isValidJid,
180
+ jidEqual,
181
+ jidToPhoneNumber,
182
+ phoneNumberToJid,
183
+ userJid,
184
+ groupJid,
185
+ broadcastJid
186
+ } from './utils/jid.js';
187
+
188
+ export {
189
+ DefaultLogger,
190
+ SilentLogger,
191
+ defaultLogger
192
+ } from './utils/logger.js';
193
+
194
+ export {
195
+ retry,
196
+ sleep,
197
+ RateLimiter,
198
+ PromiseQueue,
199
+ debounce,
200
+ throttle
201
+ } from './utils/retry.js';
202
+
203
+ // ============== SKDM (Store Key Data Manager) ==============
204
+ export {
205
+ SKDM,
206
+ PreKeyManager,
207
+ SessionManager,
208
+ IdentityKeyManager,
209
+ AppStateKeyManager,
210
+ LRUCache,
211
+ AtomicFileWriter,
212
+ type SKDMConfig,
213
+ type SKDMStats
214
+ } from './skdm/index.js';
215
+
216
+ // ============== Baileys Fixes ==============
217
+ export {
218
+ WeakEventEmitter,
219
+ ConnectionHeartbeat,
220
+ IdempotencyCache,
221
+ GroupEventCoalescer,
222
+ HistorySyncManager,
223
+ DecryptionRetryQueue,
224
+ MessageQueue,
225
+ ChunkedMediaUploader,
226
+ SmarterReconnector
227
+ } from './fixes/index.js';
228
+
229
+ // ============== New Features ==============
230
+ export {
231
+ HealthMonitor,
232
+ MultiAccountManager,
233
+ WebhookManager,
234
+ CircuitBreaker,
235
+ RateLimitTracker,
236
+ MessagePersistence,
237
+ type HealthStatus,
238
+ type AccountConfig,
239
+ type AccountStatus,
240
+ type WebhookConfig
241
+ } from './features/index.js';
242
+
243
+ // ============== Multi-Branch SKDM Recovery ==============
244
+ export {
245
+ RecoveryManager,
246
+ mapCodeToReason,
247
+ getActionDescription,
248
+ getReasonDescription,
249
+ DEFAULT_RECOVERY_BRANCHES,
250
+ DEFAULT_RECOVERY_CONFIG,
251
+ type DisconnectReason,
252
+ type RecoveryAction,
253
+ type RecoveryBranch,
254
+ type RecoveryContext,
255
+ type RecoveryConfig
256
+ } from './recovery/index.js';
257
+
258
+ // ============== All Types ==============
259
+ export * from './types/index.js';
260
+
261
+ // ============== Version ==============
262
+ export const VERSION = '1.0.0';
263
+ export const LIBRARY_NAME = 'waliwa';
264
+
265
+ // ============== Default Config ==============
266
+ export const DEFAULT_CONFIG = {
267
+ wsURL: 'wss://web.whatsapp.com/ws/chat',
268
+ browser: ['Waliwa', 'WA', '1.0.0'] as [string, string, string],
269
+ connectTimeoutMs: 20000,
270
+ keepAliveIntervalMs: 20000,
271
+ maxReconnectAttempts: 10,
272
+ reconnectInterval: 2000,
273
+ ramOptimizationLevel: 2 as 1 | 2 | 3
274
+ };
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Waliwa - Media upload/download
3
+ * Handle upload media ke WhatsApp CDN dan decrypt download
4
+ *
5
+ * Media encryption: AES-256-CBC + HMAC-SHA256, then upload ke WA CDN
6
+ * Download: reverse process - download encrypted blob, decrypt, return plain
7
+ */
8
+
9
+ import axios from 'axios';
10
+ import { Readable } from 'stream';
11
+ import {
12
+ sha256Hash,
13
+ hmacSha256,
14
+ aesEncryptCBC,
15
+ aesDecryptCBC,
16
+ hkdfDerive,
17
+ getRandomBytes,
18
+ concatBytes,
19
+ areBytesEqual
20
+ } from '../core/crypto.js';
21
+ import type {
22
+ MediaUploadResult,
23
+ MediaDownloadResult,
24
+ WALogger
25
+ } from '../types/index.js';
26
+ import { SilentLogger } from '../utils/logger.js';
27
+
28
+ // ============== Constants ==============
29
+
30
+ const WA_MEDIA_UPLOAD_URL = 'https://mmg.whatsapp.net/whatsapp/mmedia/mms';
31
+ const WA_MEDIA_DOWNLOAD_HOSTS = [
32
+ 'mmg.whatsapp.net',
33
+ 'mmg2.whatsapp.net',
34
+ 'mmg3.whatsapp.net'
35
+ ];
36
+
37
+ const MEDIA_KEY_TYPES: Record<string, number> = {
38
+ image: 1,
39
+ video: 2,
40
+ audio: 3,
41
+ document: 4,
42
+ sticker: 5,
43
+ 'thumbnail-image': 6,
44
+ 'thumbnail-video': 7,
45
+ 'thumbnail-document': 8,
46
+ 'md-msg-hist': 9,
47
+ 'app-state': 10,
48
+ 'link-thumbnail': 11,
49
+ 'media-msg': 12
50
+ };
51
+
52
+ const CHUNK_SIZE = 64 * 1024; // 64KB chunks untuk upload
53
+
54
+ // ============== Media Key Derivation ==============
55
+
56
+ /**
57
+ * Derive media encryption keys dari random mediaKey + mediaType
58
+ *
59
+ * Spec WA: HKDF-SHA256 dengan salt=length + mediaType-appended
60
+ * Output: 4 keys - iv, cipherKey, macKey, refKey
61
+ */
62
+ export function deriveMediaKeys(
63
+ mediaKey: Uint8Array,
64
+ mediaType: string
65
+ ): {
66
+ iv: Uint8Array;
67
+ cipherKey: Uint8Array;
68
+ macKey: Uint8Array;
69
+ refKey: Uint8Array;
70
+ } {
71
+ const mediaTypeNum = MEDIA_KEY_TYPES[mediaType] ?? 1;
72
+ const salt = new Uint8Array([mediaTypeNum]);
73
+ const info = new TextEncoder().encode(`WhatsApp ${mediaType} Keys`);
74
+
75
+ const derived = hkdfDerive(mediaKey, salt, info, 112); // 112 bytes total
76
+
77
+ return {
78
+ iv: derived.subarray(0, 16),
79
+ cipherKey: derived.subarray(16, 48),
80
+ macKey: derived.subarray(48, 80),
81
+ refKey: derived.subarray(80, 112)
82
+ };
83
+ }
84
+
85
+ // ============== Media Encryption ==============
86
+
87
+ export interface EncryptedMedia {
88
+ /** Encrypted media data */
89
+ ciphertext: Uint8Array;
90
+ /** MAC (HMAC-SHA256 dari encrypted data + IV) */
91
+ mac: Uint8Array;
92
+ /** Random mediaKey (32 bytes) */
93
+ mediaKey: Uint8Array;
94
+ /** SHA256 hash dari plaintext */
95
+ fileSha256: Uint8Array;
96
+ /** SHA256 hash dari encrypted data */
97
+ encFileSha256: Uint8Array;
98
+ /** Original file length */
99
+ fileLength: number;
100
+ }
101
+
102
+ /**
103
+ * Encrypt media untuk upload
104
+ * Spec: AES-256-CBC + HMAC-SHA256 untuk integrity
105
+ */
106
+ export async function encryptMedia(
107
+ plaintext: Buffer | Uint8Array,
108
+ mediaType: string
109
+ ): Promise<EncryptedMedia> {
110
+ // 1. Generate random mediaKey
111
+ const mediaKey = getRandomBytes(32);
112
+
113
+ // 2. Derive keys
114
+ const { iv, cipherKey, macKey } = deriveMediaKeys(mediaKey, mediaType);
115
+
116
+ // 3. Compute plaintext hash
117
+ const fileSha256 = sha256Hash(plaintext);
118
+
119
+ // 4. Encrypt dengan AES-256-CBC
120
+ const ciphertext = aesEncryptCBC(plaintext, cipherKey, iv);
121
+
122
+ // 5. Compute MAC: HMAC-SHA256(macKey, iv + ciphertext)
123
+ const macInput = concatBytes(iv, ciphertext);
124
+ const mac = hmacSha256(macKey, macInput);
125
+
126
+ // 6. Compute encrypted file hash
127
+ const encFileSha256 = sha256Hash(concatBytes(ciphertext, mac));
128
+
129
+ return {
130
+ ciphertext,
131
+ mac,
132
+ mediaKey,
133
+ fileSha256,
134
+ encFileSha256,
135
+ fileLength: plaintext.length
136
+ };
137
+ }
138
+
139
+ // ============== Media Decryption ==============
140
+
141
+ /**
142
+ * Decrypt media hasil download
143
+ */
144
+ export async function decryptMedia(
145
+ encryptedData: Uint8Array,
146
+ mediaKey: Uint8Array,
147
+ mediaType: string
148
+ ): Promise<Uint8Array> {
149
+ if (encryptedData.length < 16) {
150
+ throw new Error('Encrypted media too short');
151
+ }
152
+
153
+ const { iv, cipherKey, macKey } = deriveMediaKeys(mediaKey, mediaType);
154
+
155
+ // Last 10 bytes = MAC (untuk CBC mode WA spec)
156
+ // Note: WhatsApp uses 10-byte MAC instead of full 32
157
+ const mac = encryptedData.subarray(encryptedData.length - 10);
158
+ const ciphertext = encryptedData.subarray(0, encryptedData.length - 10);
159
+
160
+ // Verify MAC
161
+ const expectedMac = hmacSha256(macKey, concatBytes(iv, ciphertext));
162
+ const expectedMacShort = expectedMac.subarray(0, 10);
163
+ if (!areBytesEqual(mac, expectedMacShort)) {
164
+ throw new Error('Media MAC verification failed');
165
+ }
166
+
167
+ // Decrypt
168
+ const plaintext = aesDecryptCBC(ciphertext, cipherKey, iv);
169
+ return plaintext;
170
+ }
171
+
172
+ // ============== Media Uploader ==============
173
+
174
+ export interface MediaUploaderOptions {
175
+ logger?: WALogger;
176
+ /** Custom HTTP agent */
177
+ httpAgent?: any;
178
+ /** Timeout ms */
179
+ timeoutMs?: number;
180
+ }
181
+
182
+ export class MediaUploader {
183
+ private logger: WALogger;
184
+ private httpAgent?: any;
185
+ private timeoutMs: number;
186
+
187
+ constructor(opts: MediaUploaderOptions = {}) {
188
+ this.logger = opts.logger ?? new SilentLogger();
189
+ this.httpAgent = opts.httpAgent;
190
+ this.timeoutMs = opts.timeoutMs ?? 60000;
191
+ }
192
+
193
+ /**
194
+ * Upload media ke WhatsApp CDN
195
+ */
196
+ async upload(
197
+ media: Buffer | Uint8Array,
198
+ mediaType: string,
199
+ mimetype: string,
200
+ fileName?: string
201
+ ): Promise<MediaUploadResult> {
202
+ this.logger.debug(`Encrypting ${media.length} bytes of ${mediaType} (${mimetype})`);
203
+
204
+ // Encrypt
205
+ const encrypted = await encryptMedia(media, mediaType);
206
+
207
+ // Build upload URL
208
+ const uploadUrl = `${WA_MEDIA_UPLOAD_URL}/${mediaType}`;
209
+ const params: any = {
210
+ hash: Buffer.from(encrypted.encFileSha256).toString('base64'),
211
+ type: mimetype,
212
+ size: encrypted.fileLength,
213
+ name: fileName
214
+ };
215
+
216
+ this.logger.debug(`Uploading to ${uploadUrl}`);
217
+
218
+ // Upload via POST
219
+ const response = await axios.post(uploadUrl, Buffer.from(encrypted.ciphertext), {
220
+ params,
221
+ headers: {
222
+ 'Content-Type': 'application/octet-stream',
223
+ 'Origin': 'https://web.whatsapp.com',
224
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
225
+ },
226
+ timeout: this.timeoutMs,
227
+ maxContentLength: Infinity,
228
+ maxBodyLength: Infinity
229
+ });
230
+
231
+ if (response.status !== 200 && response.status !== 201) {
232
+ throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
233
+ }
234
+
235
+ const data = response.data;
236
+ if (!data || !data.url) {
237
+ throw new Error('Invalid upload response: missing URL');
238
+ }
239
+
240
+ this.logger.debug(`Upload successful: ${data.url}`);
241
+
242
+ return {
243
+ url: data.url,
244
+ directPath: data.direct_path || data.directPath || '',
245
+ mediaKey: encrypted.mediaKey,
246
+ encFileHash: encrypted.encFileSha256,
247
+ fileEncSHA256: encrypted.encFileSha256,
248
+ fileSHA256: encrypted.fileSha256,
249
+ fileLength: encrypted.fileLength,
250
+ mimetype
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Upload stream media (untuk large files)
256
+ */
257
+ async uploadStream(
258
+ stream: Readable,
259
+ mediaType: string,
260
+ mimetype: string,
261
+ fileLength: number
262
+ ): Promise<MediaUploadResult> {
263
+ // Collect stream ke buffer (untuk simplicity)
264
+ const chunks: Buffer[] = [];
265
+ for await (const chunk of stream) {
266
+ chunks.push(Buffer.from(chunk));
267
+ }
268
+ const buffer = Buffer.concat(chunks);
269
+ return this.upload(buffer, mediaType, mimetype);
270
+ }
271
+ }
272
+
273
+ // ============== Media Downloader ==============
274
+
275
+ export interface MediaDownloaderOptions {
276
+ logger?: WALogger;
277
+ httpAgent?: any;
278
+ timeoutMs?: number;
279
+ }
280
+
281
+ export class MediaDownloader {
282
+ private logger: WALogger;
283
+ private httpAgent?: any;
284
+ private timeoutMs: number;
285
+
286
+ constructor(opts: MediaDownloaderOptions = {}) {
287
+ this.logger = opts.logger ?? new SilentLogger();
288
+ this.httpAgent = opts.httpAgent;
289
+ this.timeoutMs = opts.timeoutMs ?? 60000;
290
+ }
291
+
292
+ /**
293
+ * Download media dari URL dan decrypt
294
+ */
295
+ async download(
296
+ url: string,
297
+ mediaKey: Uint8Array,
298
+ mediaType: string,
299
+ mimetype: string,
300
+ fileName?: string
301
+ ): Promise<MediaDownloadResult> {
302
+ this.logger.debug(`Downloading media from ${url}`);
303
+
304
+ // Determine host
305
+ let downloadUrl = url;
306
+ if (!url.startsWith('http')) {
307
+ // Relative path, prepend host
308
+ const host = WA_MEDIA_DOWNLOAD_HOSTS[0];
309
+ downloadUrl = `https://${host}${url.startsWith('/') ? '' : '/'}${url}`;
310
+ }
311
+
312
+ const response = await axios.get(downloadUrl, {
313
+ responseType: 'arraybuffer',
314
+ timeout: this.timeoutMs,
315
+ headers: {
316
+ 'Origin': 'https://web.whatsapp.com',
317
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
318
+ }
319
+ });
320
+
321
+ if (response.status !== 200) {
322
+ throw new Error(`Download failed: ${response.status}`);
323
+ }
324
+
325
+ const encryptedData = new Uint8Array(response.data);
326
+
327
+ // Decrypt
328
+ const plaintext = await decryptMedia(encryptedData, mediaKey, mediaType);
329
+
330
+ this.logger.debug(`Decrypted ${plaintext.length} bytes`);
331
+
332
+ return {
333
+ buffer: Buffer.from(plaintext),
334
+ mimetype,
335
+ fileLength: plaintext.length,
336
+ fileName
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Download dari direct path (alternative to URL)
342
+ */
343
+ async downloadFromDirectPath(
344
+ directPath: string,
345
+ mediaKey: Uint8Array,
346
+ mediaType: string,
347
+ mimetype: string,
348
+ fileName?: string
349
+ ): Promise<MediaDownloadResult> {
350
+ return this.download(directPath, mediaKey, mediaType, mimetype, fileName);
351
+ }
352
+ }
353
+
354
+ // ============== Helper ==============
355
+
356
+ /**
357
+ * Convert Readable stream ke Buffer
358
+ */
359
+ export async function streamToBuffer(stream: Readable): Promise<Buffer> {
360
+ const chunks: Buffer[] = [];
361
+ for await (const chunk of stream) {
362
+ chunks.push(Buffer.from(chunk));
363
+ }
364
+ return Buffer.concat(chunks);
365
+ }
366
+
367
+ /**
368
+ * Convert Buffer ke Readable stream
369
+ */
370
+ export function bufferToStream(buffer: Buffer): Readable {
371
+ return Readable.from(buffer);
372
+ }