@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,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waliwa - Mobile API socket layer
|
|
3
|
+
*
|
|
4
|
+
* Companion device pairing via phone number + code (no QR scan needed).
|
|
5
|
+
* Mengikuti WhatsApp Mobile API registration flow:
|
|
6
|
+
* 1. Request pairing code untuk phone number
|
|
7
|
+
* 2. User input code di WA mobile app
|
|
8
|
+
* 3. Server pair device, return identity keys
|
|
9
|
+
* 4. Switch ke WA Web protocol untuk ongoing communication
|
|
10
|
+
*
|
|
11
|
+
* Hybrid approach: pairing via Mobile API, communication via WA Web WS
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
import { WAWebSocket } from './ws-socket.js';
|
|
16
|
+
import { NoiseHandshake, TrafficCipher } from '../core/noise.js';
|
|
17
|
+
import { generateCurveKeyPair, getRandomBytes, hkdfDerive } from '../core/crypto.js';
|
|
18
|
+
import { encodeBinaryNode, decodeBinaryNode, BinaryNode } from '../core/binary.js';
|
|
19
|
+
import { generatePairingCode, parsePairingResult, PairingCodeExpiry } from '../auth/pairing-code.js';
|
|
20
|
+
import { SilentLogger } from '../utils/logger.js';
|
|
21
|
+
import type {
|
|
22
|
+
AuthenticationCreds,
|
|
23
|
+
PairingCodeResult,
|
|
24
|
+
WALogger,
|
|
25
|
+
WaliwaConfig,
|
|
26
|
+
ConnectionUpdate
|
|
27
|
+
} from '../types/index.js';
|
|
28
|
+
|
|
29
|
+
// ============== Mobile Socket ==============
|
|
30
|
+
|
|
31
|
+
export interface MobileSocketOptions {
|
|
32
|
+
phoneNumber: string;
|
|
33
|
+
creds: AuthenticationCreds;
|
|
34
|
+
logger?: WALogger;
|
|
35
|
+
/** Browser identification */
|
|
36
|
+
browser?: [string, string, string];
|
|
37
|
+
/** WA WebSocket URL setelah pairing */
|
|
38
|
+
waWsUrl?: string;
|
|
39
|
+
connectTimeoutMs?: number;
|
|
40
|
+
ramLevel?: 1 | 2 | 3;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Mobile API socket - handles companion device pairing via phone number
|
|
45
|
+
*
|
|
46
|
+
* Flow:
|
|
47
|
+
* 1. connect() - init WA WebSocket
|
|
48
|
+
* 2. requestPairing() - generate pairing code untuk user
|
|
49
|
+
* 3. wait for user to input code di mobile app
|
|
50
|
+
* 4. server sends pairing confirmation
|
|
51
|
+
* 5. switch ke normal WA Web communication
|
|
52
|
+
*/
|
|
53
|
+
export class MobileAPISocket extends EventEmitter {
|
|
54
|
+
private phoneNumber: string;
|
|
55
|
+
private creds: AuthenticationCreds;
|
|
56
|
+
private logger: WALogger;
|
|
57
|
+
private browser: [string, string, string];
|
|
58
|
+
private waWsUrl: string;
|
|
59
|
+
private connectTimeoutMs: number;
|
|
60
|
+
private ramLevel: 1 | 2 | 3;
|
|
61
|
+
|
|
62
|
+
private ws: WAWebSocket | null = null;
|
|
63
|
+
private pairingExpiry: PairingCodeExpiry | null = null;
|
|
64
|
+
private isPairing: boolean = false;
|
|
65
|
+
|
|
66
|
+
constructor(opts: MobileSocketOptions) {
|
|
67
|
+
super();
|
|
68
|
+
this.phoneNumber = opts.phoneNumber;
|
|
69
|
+
this.creds = opts.creds;
|
|
70
|
+
this.logger = opts.logger ?? new SilentLogger();
|
|
71
|
+
this.browser = opts.browser ?? ['Waliwa', 'WA', '1.0.0'];
|
|
72
|
+
this.waWsUrl = opts.waWsUrl ?? 'wss://web.whatsapp.com/ws/chat';
|
|
73
|
+
this.connectTimeoutMs = opts.connectTimeoutMs ?? 30000;
|
|
74
|
+
this.ramLevel = opts.ramLevel ?? 2;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Connect dan start pairing flow
|
|
79
|
+
*/
|
|
80
|
+
async connect(): Promise<PairingCodeResult> {
|
|
81
|
+
this.logger.info(`Starting Mobile API pairing untuk phone: ${this.phoneNumber}`);
|
|
82
|
+
|
|
83
|
+
// Init WA WebSocket dengan Noise handshake
|
|
84
|
+
this.ws = new WAWebSocket({
|
|
85
|
+
url: this.waWsUrl,
|
|
86
|
+
logger: this.logger,
|
|
87
|
+
browser: this.browser,
|
|
88
|
+
connectTimeoutMs: this.connectTimeoutMs,
|
|
89
|
+
noiseKey: this.creds.noiseKey,
|
|
90
|
+
ramLevel: this.ramLevel
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Listen for incoming nodes
|
|
94
|
+
this.ws.on('message', (data: Uint8Array) => this.handleIncomingData(data));
|
|
95
|
+
this.ws.on('close', (code: number, reason: string) => {
|
|
96
|
+
this.logger.warn(`Mobile socket closed: ${code} ${reason}`);
|
|
97
|
+
this.emit('close', code, reason);
|
|
98
|
+
});
|
|
99
|
+
this.ws.on('error', (err: Error) => {
|
|
100
|
+
this.logger.error(`Mobile socket error: ${err.message}`);
|
|
101
|
+
this.emit('error', err);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await this.ws.connect();
|
|
105
|
+
this.logger.info('WebSocket connected, generating pairing code');
|
|
106
|
+
|
|
107
|
+
// Generate pairing code
|
|
108
|
+
const pairingResult = generatePairingCode(this.phoneNumber, this.creds, this.logger);
|
|
109
|
+
|
|
110
|
+
// Setup expiry
|
|
111
|
+
this.pairingExpiry = new PairingCodeExpiry(
|
|
112
|
+
pairingResult.expiresAt,
|
|
113
|
+
() => {
|
|
114
|
+
this.logger.warn('Pairing code expired');
|
|
115
|
+
this.emit('pairing:expired');
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
this.pairingExpiry.start();
|
|
119
|
+
|
|
120
|
+
// Send pairing request ke server
|
|
121
|
+
await this.sendPairingRequest(pairingResult);
|
|
122
|
+
|
|
123
|
+
this.isPairing = true;
|
|
124
|
+
return pairingResult;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Send pairing request ke server
|
|
129
|
+
*/
|
|
130
|
+
private async sendPairingRequest(pairing: PairingCodeResult): Promise<void> {
|
|
131
|
+
if (!this.creds.pairingKey) {
|
|
132
|
+
throw new Error('No pairing key in creds');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pairNode: BinaryNode = {
|
|
136
|
+
tag: 'iq',
|
|
137
|
+
attrs: {
|
|
138
|
+
type: 'set',
|
|
139
|
+
to: 's.whatsapp.net',
|
|
140
|
+
id: `pair-${Date.now()}`
|
|
141
|
+
},
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
tag: 'pair',
|
|
145
|
+
attrs: {
|
|
146
|
+
code: pairing.code.replace('-', ''),
|
|
147
|
+
phone: pairing.phoneNumber,
|
|
148
|
+
pubkey: Buffer.from(this.creds.pairingKey.public).toString('base64'),
|
|
149
|
+
platform: this.browser[0]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
this.ws!.send(encodeBinaryNode(pairNode));
|
|
156
|
+
this.logger.debug(`Pairing request sent for ${pairing.phoneNumber}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Handle incoming data
|
|
161
|
+
*/
|
|
162
|
+
private handleIncomingData(data: Uint8Array): void {
|
|
163
|
+
try {
|
|
164
|
+
const node = decodeBinaryNode(data);
|
|
165
|
+
if (!node) return;
|
|
166
|
+
|
|
167
|
+
// Pairing response
|
|
168
|
+
if (node.tag === 'iq' && node.attrs?.id?.startsWith('pair-')) {
|
|
169
|
+
const result = parsePairingResult(node);
|
|
170
|
+
if (result.success) {
|
|
171
|
+
this.logger.info(`Pairing successful: paired with ${result.jid}`);
|
|
172
|
+
// Update creds
|
|
173
|
+
this.creds.registered = true;
|
|
174
|
+
if (result.jid) {
|
|
175
|
+
this.creds.me = result.jid;
|
|
176
|
+
}
|
|
177
|
+
if (result.identityKey) {
|
|
178
|
+
// Store server-side identity
|
|
179
|
+
this.logger.debug('Got server identity key');
|
|
180
|
+
}
|
|
181
|
+
if (this.pairingExpiry) {
|
|
182
|
+
this.pairingExpiry.cancel();
|
|
183
|
+
this.pairingExpiry = null;
|
|
184
|
+
}
|
|
185
|
+
this.isPairing = false;
|
|
186
|
+
this.emit('paired', result);
|
|
187
|
+
this.emit('connection.update', {
|
|
188
|
+
connection: 'open',
|
|
189
|
+
isNewLogin: true
|
|
190
|
+
} as ConnectionUpdate);
|
|
191
|
+
} else {
|
|
192
|
+
this.logger.error(`Pairing failed: ${result.error}`);
|
|
193
|
+
this.emit('pairing:error', new Error(result.error));
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Re-emit other nodes untuk processing selanjutnya
|
|
199
|
+
this.emit('message', node);
|
|
200
|
+
} catch (err: any) {
|
|
201
|
+
this.logger.error(`Failed to process data: ${err.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Send raw data via WebSocket
|
|
207
|
+
*/
|
|
208
|
+
send(data: Uint8Array): void {
|
|
209
|
+
if (!this.ws || !this.ws.isConnected()) {
|
|
210
|
+
throw new Error('Socket not connected');
|
|
211
|
+
}
|
|
212
|
+
this.ws.send(data);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get underlying WA WebSocket (untuk pas-pairing switch ke WA Web mode)
|
|
217
|
+
*/
|
|
218
|
+
getWebSocket(): WAWebSocket | null {
|
|
219
|
+
return this.ws;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Close socket
|
|
224
|
+
*/
|
|
225
|
+
async close(): Promise<void> {
|
|
226
|
+
if (this.pairingExpiry) {
|
|
227
|
+
this.pairingExpiry.cancel();
|
|
228
|
+
this.pairingExpiry = null;
|
|
229
|
+
}
|
|
230
|
+
if (this.ws) {
|
|
231
|
+
this.ws.close();
|
|
232
|
+
this.ws = null;
|
|
233
|
+
}
|
|
234
|
+
this.isPairing = false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
isPairingInProgress(): boolean {
|
|
238
|
+
return this.isPairing;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ============== Hybrid Socket Manager ==============
|
|
243
|
+
|
|
244
|
+
export interface HybridSocketOptions extends WaliwaConfig {
|
|
245
|
+
/** Primary mode: 'web' (QR) atau 'mobile' (pairing code) */
|
|
246
|
+
primaryMode?: 'web' | 'mobile';
|
|
247
|
+
/** Phone number (required jika primaryMode='mobile') */
|
|
248
|
+
phoneNumber?: string;
|
|
249
|
+
/** Auto-fallback ke other mode jika primary gagal */
|
|
250
|
+
autoFallback?: boolean;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Hybrid socket manager - otomatis switch antara WA Web (QR) dan Mobile API (pairing)
|
|
255
|
+
*
|
|
256
|
+
* - Primary mode: coba connect dengan mode utama
|
|
257
|
+
* - Fallback: jika gagal setelah maxRetries, switch ke mode lain
|
|
258
|
+
* - Mode switching transparent ke caller
|
|
259
|
+
*/
|
|
260
|
+
export class HybridSocketManager extends EventEmitter {
|
|
261
|
+
private opts: Required<HybridSocketOptions>;
|
|
262
|
+
private currentMode: 'web' | 'mobile';
|
|
263
|
+
private currentSocket: WAWebSocket | MobileAPISocket | null = null;
|
|
264
|
+
private retryCount: number = 0;
|
|
265
|
+
|
|
266
|
+
constructor(opts: HybridSocketOptions) {
|
|
267
|
+
super();
|
|
268
|
+
this.opts = {
|
|
269
|
+
...opts,
|
|
270
|
+
primaryMode: opts.primaryMode ?? (opts.mobile ? 'mobile' : 'web'),
|
|
271
|
+
autoFallback: opts.autoFallback ?? true,
|
|
272
|
+
phoneNumber: opts.phoneNumber ?? '',
|
|
273
|
+
wsURL: opts.wsURL ?? 'wss://web.whatsapp.com/ws/chat',
|
|
274
|
+
connectTimeoutMs: opts.connectTimeoutMs ?? 20000,
|
|
275
|
+
keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 20000,
|
|
276
|
+
ramOptimizationLevel: opts.ramOptimizationLevel ?? 2,
|
|
277
|
+
browser: opts.browser ?? ['Waliwa', 'WA', '1.0.0'],
|
|
278
|
+
printQRInTerminal: opts.printQRInTerminal ?? false,
|
|
279
|
+
mobile: opts.mobile ?? false,
|
|
280
|
+
syncFullHistory: opts.syncFullHistory ?? false,
|
|
281
|
+
maxMsgRetryCount: opts.maxMsgRetryCount ?? 5,
|
|
282
|
+
maxReconnectAttempts: opts.maxReconnectAttempts ?? 10,
|
|
283
|
+
reconnectInterval: opts.reconnectInterval ?? 2000,
|
|
284
|
+
markOnlineOnConnect: opts.markOnlineOnConnect ?? true,
|
|
285
|
+
fetchAgent: opts.fetchAgent
|
|
286
|
+
} as any;
|
|
287
|
+
this.currentMode = this.opts.primaryMode;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Connect dengan hybrid mode
|
|
292
|
+
*/
|
|
293
|
+
async connect(): Promise<any> {
|
|
294
|
+
if (this.currentMode === 'mobile') {
|
|
295
|
+
if (!this.opts.phoneNumber) {
|
|
296
|
+
if (this.opts.autoFallback) {
|
|
297
|
+
this.logger().info('No phone number provided, falling back to QR mode');
|
|
298
|
+
this.currentMode = 'web';
|
|
299
|
+
return this.connect();
|
|
300
|
+
}
|
|
301
|
+
throw new Error('phoneNumber required untuk Mobile API mode');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const mobileSocket = new MobileAPISocket({
|
|
305
|
+
phoneNumber: this.opts.phoneNumber!,
|
|
306
|
+
creds: this.opts.authState!.creds,
|
|
307
|
+
logger: this.logger(),
|
|
308
|
+
browser: this.opts.browser!,
|
|
309
|
+
waWsUrl: this.opts.wsURL!,
|
|
310
|
+
connectTimeoutMs: this.opts.connectTimeoutMs!,
|
|
311
|
+
ramLevel: this.opts.ramOptimizationLevel!
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
mobileSocket.on('paired', (result) => this.emit('paired', result));
|
|
315
|
+
mobileSocket.on('pairing:error', (err) => this.handleModeError(err));
|
|
316
|
+
mobileSocket.on('pairing:expired', () => this.handleModeError(new Error('Pairing expired')));
|
|
317
|
+
mobileSocket.on('connection.update', (update) => this.emit('connection.update', update));
|
|
318
|
+
mobileSocket.on('message', (node) => this.emit('message', node));
|
|
319
|
+
|
|
320
|
+
this.currentSocket = mobileSocket;
|
|
321
|
+
return mobileSocket.connect();
|
|
322
|
+
} else {
|
|
323
|
+
// Web mode
|
|
324
|
+
const wsSocket = new WAWebSocket({
|
|
325
|
+
url: this.opts.wsURL!,
|
|
326
|
+
logger: this.logger(),
|
|
327
|
+
browser: this.opts.browser!,
|
|
328
|
+
connectTimeoutMs: this.opts.connectTimeoutMs!,
|
|
329
|
+
keepAliveIntervalMs: this.opts.keepAliveIntervalMs!,
|
|
330
|
+
noiseKey: this.opts.authState!.creds.noiseKey,
|
|
331
|
+
ramLevel: this.opts.ramOptimizationLevel!
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
wsSocket.on('message', (data: Uint8Array) => this.emit('message', data));
|
|
335
|
+
wsSocket.on('close', (code: number, reason: string) => {
|
|
336
|
+
this.emit('close', code, reason);
|
|
337
|
+
});
|
|
338
|
+
wsSocket.on('error', (err: Error) => this.handleModeError(err));
|
|
339
|
+
|
|
340
|
+
this.currentSocket = wsSocket;
|
|
341
|
+
return wsSocket.connect();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Handle error dari current socket - mungkin switch mode
|
|
347
|
+
*/
|
|
348
|
+
private handleModeError(err: Error): void {
|
|
349
|
+
this.retryCount++;
|
|
350
|
+
this.logger().error(`Socket error (${this.currentMode} mode): ${err.message}`);
|
|
351
|
+
|
|
352
|
+
if (this.opts.autoFallback && this.retryCount >= 3) {
|
|
353
|
+
// Switch mode
|
|
354
|
+
const newMode = this.currentMode === 'web' ? 'mobile' : 'web';
|
|
355
|
+
this.logger().warn(`Switching from ${this.currentMode} to ${newMode} mode after ${this.retryCount} retries`);
|
|
356
|
+
this.currentMode = newMode;
|
|
357
|
+
this.retryCount = 0;
|
|
358
|
+
|
|
359
|
+
// Close current dan reconnect
|
|
360
|
+
if (this.currentSocket) {
|
|
361
|
+
if (this.currentSocket instanceof MobileAPISocket) {
|
|
362
|
+
this.currentSocket.close();
|
|
363
|
+
} else {
|
|
364
|
+
this.currentSocket.close();
|
|
365
|
+
}
|
|
366
|
+
this.currentSocket = null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Reconnect dengan mode baru
|
|
370
|
+
this.connect().catch(reconnectErr => {
|
|
371
|
+
this.logger().error(`Reconnect failed: ${reconnectErr.message}`);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Send data via current socket
|
|
378
|
+
*/
|
|
379
|
+
send(data: Uint8Array): void {
|
|
380
|
+
if (!this.currentSocket) {
|
|
381
|
+
throw new Error('No active socket');
|
|
382
|
+
}
|
|
383
|
+
if (this.currentSocket instanceof MobileAPISocket) {
|
|
384
|
+
this.currentSocket.send(data);
|
|
385
|
+
} else {
|
|
386
|
+
this.currentSocket.send(data);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Close socket
|
|
392
|
+
*/
|
|
393
|
+
async close(): Promise<void> {
|
|
394
|
+
if (this.currentSocket) {
|
|
395
|
+
if (this.currentSocket instanceof MobileAPISocket) {
|
|
396
|
+
await this.currentSocket.close();
|
|
397
|
+
} else {
|
|
398
|
+
this.currentSocket.close();
|
|
399
|
+
}
|
|
400
|
+
this.currentSocket = null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get current mode
|
|
406
|
+
*/
|
|
407
|
+
getCurrentMode(): 'web' | 'mobile' {
|
|
408
|
+
return this.currentMode;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private logger(): WALogger {
|
|
412
|
+
return this.opts.logger ?? new SilentLogger();
|
|
413
|
+
}
|
|
414
|
+
}
|