@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.
- package/LICENSE +21 -0
- package/README.md +600 -0
- package/dist/advanced/features.d.ts +211 -0
- package/dist/advanced/features.d.ts.map +1 -0
- package/dist/advanced/features.js +671 -0
- package/dist/advanced/features.js.map +1 -0
- package/dist/auth/auth-state.d.ts +89 -0
- package/dist/auth/auth-state.d.ts.map +1 -0
- package/dist/auth/auth-state.js +330 -0
- package/dist/auth/auth-state.js.map +1 -0
- package/dist/auth/pairing-code.d.ts +78 -0
- package/dist/auth/pairing-code.d.ts.map +1 -0
- package/dist/auth/pairing-code.js +191 -0
- package/dist/auth/pairing-code.js.map +1 -0
- package/dist/auth/qr-code.d.ts +60 -0
- package/dist/auth/qr-code.d.ts.map +1 -0
- package/dist/auth/qr-code.js +151 -0
- package/dist/auth/qr-code.js.map +1 -0
- package/dist/calls/handler.d.ts +61 -0
- package/dist/calls/handler.d.ts.map +1 -0
- package/dist/calls/handler.js +117 -0
- package/dist/calls/handler.js.map +1 -0
- package/dist/core/binary.d.ts +74 -0
- package/dist/core/binary.d.ts.map +1 -0
- package/dist/core/binary.js +448 -0
- package/dist/core/binary.js.map +1 -0
- package/dist/core/crypto.d.ts +60 -0
- package/dist/core/crypto.d.ts.map +1 -0
- package/dist/core/crypto.js +170 -0
- package/dist/core/crypto.js.map +1 -0
- package/dist/core/noise.d.ts +106 -0
- package/dist/core/noise.d.ts.map +1 -0
- package/dist/core/noise.js +307 -0
- package/dist/core/noise.js.map +1 -0
- package/dist/events/emitter.d.ts +44 -0
- package/dist/events/emitter.d.ts.map +1 -0
- package/dist/events/emitter.js +79 -0
- package/dist/events/emitter.js.map +1 -0
- package/dist/features/index.d.ts +342 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +755 -0
- package/dist/features/index.js.map +1 -0
- package/dist/fixes/index.d.ts +333 -0
- package/dist/fixes/index.d.ts.map +1 -0
- package/dist/fixes/index.js +762 -0
- package/dist/fixes/index.js.map +1 -0
- package/dist/groups/management.d.ts +86 -0
- package/dist/groups/management.d.ts.map +1 -0
- package/dist/groups/management.js +443 -0
- package/dist/groups/management.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +236 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/media.d.ts +93 -0
- package/dist/messages/media.d.ts.map +1 -0
- package/dist/messages/media.js +252 -0
- package/dist/messages/media.js.map +1 -0
- package/dist/messages/message-encoder.d.ts +70 -0
- package/dist/messages/message-encoder.d.ts.map +1 -0
- package/dist/messages/message-encoder.js +453 -0
- package/dist/messages/message-encoder.js.map +1 -0
- package/dist/messages/send.d.ts +101 -0
- package/dist/messages/send.d.ts.map +1 -0
- package/dist/messages/send.js +409 -0
- package/dist/messages/send.js.map +1 -0
- package/dist/recovery/index.d.ts +125 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/index.js +584 -0
- package/dist/recovery/index.js.map +1 -0
- package/dist/skdm/index.d.ts +220 -0
- package/dist/skdm/index.d.ts.map +1 -0
- package/dist/skdm/index.js +600 -0
- package/dist/skdm/index.js.map +1 -0
- package/dist/socket/hybrid.d.ts +118 -0
- package/dist/socket/hybrid.d.ts.map +1 -0
- package/dist/socket/hybrid.js +352 -0
- package/dist/socket/hybrid.js.map +1 -0
- package/dist/socket/wa-socket.d.ts +300 -0
- package/dist/socket/wa-socket.d.ts.map +1 -0
- package/dist/socket/wa-socket.js +1094 -0
- package/dist/socket/wa-socket.js.map +1 -0
- package/dist/socket/ws-socket.d.ts +96 -0
- package/dist/socket/ws-socket.d.ts.map +1 -0
- package/dist/socket/ws-socket.js +302 -0
- package/dist/socket/ws-socket.js.map +1 -0
- package/dist/types/index.d.ts +444 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/jid.d.ts +46 -0
- package/dist/utils/jid.d.ts.map +1 -0
- package/dist/utils/jid.js +152 -0
- package/dist/utils/jid.js.map +1 -0
- package/dist/utils/logger.d.ts +45 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +79 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/retry.d.ts +43 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +175 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/API.md +745 -0
- package/docs/ARCHITECTURE.md +307 -0
- package/docs/BAILEYS_FIXES.md +360 -0
- package/docs/FEATURES.md +532 -0
- package/docs/PROTOCOL.md +489 -0
- package/docs/RECOVERY.md +409 -0
- package/docs/SKDM.md +233 -0
- package/examples/ai-bot/index.ts +479 -0
- package/examples/ai-bot/package.json +17 -0
- package/examples/echo-bot/index.ts +171 -0
- package/examples/echo-bot/package.json +18 -0
- package/examples/group-bot/index.ts +557 -0
- package/examples/group-bot/package.json +17 -0
- package/examples/rest-gateway/index.ts +499 -0
- package/examples/rest-gateway/package.json +19 -0
- package/package.json +75 -0
- package/src/advanced/features.ts +817 -0
- package/src/auth/auth-state.ts +342 -0
- package/src/auth/pairing-code.ts +246 -0
- package/src/auth/qr-code.ts +191 -0
- package/src/calls/handler.ts +153 -0
- package/src/core/binary.ts +464 -0
- package/src/core/crypto.ts +189 -0
- package/src/core/noise.ts +406 -0
- package/src/events/emitter.ts +88 -0
- package/src/features/index.ts +921 -0
- package/src/fixes/index.ts +882 -0
- package/src/groups/management.ts +497 -0
- package/src/index.ts +274 -0
- package/src/messages/media.ts +372 -0
- package/src/messages/message-encoder.ts +520 -0
- package/src/messages/send.ts +521 -0
- package/src/recovery/index.ts +704 -0
- package/src/skdm/index.ts +693 -0
- package/src/socket/hybrid.ts +414 -0
- package/src/socket/wa-socket.ts +1347 -0
- package/src/socket/ws-socket.ts +355 -0
- package/src/types/index.ts +457 -0
- package/src/utils/jid.ts +156 -0
- package/src/utils/logger.ts +84 -0
- package/src/utils/retry.ts +205 -0
- 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
|