@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
@@ -0,0 +1,1094 @@
1
+ "use strict";
2
+ /**
3
+ * Waliwa - Main WASocket interface
4
+ * Entry point untuk WhatsApp bot - gabungin semua components (WebSocket, Auth, Messages, Groups, Calls)
5
+ *
6
+ * API Baileys-style dengan RAM optimization:
7
+ * - Lazy initialization
8
+ * - Buffer pooling
9
+ * - Debounced writes
10
+ * - Event-driven (no polling overhead)
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.WASocketImpl = void 0;
14
+ exports.makeWASocket = makeWASocket;
15
+ const ws_socket_js_1 = require("../socket/ws-socket.js");
16
+ const binary_js_1 = require("../core/binary.js");
17
+ const emitter_js_1 = require("../events/emitter.js");
18
+ const send_js_1 = require("../messages/send.js");
19
+ const media_js_1 = require("../messages/media.js");
20
+ const management_js_1 = require("../groups/management.js");
21
+ const handler_js_1 = require("../calls/handler.js");
22
+ const auth_state_js_1 = require("../auth/auth-state.js");
23
+ const qr_code_js_1 = require("../auth/qr-code.js");
24
+ const pairing_code_js_1 = require("../auth/pairing-code.js");
25
+ const jid_js_1 = require("../utils/jid.js");
26
+ const logger_js_1 = require("../utils/logger.js");
27
+ const retry_js_1 = require("../utils/retry.js");
28
+ // SKDM imports
29
+ const index_js_1 = require("../skdm/index.js");
30
+ // Fixes imports
31
+ const index_js_2 = require("../fixes/index.js");
32
+ // Features imports
33
+ const index_js_3 = require("../features/index.js");
34
+ // Recovery imports
35
+ const index_js_4 = require("../recovery/index.js");
36
+ // ============== WASocket Implementation ==============
37
+ class WASocketImpl {
38
+ ev;
39
+ authState;
40
+ user;
41
+ logger;
42
+ ws = null;
43
+ config;
44
+ messageSender;
45
+ messageReceiver;
46
+ mediaUploader;
47
+ mediaDownloader;
48
+ groupManager = null;
49
+ callHandler = null;
50
+ pendingTracker;
51
+ iqHandlers = new Map();
52
+ qrRefreshManager = null;
53
+ rateLimiter;
54
+ connectionState = 'close';
55
+ // SKDM
56
+ skdm = null;
57
+ // Fixes
58
+ heartbeat = null;
59
+ idempotencyCache;
60
+ groupEventCoalescer = null;
61
+ historySyncManager;
62
+ decryptionRetryQueue = null;
63
+ messageQueue;
64
+ chunkedUploader;
65
+ reconnector = null;
66
+ // Features
67
+ healthMonitor;
68
+ webhookManager;
69
+ circuitBreaker;
70
+ rateLimitTracker;
71
+ messagePersistence;
72
+ // Recovery (Multi-Branch SKDM Recovery)
73
+ recoveryManager;
74
+ // Auto-recovery timer
75
+ reconnectTimer = null;
76
+ reconnectAttempts = 0;
77
+ constructor(config = {}, authState) {
78
+ this.ev = new emitter_js_1.WAEventEmitter();
79
+ this.authState = authState;
80
+ this.logger = config.logger ?? logger_js_1.defaultLogger;
81
+ // Default config
82
+ this.config = {
83
+ authState,
84
+ logger: this.logger,
85
+ printQRInTerminal: config.printQRInTerminal ?? false,
86
+ mobile: config.mobile ?? false,
87
+ phoneNumber: config.phoneNumber,
88
+ browser: config.browser ?? ['Waliwa', 'WA', '1.0.0'],
89
+ reconnectInterval: config.reconnectInterval ?? 2000,
90
+ maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
91
+ syncFullHistory: config.syncFullHistory ?? false,
92
+ maxMsgRetryCount: config.maxMsgRetryCount ?? 5,
93
+ wsURL: config.wsURL ?? 'wss://web.whatsapp.com/ws/chat',
94
+ connectTimeoutMs: config.connectTimeoutMs ?? 20000,
95
+ keepAliveIntervalMs: config.keepAliveIntervalMs ?? 20000,
96
+ ramOptimizationLevel: config.ramOptimizationLevel ?? 2,
97
+ markOnlineOnConnect: config.markOnlineOnConnect ?? true,
98
+ fetchAgent: config.fetchAgent
99
+ };
100
+ this.pendingTracker = new send_js_1.PendingMessageTracker();
101
+ this.messageSender = new send_js_1.MessageSender({
102
+ send: (data) => this.send(data),
103
+ logger: this.logger,
104
+ tracker: this.pendingTracker
105
+ });
106
+ this.messageReceiver = new send_js_1.MessageReceiver({
107
+ logger: this.logger,
108
+ onMessage: (messages, type) => {
109
+ this.ev.emit('message.upsert', { messages, type });
110
+ },
111
+ onStatusUpdate: (key, status) => {
112
+ this.ev.emit('message.update', {
113
+ messages: [{ key, status }],
114
+ keys: [key]
115
+ });
116
+ }
117
+ });
118
+ this.mediaUploader = new media_js_1.MediaUploader({
119
+ logger: this.logger,
120
+ httpAgent: this.config.fetchAgent
121
+ });
122
+ this.mediaDownloader = new media_js_1.MediaDownloader({
123
+ logger: this.logger,
124
+ httpAgent: this.config.fetchAgent
125
+ });
126
+ // Rate limiter: 20 messages/sec
127
+ this.rateLimiter = new retry_js_1.RateLimiter(20);
128
+ // Initialize fixes (Baileys issues)
129
+ this.idempotencyCache = new index_js_2.IdempotencyCache(10000, 24 * 60 * 60 * 1000, this.logger);
130
+ this.historySyncManager = new index_js_2.HistorySyncManager(this.logger);
131
+ this.messageQueue = new index_js_2.MessageQueue(10, this.logger);
132
+ this.chunkedUploader = new index_js_2.ChunkedMediaUploader({ logger: this.logger });
133
+ // Initialize features
134
+ this.healthMonitor = new index_js_3.HealthMonitor({ logger: this.logger });
135
+ this.webhookManager = new index_js_3.WebhookManager({ logger: this.logger });
136
+ this.circuitBreaker = new index_js_3.CircuitBreaker({ logger: this.logger });
137
+ this.rateLimitTracker = new index_js_3.RateLimitTracker({ logger: this.logger });
138
+ this.messagePersistence = new index_js_3.MessagePersistence({ logger: this.logger });
139
+ // Initialize SKDM (Store Key Data Manager)
140
+ this.skdm = new index_js_1.SKDM(this.authState.creds, this.authState.keys, {
141
+ storageFolder: './auth',
142
+ logger: this.logger,
143
+ ...config
144
+ });
145
+ // Initialize Multi-Branch SKDM Recovery system
146
+ const recoveryConfig = config.recovery ?? {};
147
+ this.recoveryManager = new index_js_4.RecoveryManager({
148
+ enabled: recoveryConfig.enabled ?? true,
149
+ maxAttemptsPerBranch: recoveryConfig.maxAttemptsPerBranch ?? 3,
150
+ maxTotalAttempts: recoveryConfig.maxTotalAttempts ?? 20,
151
+ initialDelayMs: recoveryConfig.initialDelayMs ?? 1000,
152
+ maxDelayMs: recoveryConfig.maxDelayMs ?? 60000,
153
+ rateLimitedDelayMs: recoveryConfig.rateLimitedDelayMs ?? 300000,
154
+ logger: this.logger
155
+ });
156
+ // Bind recovery action handlers ke WASocket internals
157
+ this.setupRecoveryHandlers();
158
+ // Listen ke recovery events untuk forwarding ke main event emitter
159
+ this.recoveryManager.on('recovery:start', (info) => {
160
+ this.logger.info(`Recovery started: ${info.reason}`);
161
+ this.ev.emit('connection.update', {
162
+ connection: 'connecting',
163
+ lastDisconnect: { error: undefined, output: { code: info.code, reason: info.message } }
164
+ });
165
+ });
166
+ this.recoveryManager.on('recovery:success', (info) => {
167
+ this.logger.info(`Recovery succeeded: ${info.reason} (${info.totalAttempts} attempts)`);
168
+ this.healthMonitor.recordReconnect();
169
+ });
170
+ this.recoveryManager.on('recovery:failed', (info) => {
171
+ this.logger.error(`Recovery failed: ${info.reason} (fatal: ${info.fatal})`);
172
+ if (info.fatal) {
173
+ this.ev.emit('connection.update', {
174
+ connection: 'close',
175
+ lastDisconnect: {
176
+ error: new Error(`Fatal disconnect: ${info.reason}`),
177
+ output: { code: 0, reason: info.reason }
178
+ }
179
+ });
180
+ }
181
+ });
182
+ }
183
+ /**
184
+ * Setup recovery handlers - bind ke internal WASocket methods
185
+ */
186
+ setupRecoveryHandlers() {
187
+ // Connect action
188
+ this.recoveryManager.connect = async () => {
189
+ try {
190
+ if (this.ws) {
191
+ this.ws.close(4000, 'reconnect');
192
+ this.ws = null;
193
+ }
194
+ await this.connect();
195
+ return this.connectionState === 'open';
196
+ }
197
+ catch (err) {
198
+ this.logger.error(`Recovery connect failed: ${err.message}`);
199
+ return false;
200
+ }
201
+ };
202
+ // Stream reconnect (resume from stream state)
203
+ this.recoveryManager.streamReconnect = async () => {
204
+ try {
205
+ this.logger.info('Attempting stream reconnect (resume session)...');
206
+ // Reconnect tapi keep session state
207
+ if (this.ws) {
208
+ this.ws.close(4000, 'stream_reconnect');
209
+ this.ws = null;
210
+ }
211
+ // Quick reconnect dengan credentials yang sudah ada
212
+ await this.connect();
213
+ return this.connectionState === 'open';
214
+ }
215
+ catch (err) {
216
+ this.logger.error(`Stream reconnect failed: ${err.message}`);
217
+ return false;
218
+ }
219
+ };
220
+ // Key refresh via SKDM
221
+ this.recoveryManager.refreshKeys = async () => {
222
+ try {
223
+ this.logger.info('Refreshing Signal keys via SKDM...');
224
+ if (this.skdm) {
225
+ await this.skdm.preKeyManager.regeneratePreKeys();
226
+ this.skdm.markKeysDirty();
227
+ this.skdm.markCredsDirty();
228
+ await this.skdm.flush();
229
+ }
230
+ }
231
+ catch (err) {
232
+ this.logger.error(`Key refresh failed: ${err.message}`);
233
+ }
234
+ };
235
+ // Pre-key fetch
236
+ this.recoveryManager.fetchPreKeys = async () => {
237
+ try {
238
+ this.logger.info('Fetching pre-keys...');
239
+ if (this.skdm) {
240
+ await this.skdm.preKeyManager.regeneratePreKeys();
241
+ // Force upload ke server (will happen on next connect)
242
+ }
243
+ }
244
+ catch (err) {
245
+ this.logger.error(`Pre-key fetch failed: ${err.message}`);
246
+ }
247
+ };
248
+ // Full re-auth (new QR)
249
+ this.recoveryManager.startFullReauth = async () => {
250
+ try {
251
+ this.logger.info('Starting full re-authentication...');
252
+ // Clear existing creds untuk force new QR
253
+ if (this.skdm) {
254
+ // Keep noiseKey, tapi reset registration state
255
+ this.authState.creds.registered = false;
256
+ this.authState.creds.serverToken = undefined;
257
+ this.authState.creds.clientToken = undefined;
258
+ this.skdm.markCredsDirty();
259
+ await this.skdm.flush();
260
+ }
261
+ // Next connect akan trigger QR auth
262
+ }
263
+ catch (err) {
264
+ this.logger.error(`Full re-auth setup failed: ${err.message}`);
265
+ }
266
+ };
267
+ // Update app version
268
+ this.recoveryManager.updateAppVersion = async () => {
269
+ try {
270
+ this.logger.info('Updating app/browser version...');
271
+ // Bump version untuk bypass version-restricted endpoints
272
+ const versions = ['1.0.0', '1.1.0', '1.2.0', '2.0.0'];
273
+ const currentVersion = this.config.browser[2];
274
+ const currentIdx = versions.indexOf(currentVersion);
275
+ const newVersion = versions[Math.min(currentIdx + 1, versions.length - 1)] ?? '2.0.0';
276
+ this.config.browser = [this.config.browser[0], this.config.browser[1], newVersion];
277
+ this.logger.info(`App version updated to ${newVersion}`);
278
+ }
279
+ catch (err) {
280
+ this.logger.error(`Update app version failed: ${err.message}`);
281
+ }
282
+ };
283
+ }
284
+ // ============== Connection ==============
285
+ /**
286
+ * Start connection ke WhatsApp server
287
+ */
288
+ async connect() {
289
+ if (this.connectionState === 'open' || this.connectionState === 'connecting') {
290
+ return;
291
+ }
292
+ this.connectionState = 'connecting';
293
+ this.ev.emit('connection.update', { connection: 'connecting' });
294
+ try {
295
+ // Create WebSocket
296
+ this.ws = new ws_socket_js_1.WAWebSocket({
297
+ url: this.config.wsURL,
298
+ logger: this.logger,
299
+ browser: this.config.browser,
300
+ connectTimeoutMs: this.config.connectTimeoutMs,
301
+ keepAliveIntervalMs: this.config.keepAliveIntervalMs,
302
+ noiseKey: this.authState.creds.noiseKey,
303
+ ramLevel: this.config.ramOptimizationLevel
304
+ });
305
+ // Listen for messages
306
+ this.ws.on('message', (data) => this.handleIncomingData(data));
307
+ this.ws.on('close', (code, reason) => this.handleDisconnect(code, reason));
308
+ this.ws.on('error', (err) => {
309
+ this.logger.error(`Socket error: ${err.message}`);
310
+ this.ev.emit('connection.update', {
311
+ connection: 'close',
312
+ lastDisconnect: { error: err, output: { code: 0, reason: err.message } }
313
+ });
314
+ });
315
+ await this.ws.connect();
316
+ this.connectionState = 'open';
317
+ this.logger.info('WhatsApp connection established');
318
+ // Initialize group manager & call handler
319
+ this.groupManager = new management_js_1.GroupManager({
320
+ send: (data) => this.send(data),
321
+ logger: this.logger,
322
+ registerIqHandler: (id, handler) => {
323
+ this.iqHandlers.set(id, handler);
324
+ }
325
+ });
326
+ this.callHandler = new handler_js_1.CallHandler({
327
+ send: (data) => this.send(data),
328
+ logger: this.logger,
329
+ onCall: (event) => this.ev.emit('calls', event),
330
+ autoReject: false
331
+ });
332
+ // Start auth flow
333
+ if (!this.authState.creds.registered) {
334
+ if (this.config.mobile && this.config.phoneNumber) {
335
+ await this.requestPairingCode(this.config.phoneNumber);
336
+ }
337
+ else {
338
+ await this.startQRAuth();
339
+ }
340
+ }
341
+ else {
342
+ // Already registered - send login
343
+ await this.sendLogin();
344
+ }
345
+ // Initialize SKDM dan start background jobs
346
+ await this.skdm?.initialize();
347
+ // Start heartbeat (Baileys fix #4: connection hang)
348
+ this.heartbeat = new index_js_2.ConnectionHeartbeat({
349
+ sendPing: async () => {
350
+ // Send WA ping frame
351
+ const { encodeBinaryNode } = await import('../core/binary.js');
352
+ const pingNode = {
353
+ tag: 'iq',
354
+ attrs: { type: 'get', id: `ping-${Date.now()}` },
355
+ content: [{ tag: 'ping', attrs: {} }]
356
+ };
357
+ this.send(encodeBinaryNode(pingNode));
358
+ },
359
+ onDead: () => {
360
+ this.logger.error('Connection detected as dead, triggering reconnect');
361
+ this.handleDisconnect(4101, 'heartbeat dead');
362
+ },
363
+ logger: this.logger,
364
+ pingIntervalMs: this.config.keepAliveIntervalMs,
365
+ pongTimeoutMs: 10000,
366
+ maxMissedPongs: 3
367
+ });
368
+ this.heartbeat.start();
369
+ // Start group event coalescer (Baileys fix #7: group events missing)
370
+ this.groupEventCoalescer = new index_js_2.GroupEventCoalescer((events) => {
371
+ for (const event of events) {
372
+ this.ev.emit('group-participants.update', event);
373
+ }
374
+ }, 100, this.logger);
375
+ // Start health monitor
376
+ this.healthMonitor.start(60000);
377
+ this.healthMonitor.recordPing();
378
+ // Send pending messages dari persistence (recover after restart)
379
+ const pendingMessages = this.messagePersistence.getPending();
380
+ if (pendingMessages.length > 0) {
381
+ this.logger.info(`Recovering ${pendingMessages.length} pending messages from persistence`);
382
+ for (const msg of pendingMessages) {
383
+ // Re-queue untuk send
384
+ this.sendMessage(msg.recipientJid, msg.content).catch(err => {
385
+ this.logger.error(`Failed to recover message ${msg.id}: ${err.message}`);
386
+ });
387
+ }
388
+ }
389
+ this.ev.emit('connection.update', { connection: 'open' });
390
+ }
391
+ catch (err) {
392
+ this.connectionState = 'close';
393
+ this.logger.error(`Connect failed: ${err.message}`);
394
+ this.ev.emit('connection.update', {
395
+ connection: 'close',
396
+ lastDisconnect: { error: err, output: { code: 0, reason: err.message } }
397
+ });
398
+ throw err;
399
+ }
400
+ }
401
+ /**
402
+ * Handle incoming data dari WebSocket
403
+ */
404
+ handleIncomingData(data) {
405
+ try {
406
+ const node = (0, binary_js_1.decodeBinaryNode)(data);
407
+ if (!node)
408
+ return;
409
+ // Record activity for heartbeat (Baileys fix #4)
410
+ this.heartbeat?.receivedPong();
411
+ this.healthMonitor.recordPing();
412
+ this.healthMonitor.recordReceive();
413
+ // Trigger webhook untuk incoming node (Features: webhook system)
414
+ this.webhookManager.trigger('node.received', { tag: node.tag, attrs: node.attrs });
415
+ // Check if it's an IQ response
416
+ if (node.tag === 'iq' && node.attrs?.id) {
417
+ // Check if it's a pong response
418
+ if (node.attrs.id?.startsWith('ping-')) {
419
+ this.heartbeat?.receivedPong();
420
+ return;
421
+ }
422
+ const handler = this.iqHandlers.get(node.attrs.id);
423
+ if (handler) {
424
+ this.iqHandlers.delete(node.attrs.id);
425
+ handler(node);
426
+ return;
427
+ }
428
+ }
429
+ // Handle by tag
430
+ switch (node.tag) {
431
+ case 'message': {
432
+ // Baileys fix #6: duplicate messages - check idempotency
433
+ const messageId = node.attrs?.id;
434
+ if (messageId && this.idempotencyCache.isDuplicate(messageId)) {
435
+ this.logger.debug(`Skipping duplicate message: ${messageId}`);
436
+ return;
437
+ }
438
+ this.messageReceiver.handleNode(node);
439
+ break;
440
+ }
441
+ case 'receipt':
442
+ case 'status':
443
+ case 'ack':
444
+ this.messageReceiver.handleNode(node);
445
+ break;
446
+ case 'call':
447
+ this.callHandler?.handleCallNode(node);
448
+ break;
449
+ case 'chat':
450
+ this.handleChatNode(node);
451
+ break;
452
+ case 'presence':
453
+ this.handlePresenceNode(node);
454
+ break;
455
+ case 'notification':
456
+ this.handleNotificationNode(node);
457
+ break;
458
+ case 'success':
459
+ this.logger.info('Authentication successful');
460
+ if (node.attrs?.user) {
461
+ this.user = { id: node.attrs.user, name: node.attrs.pushname };
462
+ this.authState.creds.me = node.attrs.user;
463
+ this.authState.creds.registered = true;
464
+ this.skdm?.markCredsDirty();
465
+ }
466
+ this.ev.emit('creds.update', this.authState.creds);
467
+ this.webhookManager.trigger('auth.success', { user: this.user });
468
+ break;
469
+ case 'failure':
470
+ this.logger.error('Authentication failed');
471
+ this.ev.emit('connection.update', {
472
+ connection: 'close',
473
+ lastDisconnect: { error: new Error('Auth failed'), output: node.attrs }
474
+ });
475
+ this.webhookManager.trigger('auth.failure', { attrs: node.attrs });
476
+ break;
477
+ default:
478
+ this.logger.trace(`Unhandled top-level node: ${node.tag}`);
479
+ }
480
+ }
481
+ catch (err) {
482
+ this.logger.error(`Failed to process incoming data: ${err.message}`);
483
+ }
484
+ }
485
+ handleChatNode(node) {
486
+ const attrs = node.attrs || {};
487
+ // Chat presence: typing, recording
488
+ const from = attrs.from;
489
+ if (attrs.type === 'composing' || attrs.type === 'recording' || attrs.type === 'paused') {
490
+ this.ev.emit('presence.update', {
491
+ jid: from,
492
+ presence: attrs.type
493
+ });
494
+ }
495
+ }
496
+ handlePresenceNode(node) {
497
+ const attrs = node.attrs || {};
498
+ const from = attrs.from;
499
+ const type = attrs.type;
500
+ if (from && type) {
501
+ this.ev.emit('presence.update', {
502
+ jid: from,
503
+ presence: type,
504
+ t: attrs.t ? parseInt(attrs.t, 10) : undefined
505
+ });
506
+ }
507
+ }
508
+ handleNotificationNode(node) {
509
+ const attrs = node.attrs || {};
510
+ const type = attrs.type;
511
+ if (Array.isArray(node.content)) {
512
+ for (const child of node.content) {
513
+ if (typeof child !== 'object' || !child.tag)
514
+ continue;
515
+ switch (child.tag) {
516
+ case 'add':
517
+ case 'remove':
518
+ case 'promote':
519
+ case 'demote':
520
+ // Group participant update
521
+ const participants = Array.isArray(child.content)
522
+ ? child.content.filter(c => c?.tag === 'participant').map(c => c.attrs?.jid).filter(Boolean)
523
+ : [];
524
+ this.ev.emit('group-participants.update', {
525
+ id: attrs.from,
526
+ participants,
527
+ action: child.tag,
528
+ author: attrs.participant,
529
+ timestamp: Date.now()
530
+ });
531
+ break;
532
+ case 'subject':
533
+ // Group subject updated
534
+ this.ev.emit('groups.update', [{
535
+ id: attrs.from,
536
+ subject: child.attrs?.subject
537
+ }]);
538
+ break;
539
+ }
540
+ }
541
+ }
542
+ }
543
+ handleDisconnect(code, reason) {
544
+ this.connectionState = 'close';
545
+ this.pendingTracker.rejectAll(new Error(`Connection closed: ${code}`));
546
+ // Cleanup heartbeat
547
+ this.heartbeat?.stop();
548
+ this.heartbeat = null;
549
+ // Emit connection update
550
+ this.ev.emit('connection.update', {
551
+ connection: 'close',
552
+ lastDisconnect: {
553
+ error: undefined,
554
+ output: { code, reason }
555
+ }
556
+ });
557
+ // Skip recovery untuk intentional close (1000, 1001)
558
+ if (code === 1000 || code === 1001) {
559
+ this.logger.info('Intentional close, skipping recovery');
560
+ return;
561
+ }
562
+ // Skip jika recovery sudah in progress atau disabled
563
+ if (this.recoveryManager.isCurrentlyRecovering()) {
564
+ this.logger.warn('Recovery already in progress');
565
+ return;
566
+ }
567
+ // Map code ke DisconnectReason dan trigger multi-branch recovery
568
+ const disconnectReason = (0, index_js_4.mapCodeToReason)(code, reason);
569
+ this.logger.info(`Disconnect detected: ${disconnectReason} (code=${code}, reason=${reason})`);
570
+ // Trigger async recovery (non-blocking)
571
+ this.recoveryManager.recover(disconnectReason, code, reason).catch(err => {
572
+ this.logger.error(`Recovery manager error: ${err.message}`);
573
+ });
574
+ }
575
+ // ============== Auth ==============
576
+ /**
577
+ * Start QR-based authentication
578
+ */
579
+ async startQRAuth() {
580
+ const qrData = (0, qr_code_js_1.generateQRCodeData)({
581
+ creds: this.authState.creds,
582
+ logger: this.logger,
583
+ printInTerminal: this.config.printQRInTerminal
584
+ });
585
+ this.ev.emit('qr.update', qrData);
586
+ this.ev.emit('connection.update', {
587
+ connection: 'connecting',
588
+ qr: qrData.qrString
589
+ });
590
+ // Setup refresh manager
591
+ this.qrRefreshManager = new qr_code_js_1.QRRefreshManager((qr) => {
592
+ const newQr = (0, qr_code_js_1.generateQRCodeData)({
593
+ creds: this.authState.creds,
594
+ logger: this.logger,
595
+ printInTerminal: this.config.printQRInTerminal
596
+ });
597
+ this.ev.emit('qr.update', newQr);
598
+ this.ev.emit('connection.update', {
599
+ connection: 'connecting',
600
+ qr: newQr.qrString
601
+ });
602
+ }, () => {
603
+ this.logger.warn('QR code timed out');
604
+ this.ev.emit('connection.update', {
605
+ connection: 'close',
606
+ lastDisconnect: {
607
+ error: new Error('QR timeout'),
608
+ output: { code: 0, reason: 'QR timeout' }
609
+ }
610
+ });
611
+ });
612
+ this.qrRefreshManager.start(qrData);
613
+ }
614
+ /**
615
+ * Send login message untuk existing session
616
+ */
617
+ async sendLogin() {
618
+ // Build login node dengan stored credentials
619
+ const loginNode = {
620
+ tag: 'iq',
621
+ attrs: {
622
+ type: 'set',
623
+ to: 's.whatsapp.net',
624
+ id: `login-${Date.now()}`
625
+ },
626
+ content: [
627
+ {
628
+ tag: 'login',
629
+ attrs: {
630
+ user: this.authState.creds.me?.toString() ?? '',
631
+ serverToken: this.authState.creds.serverToken ?? '',
632
+ clientToken: this.authState.creds.clientToken ?? ''
633
+ }
634
+ }
635
+ ]
636
+ };
637
+ this.send((0, binary_js_1.encodeBinaryNode)(loginNode));
638
+ }
639
+ // ============== Internal Send ==============
640
+ async send(data) {
641
+ if (!this.ws || !this.ws.isConnected()) {
642
+ throw new Error('Cannot send: WebSocket not connected');
643
+ }
644
+ await this.rateLimiter.acquire();
645
+ this.ws.send(data);
646
+ }
647
+ // ============== Public API Methods ==============
648
+ async logout() {
649
+ if (this.authState instanceof auth_state_js_1.FileAuthState) {
650
+ await this.authState.clear();
651
+ }
652
+ await this.end(true);
653
+ }
654
+ async refreshQR() {
655
+ const qrData = (0, qr_code_js_1.generateQRCodeData)({
656
+ creds: this.authState.creds,
657
+ logger: this.logger,
658
+ printInTerminal: this.config.printQRInTerminal
659
+ });
660
+ this.ev.emit('qr.update', qrData);
661
+ return qrData;
662
+ }
663
+ async requestPairingCode(phoneNumber) {
664
+ const cleanPhone = (0, pairing_code_js_1.normalizePhoneNumber)(phoneNumber);
665
+ const result = (0, pairing_code_js_1.generatePairingCode)(cleanPhone, this.authState.creds, this.logger);
666
+ this.ev.emit('pairing-code.update', result);
667
+ return result;
668
+ }
669
+ async sendMessage(jid, content, opts = {}) {
670
+ const recipientJid = (0, jid_js_1.jidToString)(jid);
671
+ // Circuit breaker check (Features: circuit breaker)
672
+ if (!this.circuitBreaker.canSend(recipientJid)) {
673
+ const state = this.circuitBreaker.getState(recipientJid);
674
+ throw new Error(`Circuit breaker is ${state} for ${recipientJid} - too many failures`);
675
+ }
676
+ // Rate limit check (Features: rate limit tracker)
677
+ if (!this.rateLimitTracker.canSend(recipientJid)) {
678
+ const quota = this.rateLimitTracker.getRemainingQuota(recipientJid);
679
+ throw new Error(`Rate limit exceeded for ${recipientJid} (remaining: sec=${quota.second}, min=${quota.minute}, hr=${quota.hour})`);
680
+ }
681
+ // Persist message untuk survive restart (Features: message persistence)
682
+ const messageId = opts.messageId ?? `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
683
+ this.messagePersistence.save({
684
+ id: messageId,
685
+ recipientJid,
686
+ content,
687
+ status: 'pending',
688
+ attempts: 0,
689
+ createdAt: Date.now(),
690
+ updatedAt: Date.now()
691
+ });
692
+ // Use message queue untuk serialize sends per recipient (Baileys fix #8: race condition)
693
+ const startTime = Date.now();
694
+ try {
695
+ const result = await this.messageQueue.enqueue(recipientJid, () => this.messageSender.sendMessage(jid, content, { ...opts, messageId }));
696
+ // Update persistence
697
+ this.messagePersistence.update(messageId, {
698
+ status: 'sent',
699
+ attempts: 1
700
+ });
701
+ // Record success
702
+ this.circuitBreaker.recordSuccess(recipientJid);
703
+ this.healthMonitor.recordSend(Date.now() - startTime, true);
704
+ // Trigger webhook
705
+ this.webhookManager.trigger('message.sent', { jid: recipientJid, messageId });
706
+ return result;
707
+ }
708
+ catch (err) {
709
+ this.messagePersistence.update(messageId, {
710
+ status: 'failed',
711
+ lastError: err.message,
712
+ attempts: 1
713
+ });
714
+ this.circuitBreaker.recordFailure(recipientJid);
715
+ this.healthMonitor.recordSend(0, false);
716
+ // Trigger webhook untuk failure
717
+ this.webhookManager.trigger('message.failed', { jid: recipientJid, messageId, error: err.message });
718
+ throw err;
719
+ }
720
+ }
721
+ async forwardMessage(to, message) {
722
+ return this.messageSender.forwardMessage(to, message);
723
+ }
724
+ async deleteMessage(jid, key, opts = {}) {
725
+ return this.messageSender.deleteMessage(jid, key, opts);
726
+ }
727
+ async reactToMessage(jid, key, emoji) {
728
+ return this.messageSender.reactToMessage(jid, key, emoji);
729
+ }
730
+ async sendReadReceipt(jid, messageIds, participant) {
731
+ const node = (0, send_js_1.buildReadReceiptNode)(jid, messageIds, participant);
732
+ await this.send((0, binary_js_1.encodeBinaryNode)(node));
733
+ }
734
+ async sendPresenceUpdate(jid, presence) {
735
+ const node = (0, send_js_1.buildChatPresenceNode)(jid, presence);
736
+ await this.send((0, binary_js_1.encodeBinaryNode)(node));
737
+ }
738
+ async updatePresence(presence) {
739
+ const node = {
740
+ tag: 'presence',
741
+ attrs: { type: presence }
742
+ };
743
+ await this.send((0, binary_js_1.encodeBinaryNode)(node));
744
+ }
745
+ async uploadMedia(media, mediaType, mimetype, fileName) {
746
+ return this.mediaUploader.upload(media, mediaType, mimetype, fileName);
747
+ }
748
+ async downloadMedia(url, mediaKey, mediaType, mimetype, fileName) {
749
+ return this.mediaDownloader.download(url, mediaKey, mediaType, mimetype, fileName);
750
+ }
751
+ // ============== Group Methods ==============
752
+ async groupCreate(opts) {
753
+ if (!this.groupManager)
754
+ throw new Error('Group manager not initialized');
755
+ return this.groupManager.createGroup(opts);
756
+ }
757
+ async groupMetadata(jid) {
758
+ if (!this.groupManager)
759
+ throw new Error('Group manager not initialized');
760
+ return this.groupManager.getGroupMetadata(jid);
761
+ }
762
+ async groupUpdateSubject(jid, subject) {
763
+ if (!this.groupManager)
764
+ throw new Error('Group manager not initialized');
765
+ return this.groupManager.updateGroupSubject(jid, subject);
766
+ }
767
+ async groupUpdateDescription(jid, desc) {
768
+ if (!this.groupManager)
769
+ throw new Error('Group manager not initialized');
770
+ return this.groupManager.updateGroupDescription(jid, desc);
771
+ }
772
+ async groupParticipantsUpdate(jid, participants, action) {
773
+ if (!this.groupManager)
774
+ throw new Error('Group manager not initialized');
775
+ switch (action) {
776
+ case 'add':
777
+ return this.groupManager.addParticipants(jid, participants);
778
+ case 'remove':
779
+ return this.groupManager.removeParticipants(jid, participants);
780
+ case 'promote':
781
+ return this.groupManager.promoteParticipants(jid, participants);
782
+ case 'demote':
783
+ return this.groupManager.demoteParticipants(jid, participants);
784
+ }
785
+ }
786
+ async groupLeave(jid) {
787
+ if (!this.groupManager)
788
+ throw new Error('Group manager not initialized');
789
+ return this.groupManager.leaveGroup(jid);
790
+ }
791
+ async groupInviteCode(jid) {
792
+ if (!this.groupManager)
793
+ throw new Error('Group manager not initialized');
794
+ return this.groupManager.getInviteCode(jid);
795
+ }
796
+ async groupRevokeInvite(jid) {
797
+ if (!this.groupManager)
798
+ throw new Error('Group manager not initialized');
799
+ return this.groupManager.revokeInviteCode(jid);
800
+ }
801
+ async groupJoinViaCode(code) {
802
+ if (!this.groupManager)
803
+ throw new Error('Group manager not initialized');
804
+ return this.groupManager.joinGroupViaCode(code);
805
+ }
806
+ async groupSettingUpdate(jid, setting, value) {
807
+ if (!this.groupManager)
808
+ throw new Error('Group manager not initialized');
809
+ if (setting === 'restrict') {
810
+ return this.groupManager.setGroupRestrict(jid, value);
811
+ }
812
+ else {
813
+ return this.groupManager.setGroupAnnounce(jid, value);
814
+ }
815
+ }
816
+ // ============== Call Methods ==============
817
+ async rejectCall(callId, from) {
818
+ if (!this.callHandler)
819
+ throw new Error('Call handler not initialized');
820
+ return this.callHandler.rejectCall(callId, from);
821
+ }
822
+ // ============== Chat Methods ==============
823
+ async chatModify(modification, jid) {
824
+ const node = {
825
+ tag: 'iq',
826
+ attrs: {
827
+ type: 'set',
828
+ to: (0, jid_js_1.jidToString)(jid),
829
+ id: `chatmod-${Date.now()}`
830
+ },
831
+ content: [modification]
832
+ };
833
+ await this.send((0, binary_js_1.encodeBinaryNode)(node));
834
+ }
835
+ async readChat(jid) {
836
+ const node = {
837
+ tag: 'iq',
838
+ attrs: {
839
+ type: 'set',
840
+ to: (0, jid_js_1.jidToString)(jid),
841
+ id: `readchat-${Date.now()}`
842
+ },
843
+ content: [{
844
+ tag: 'markChatAsRead',
845
+ attrs: {}
846
+ }]
847
+ };
848
+ await this.send((0, binary_js_1.encodeBinaryNode)(node));
849
+ }
850
+ async archiveChat(jid, archive) {
851
+ const action = archive ? 'archive' : 'unarchive';
852
+ const node = {
853
+ tag: 'iq',
854
+ attrs: {
855
+ type: 'set',
856
+ to: (0, jid_js_1.jidToString)(jid),
857
+ id: `archive-${Date.now()}`
858
+ },
859
+ content: [{
860
+ tag: 'chat',
861
+ attrs: { action }
862
+ }]
863
+ };
864
+ await this.send((0, binary_js_1.encodeBinaryNode)(node));
865
+ }
866
+ async muteChat(jid, durationMs) {
867
+ const node = {
868
+ tag: 'iq',
869
+ attrs: {
870
+ type: 'set',
871
+ to: (0, jid_js_1.jidToString)(jid),
872
+ id: `mute-${Date.now()}`
873
+ },
874
+ content: [{
875
+ tag: 'mute',
876
+ attrs: {
877
+ action: 'mute',
878
+ muteDuration: Math.floor(durationMs / 1000).toString()
879
+ }
880
+ }]
881
+ };
882
+ await this.send((0, binary_js_1.encodeBinaryNode)(node));
883
+ }
884
+ async pinChat(jid, durationMs) {
885
+ const node = {
886
+ tag: 'iq',
887
+ attrs: {
888
+ type: 'set',
889
+ to: (0, jid_js_1.jidToString)(jid),
890
+ id: `pin-${Date.now()}`
891
+ },
892
+ content: [{
893
+ tag: 'pin',
894
+ attrs: {
895
+ type: 'pinV2',
896
+ pinDuration: Math.floor(durationMs / 1000).toString()
897
+ }
898
+ }]
899
+ };
900
+ await this.send((0, binary_js_1.encodeBinaryNode)(node));
901
+ }
902
+ async deleteChat(jid) {
903
+ const node = {
904
+ tag: 'iq',
905
+ attrs: {
906
+ type: 'set',
907
+ to: (0, jid_js_1.jidToString)(jid),
908
+ id: `delchat-${Date.now()}`
909
+ },
910
+ content: [{
911
+ tag: 'chat',
912
+ attrs: { action: 'clear' }
913
+ }]
914
+ };
915
+ await this.send((0, binary_js_1.encodeBinaryNode)(node));
916
+ }
917
+ async presenceSubscribe(jids) {
918
+ const node = {
919
+ tag: 'iq',
920
+ attrs: {
921
+ type: 'set',
922
+ to: 's.whatsapp.net',
923
+ id: `presence-sub-${Date.now()}`
924
+ },
925
+ content: jids.map(j => ({
926
+ tag: 'user',
927
+ attrs: { jid: (0, jid_js_1.jidToString)(j) }
928
+ }))
929
+ };
930
+ await this.send((0, binary_js_1.encodeBinaryNode)(node));
931
+ }
932
+ // ============== Connection Management ==============
933
+ async end(newConnection = false) {
934
+ if (this.reconnectTimer) {
935
+ clearTimeout(this.reconnectTimer);
936
+ this.reconnectTimer = null;
937
+ }
938
+ if (this.qrRefreshManager) {
939
+ this.qrRefreshManager.stop();
940
+ this.qrRefreshManager = null;
941
+ }
942
+ // Cancel ongoing recovery
943
+ this.recoveryManager?.cancel();
944
+ // Cleanup fixes
945
+ this.heartbeat?.stop();
946
+ this.heartbeat = null;
947
+ this.idempotencyCache.destroy();
948
+ this.groupEventCoalescer?.destroy();
949
+ this.groupEventCoalescer = null;
950
+ this.decryptionRetryQueue?.destroy();
951
+ this.decryptionRetryQueue = null;
952
+ this.messageQueue.clearAll();
953
+ // Cleanup features
954
+ this.healthMonitor.stop();
955
+ this.webhookManager.destroy();
956
+ // Cleanup SKDM
957
+ await this.skdm?.destroy();
958
+ this.pendingTracker.rejectAll(new Error('Connection ended'));
959
+ this.rateLimiter.destroy();
960
+ if (this.ws) {
961
+ this.ws.close(1000, 'normal');
962
+ this.ws = null;
963
+ }
964
+ this.connectionState = 'close';
965
+ this.ev.emit('connection.update', { connection: 'close' });
966
+ }
967
+ // ============== Public Accessors untuk SKDM & Features ==============
968
+ /**
969
+ * Get SKDM (Store Key Data Manager) instance
970
+ */
971
+ getSKDM() {
972
+ return this.skdm;
973
+ }
974
+ /**
975
+ * Get health status
976
+ */
977
+ getHealth() {
978
+ return this.healthMonitor.getHealth();
979
+ }
980
+ /**
981
+ * Get SKDM stats (pre-keys, sessions, identities)
982
+ */
983
+ getSKDMStats() {
984
+ return this.skdm?.getStats();
985
+ }
986
+ /**
987
+ * Get system stats (queue size, circuit breakers, rate limits)
988
+ */
989
+ getSystemStats() {
990
+ return {
991
+ messageQueue: this.messageQueue.getStats(),
992
+ circuitBreakers: this.circuitBreaker.getOpenCircuits(),
993
+ rateLimits: this.rateLimitTracker.getStats(),
994
+ webhooks: this.webhookManager.getStats(),
995
+ messagePersistence: {
996
+ total: this.messagePersistence.size(),
997
+ pending: this.messagePersistence.getPending().length,
998
+ failed: this.messagePersistence.getFailed().length
999
+ },
1000
+ idempotency: this.idempotencyCache.getStats(),
1001
+ historySync: this.historySyncManager.getStats(),
1002
+ health: this.healthMonitor.getHealth(),
1003
+ recovery: this.recoveryManager.getStats()
1004
+ };
1005
+ }
1006
+ /**
1007
+ * Get recovery stats - shows recovery success/failure rate per disconnect reason
1008
+ */
1009
+ getRecoveryStats() {
1010
+ return this.recoveryManager.getStats();
1011
+ }
1012
+ /**
1013
+ * Get current recovery context (jika lagi recovering)
1014
+ */
1015
+ getCurrentRecovery() {
1016
+ return this.recoveryManager.getCurrentContext();
1017
+ }
1018
+ /**
1019
+ * Check apakah lagi dalam proses recovery
1020
+ */
1021
+ isRecovering() {
1022
+ return this.recoveryManager.isCurrentlyRecovering();
1023
+ }
1024
+ /**
1025
+ * Manually trigger recovery untuk a reason (untuk testing)
1026
+ */
1027
+ async triggerRecovery(reason, code, message) {
1028
+ return this.recoveryManager.recover(reason, code, message);
1029
+ }
1030
+ /**
1031
+ * Register webhook
1032
+ */
1033
+ registerWebhook(config) {
1034
+ this.webhookManager.register(config);
1035
+ }
1036
+ /**
1037
+ * Retry failed messages dari persistence
1038
+ */
1039
+ async retryFailedMessages() {
1040
+ const failed = this.messagePersistence.getFailed();
1041
+ this.logger.info(`Retrying ${failed.length} failed messages`);
1042
+ for (const msg of failed) {
1043
+ this.messagePersistence.update(msg.id, { status: 'pending' });
1044
+ try {
1045
+ await this.sendMessage(msg.recipientJid, msg.content);
1046
+ }
1047
+ catch (err) {
1048
+ this.logger.error(`Retry failed for ${msg.id}: ${err.message}`);
1049
+ }
1050
+ }
1051
+ }
1052
+ /**
1053
+ * Reset circuit breaker untuk a recipient
1054
+ */
1055
+ resetCircuitBreaker(recipientJid) {
1056
+ this.circuitBreaker.reset(recipientJid);
1057
+ }
1058
+ getConnectionState() {
1059
+ return this.connectionState;
1060
+ }
1061
+ }
1062
+ exports.WASocketImpl = WASocketImpl;
1063
+ // ============== Factory Function ==============
1064
+ /**
1065
+ * Create WhatsApp socket - main entry point
1066
+ * Mirip Baileys' makeWASocket()
1067
+ *
1068
+ * @example
1069
+ * ```ts
1070
+ * import { makeWASocket, useFileAuthState } from 'waliwa';
1071
+ *
1072
+ * const { state } = await useFileAuthState('./auth');
1073
+ * const sock = makeWASocket({ authState: state, printQRInTerminal: true });
1074
+ * await sock.connect();
1075
+ *
1076
+ * sock.ev.on('message.upsert', async ({ messages }) => {
1077
+ * for (const msg of messages) {
1078
+ * console.log('Got message:', msg);
1079
+ * }
1080
+ * });
1081
+ * ```
1082
+ */
1083
+ function makeWASocket(config) {
1084
+ if (!config.authState) {
1085
+ throw new Error('authState is required in config');
1086
+ }
1087
+ const socket = new WASocketImpl(config, config.authState);
1088
+ // Auto-connect
1089
+ socket.connect().catch(err => {
1090
+ config.logger?.error?.(`Initial connect failed: ${err.message}`);
1091
+ });
1092
+ return socket;
1093
+ }
1094
+ //# sourceMappingURL=wa-socket.js.map