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