whatsapp-pi 1.0.29 → 1.0.32
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/package.json +1 -1
- package/src/models/whatsapp.types.ts +0 -59
- package/src/services/session.manager.ts +0 -68
- package/src/services/whatsapp.service.ts +0 -24
- package/src/services/qr-config.service.ts +0 -160
- package/src/services/qr-error.handler.ts +0 -151
- package/src/services/qr-events.service.ts +0 -179
- package/src/services/qr-renderer.service.ts +0 -154
package/package.json
CHANGED
|
@@ -78,62 +78,3 @@ export interface RecentsStore {
|
|
|
78
78
|
messagesBySender: Record<string, RecentConversationMessage[]>;
|
|
79
79
|
updatedAt: number;
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
// QR Code Display Types
|
|
83
|
-
export interface QRCodeSession {
|
|
84
|
-
id: string;
|
|
85
|
-
qrData: string;
|
|
86
|
-
expiresAt: Date;
|
|
87
|
-
isActive: boolean;
|
|
88
|
-
createdAt: Date;
|
|
89
|
-
refreshCount: number;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface QRDisplayState {
|
|
93
|
-
isDisplaying: boolean;
|
|
94
|
-
currentSession?: QRCodeSession;
|
|
95
|
-
lastInstruction: string;
|
|
96
|
-
warningMessage?: string;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export enum PairingStatus {
|
|
100
|
-
IDLE = 'idle',
|
|
101
|
-
GENERATING_QR = 'generating',
|
|
102
|
-
DISPLAYING_QR = 'displaying',
|
|
103
|
-
SCANNING = 'scanning',
|
|
104
|
-
CONNECTED = 'connected',
|
|
105
|
-
FAILED = 'failed'
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export type ExtendedSessionStatus = SessionStatus | 'pairing' | 'qr-expired';
|
|
109
|
-
|
|
110
|
-
export interface QRCodeDisplayOptions {
|
|
111
|
-
terminalWidth?: number;
|
|
112
|
-
refreshInterval: number;
|
|
113
|
-
showInstructions: boolean;
|
|
114
|
-
showExpirationWarning: boolean;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export class QRError extends Error {
|
|
118
|
-
constructor(
|
|
119
|
-
public code: string,
|
|
120
|
-
message: string,
|
|
121
|
-
public technical?: string,
|
|
122
|
-
public recoverable: boolean = true
|
|
123
|
-
) {
|
|
124
|
-
super(message);
|
|
125
|
-
this.name = 'QRError';
|
|
126
|
-
this.timestamp = new Date();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
public timestamp: Date;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export enum QRErrorCode {
|
|
133
|
-
TERMINAL_UNSUPPORTED = 'TERMINAL_UNSUPPORTED',
|
|
134
|
-
QR_GENERATION_FAILED = 'QR_GENERATION_FAILED',
|
|
135
|
-
DISPLAY_ERROR = 'DISPLAY_ERROR',
|
|
136
|
-
CONNECTION_TIMEOUT = 'CONNECTION_TIMEOUT',
|
|
137
|
-
NETWORK_ERROR = 'NETWORK_ERROR',
|
|
138
|
-
INVALID_CREDENTIALS = 'INVALID_CREDENTIALS'
|
|
139
|
-
}
|
|
@@ -285,72 +285,4 @@ export class SessionManager {
|
|
|
285
285
|
getAuthStateDir(): string {
|
|
286
286
|
return this.authStateDir;
|
|
287
287
|
}
|
|
288
|
-
|
|
289
|
-
// QR Code Detection Methods
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Checks if QR code flow is needed (no valid credentials exist)
|
|
293
|
-
*/
|
|
294
|
-
public async needsQRCode(): Promise<boolean> {
|
|
295
|
-
try {
|
|
296
|
-
// Check if we have valid authentication state
|
|
297
|
-
const hasValidCredentials = await this.hasValidCredentials();
|
|
298
|
-
return !hasValidCredentials;
|
|
299
|
-
} catch (error) {
|
|
300
|
-
// If we can't determine, assume QR is needed
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Checks if valid credentials exist
|
|
307
|
-
*/
|
|
308
|
-
public async hasValidCredentials(): Promise<boolean> {
|
|
309
|
-
try {
|
|
310
|
-
// Check if credentials file exists and is readable
|
|
311
|
-
const credsPath = join(this.authStateDir, 'creds.json');
|
|
312
|
-
await readFile(credsPath, 'utf-8');
|
|
313
|
-
|
|
314
|
-
// Also check if we have the hasAuthState flag set
|
|
315
|
-
return this.hasAuthState;
|
|
316
|
-
} catch {
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Marks QR code pairing as completed
|
|
323
|
-
*/
|
|
324
|
-
public async markQRCompleted(): Promise<void> {
|
|
325
|
-
await this.markAuthStateAvailable();
|
|
326
|
-
this.status = 'connected';
|
|
327
|
-
await this.saveConfig();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Invalidates current QR session (used for logout/re-pairing)
|
|
332
|
-
*/
|
|
333
|
-
public async invalidateQRSession(): Promise<void> {
|
|
334
|
-
// Clear auth state to trigger QR flow on next connection
|
|
335
|
-
this.hasAuthState = false;
|
|
336
|
-
this.status = 'logged-out';
|
|
337
|
-
await this.saveConfig();
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Waits for credentials to become available (with timeout)
|
|
342
|
-
*/
|
|
343
|
-
public async waitForCredentials(timeout: number = 120000): Promise<boolean> {
|
|
344
|
-
const startTime = Date.now();
|
|
345
|
-
const checkInterval = 1000; // Check every second
|
|
346
|
-
|
|
347
|
-
while (Date.now() - startTime < timeout) {
|
|
348
|
-
if (await this.hasValidCredentials()) {
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return false; // Timeout reached
|
|
355
|
-
}
|
|
356
288
|
}
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
} from '@whiskeysockets/baileys';
|
|
8
8
|
import P from 'pino';
|
|
9
9
|
import { Boom } from '@hapi/boom';
|
|
10
|
-
import qr from 'qrcode-terminal';
|
|
11
10
|
import { SessionManager } from './session.manager.js';
|
|
12
11
|
import { IncomingMessage, WhatsAppSession, SessionStatus } from '../models/whatsapp.types.js';
|
|
13
12
|
import { MessageSender } from './message.sender.js';
|
|
@@ -120,7 +119,6 @@ export class WhatsAppService {
|
|
|
120
119
|
|
|
121
120
|
if (qr) {
|
|
122
121
|
this.sessionManager.setStatus('pairing');
|
|
123
|
-
this.displayQRCode(qr);
|
|
124
122
|
this.onQRCode?.(qr);
|
|
125
123
|
this.onStatusUpdate?.('| WhatsApp: Pairing...');
|
|
126
124
|
}
|
|
@@ -258,28 +256,6 @@ export class WhatsAppService {
|
|
|
258
256
|
this.onStatusUpdate = callback;
|
|
259
257
|
}
|
|
260
258
|
|
|
261
|
-
private displayQRCode(qrData: string): void {
|
|
262
|
-
// Clear screen and show QR code with instructions
|
|
263
|
-
console.clear();
|
|
264
|
-
|
|
265
|
-
console.log('');
|
|
266
|
-
console.log('WhatsApp: Pairing...');
|
|
267
|
-
console.log('Scan this QR code with your WhatsApp mobile app:');
|
|
268
|
-
console.log('');
|
|
269
|
-
|
|
270
|
-
// Display QR code
|
|
271
|
-
qr.generate(qrData, { small: true });
|
|
272
|
-
|
|
273
|
-
console.log('');
|
|
274
|
-
console.log('1. Open WhatsApp on your phone');
|
|
275
|
-
console.log('2. Go to Settings > Linked Devices');
|
|
276
|
-
console.log('3. Tap "Link a device"');
|
|
277
|
-
console.log('4. Point your camera at this QR code');
|
|
278
|
-
console.log('');
|
|
279
|
-
console.log('QR code will refresh automatically if it expires.');
|
|
280
|
-
console.log('');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
259
|
public getLastRemoteJid(): string | null {
|
|
284
260
|
return this.lastRemoteJid;
|
|
285
261
|
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { QRCodeDisplayOptions } from '../models/whatsapp.types.js';
|
|
2
|
-
|
|
3
|
-
export interface QRDisplayConfig {
|
|
4
|
-
terminalWidth?: number;
|
|
5
|
-
refreshInterval: number;
|
|
6
|
-
maxRefreshAttempts: number;
|
|
7
|
-
showInstructions: boolean;
|
|
8
|
-
showExpirationWarning: boolean;
|
|
9
|
-
expirationWarningTime: number;
|
|
10
|
-
retryOnError: boolean;
|
|
11
|
-
maxRetries: number;
|
|
12
|
-
retryDelay: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class QRConfigService {
|
|
16
|
-
private static instance: QRConfigService;
|
|
17
|
-
private config: QRDisplayConfig;
|
|
18
|
-
|
|
19
|
-
private constructor() {
|
|
20
|
-
this.config = this.getDefaultConfig();
|
|
21
|
-
this.loadFromEnvironment();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Gets singleton instance
|
|
26
|
-
*/
|
|
27
|
-
static getInstance(): QRConfigService {
|
|
28
|
-
if (!QRConfigService.instance) {
|
|
29
|
-
QRConfigService.instance = new QRConfigService();
|
|
30
|
-
}
|
|
31
|
-
return QRConfigService.instance;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Gets current configuration
|
|
36
|
-
*/
|
|
37
|
-
getConfig(): QRDisplayConfig {
|
|
38
|
-
return { ...this.config };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Updates configuration
|
|
43
|
-
*/
|
|
44
|
-
updateConfig(config: Partial<QRDisplayConfig>): void {
|
|
45
|
-
this.config = { ...this.config, ...config };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Resets configuration to defaults
|
|
50
|
-
*/
|
|
51
|
-
resetConfig(): void {
|
|
52
|
-
this.config = this.getDefaultConfig();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Gets QR display options for renderer
|
|
57
|
-
*/
|
|
58
|
-
getDisplayOptions(): QRCodeDisplayOptions {
|
|
59
|
-
return {
|
|
60
|
-
terminalWidth: this.config.terminalWidth,
|
|
61
|
-
refreshInterval: this.config.refreshInterval,
|
|
62
|
-
showInstructions: this.config.showInstructions,
|
|
63
|
-
showExpirationWarning: this.config.showExpirationWarning
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Gets retry configuration
|
|
69
|
-
*/
|
|
70
|
-
getRetryConfig() {
|
|
71
|
-
return {
|
|
72
|
-
retryOnError: this.config.retryOnError,
|
|
73
|
-
maxRetries: this.config.maxRetries,
|
|
74
|
-
retryDelay: this.config.retryDelay
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Gets refresh configuration
|
|
80
|
-
*/
|
|
81
|
-
getRefreshConfig() {
|
|
82
|
-
return {
|
|
83
|
-
refreshInterval: this.config.refreshInterval,
|
|
84
|
-
maxRefreshAttempts: this.config.maxRefreshAttempts,
|
|
85
|
-
expirationWarningTime: this.config.expirationWarningTime,
|
|
86
|
-
showExpirationWarning: this.config.showExpirationWarning
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Loads configuration from environment variables
|
|
92
|
-
*/
|
|
93
|
-
private loadFromEnvironment(): void {
|
|
94
|
-
// QR refresh interval in milliseconds
|
|
95
|
-
if (process.env.QR_REFRESH_INTERVAL) {
|
|
96
|
-
const interval = parseInt(process.env.QR_REFRESH_INTERVAL, 10);
|
|
97
|
-
if (!isNaN(interval) && interval > 0) {
|
|
98
|
-
this.config.refreshInterval = interval;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Show expiration warning
|
|
103
|
-
if (process.env.QR_SHOW_WARNING !== undefined) {
|
|
104
|
-
this.config.showExpirationWarning = process.env.QR_SHOW_WARNING === 'true';
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Warning time in seconds
|
|
108
|
-
if (process.env.QR_WARNING_TIME) {
|
|
109
|
-
const warningTime = parseInt(process.env.QR_WARNING_TIME, 10);
|
|
110
|
-
if (!isNaN(warningTime) && warningTime > 0) {
|
|
111
|
-
this.config.expirationWarningTime = warningTime;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Max refresh attempts
|
|
116
|
-
if (process.env.QR_MAX_REFRESH_ATTEMPTS) {
|
|
117
|
-
const maxAttempts = parseInt(process.env.QR_MAX_REFRESH_ATTEMPTS, 10);
|
|
118
|
-
if (!isNaN(maxAttempts) && maxAttempts > 0) {
|
|
119
|
-
this.config.maxRefreshAttempts = maxAttempts;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Retry on error
|
|
124
|
-
if (process.env.QR_RETRY_ON_ERROR !== undefined) {
|
|
125
|
-
this.config.retryOnError = process.env.QR_RETRY_ON_ERROR === 'true';
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Max retries
|
|
129
|
-
if (process.env.QR_MAX_RETRIES) {
|
|
130
|
-
const maxRetries = parseInt(process.env.QR_MAX_RETRIES, 10);
|
|
131
|
-
if (!isNaN(maxRetries) && maxRetries >= 0) {
|
|
132
|
-
this.config.maxRetries = maxRetries;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Retry delay
|
|
137
|
-
if (process.env.QR_RETRY_DELAY) {
|
|
138
|
-
const retryDelay = parseInt(process.env.QR_RETRY_DELAY, 10);
|
|
139
|
-
if (!isNaN(retryDelay) && retryDelay > 0) {
|
|
140
|
-
this.config.retryDelay = retryDelay;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Gets default configuration
|
|
147
|
-
*/
|
|
148
|
-
private getDefaultConfig(): QRDisplayConfig {
|
|
149
|
-
return {
|
|
150
|
-
refreshInterval: 60000, // 60 seconds
|
|
151
|
-
maxRefreshAttempts: 10, // Unlimited for practical purposes
|
|
152
|
-
showInstructions: true,
|
|
153
|
-
showExpirationWarning: true,
|
|
154
|
-
expirationWarningTime: 15, // 15 seconds before expiration
|
|
155
|
-
retryOnError: true,
|
|
156
|
-
maxRetries: 3,
|
|
157
|
-
retryDelay: 2000 // 2 seconds
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { QRError, QRErrorCode } from '../models/whatsapp.types.js';
|
|
2
|
-
|
|
3
|
-
export class QRErrorHandler {
|
|
4
|
-
private errorHistory: QRError[] = [];
|
|
5
|
-
private maxHistorySize = 10;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Handles a QR-related error
|
|
9
|
-
*/
|
|
10
|
-
async handleError(error: QRError): Promise<void> {
|
|
11
|
-
// Add to error history
|
|
12
|
-
this.addToHistory(error);
|
|
13
|
-
|
|
14
|
-
// Log the error
|
|
15
|
-
this.logError(error);
|
|
16
|
-
|
|
17
|
-
// Show user-friendly message
|
|
18
|
-
this.displayError(error);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Determines if an error is recoverable
|
|
23
|
-
*/
|
|
24
|
-
canRecover(error: QRError): boolean {
|
|
25
|
-
return error.recoverable;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Gets recovery action for an error
|
|
30
|
-
*/
|
|
31
|
-
getRecoveryAction(error: QRError): string | null {
|
|
32
|
-
switch (error.code) {
|
|
33
|
-
case QRErrorCode.TERMINAL_UNSUPPORTED:
|
|
34
|
-
return 'Try using a modern terminal (Windows Terminal, iTerm2, or VS Code terminal) that supports UTF-8 characters.';
|
|
35
|
-
|
|
36
|
-
case QRErrorCode.QR_GENERATION_FAILED:
|
|
37
|
-
return 'Check your internet connection and try running the command again.';
|
|
38
|
-
|
|
39
|
-
case QRErrorCode.DISPLAY_ERROR:
|
|
40
|
-
return 'Try making your terminal window larger and run the command again.';
|
|
41
|
-
|
|
42
|
-
case QRErrorCode.CONNECTION_TIMEOUT:
|
|
43
|
-
return 'The QR code expired. A new one will be generated automatically.';
|
|
44
|
-
|
|
45
|
-
case QRErrorCode.NETWORK_ERROR:
|
|
46
|
-
return 'Check your internet connection and try again.';
|
|
47
|
-
|
|
48
|
-
case QRErrorCode.INVALID_CREDENTIALS:
|
|
49
|
-
return 'Run /whatsapp-logout to clear credentials and try again.';
|
|
50
|
-
|
|
51
|
-
default:
|
|
52
|
-
return 'Try restarting the application.';
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Creates a standardized QR error
|
|
58
|
-
*/
|
|
59
|
-
createError(code: QRErrorCode, message: string, technical?: string): QRError {
|
|
60
|
-
return new QRError(
|
|
61
|
-
code,
|
|
62
|
-
message,
|
|
63
|
-
technical,
|
|
64
|
-
this.isRecoverableByDefault(code)
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Gets recent error history
|
|
70
|
-
*/
|
|
71
|
-
getErrorHistory(): QRError[] {
|
|
72
|
-
return [...this.errorHistory];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Clears error history
|
|
77
|
-
*/
|
|
78
|
-
clearHistory(): void {
|
|
79
|
-
this.errorHistory = [];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Checks if specific error type has occurred recently
|
|
84
|
-
*/
|
|
85
|
-
hasRecentError(code: QRErrorCode, withinMs: number = 60000): boolean {
|
|
86
|
-
const now = new Date();
|
|
87
|
-
return this.errorHistory.some(error =>
|
|
88
|
-
error.code === code &&
|
|
89
|
-
(now.getTime() - error.timestamp.getTime()) < withinMs
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Private method to add error to history
|
|
95
|
-
*/
|
|
96
|
-
private addToHistory(error: QRError): void {
|
|
97
|
-
this.errorHistory.push(error);
|
|
98
|
-
|
|
99
|
-
// Keep only recent errors
|
|
100
|
-
if (this.errorHistory.length > this.maxHistorySize) {
|
|
101
|
-
this.errorHistory = this.errorHistory.slice(-this.maxHistorySize);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Private method to log error
|
|
107
|
-
*/
|
|
108
|
-
private logError(error: QRError): void {
|
|
109
|
-
const logMessage = `[QR Error] ${error.code}: ${error.message}`;
|
|
110
|
-
|
|
111
|
-
if (error.technical) {
|
|
112
|
-
console.error(`${logMessage} (${error.technical})`);
|
|
113
|
-
} else {
|
|
114
|
-
console.error(logMessage);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Private method to display error to user
|
|
120
|
-
*/
|
|
121
|
-
private displayError(error: QRError): void {
|
|
122
|
-
console.error(`❌ ${error.message}`);
|
|
123
|
-
|
|
124
|
-
const recoveryAction = this.getRecoveryAction(error);
|
|
125
|
-
if (recoveryAction) {
|
|
126
|
-
console.log(`💡 ${recoveryAction}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Private method to determine if error is recoverable by default
|
|
132
|
-
*/
|
|
133
|
-
private isRecoverableByDefault(code: QRErrorCode): boolean {
|
|
134
|
-
switch (code) {
|
|
135
|
-
case QRErrorCode.TERMINAL_UNSUPPORTED:
|
|
136
|
-
return false; // Requires terminal change
|
|
137
|
-
|
|
138
|
-
case QRErrorCode.INVALID_CREDENTIALS:
|
|
139
|
-
return true; // Can be fixed with logout
|
|
140
|
-
|
|
141
|
-
case QRErrorCode.QR_GENERATION_FAILED:
|
|
142
|
-
case QRErrorCode.DISPLAY_ERROR:
|
|
143
|
-
case QRErrorCode.CONNECTION_TIMEOUT:
|
|
144
|
-
case QRErrorCode.NETWORK_ERROR:
|
|
145
|
-
return true; // Generally recoverable
|
|
146
|
-
|
|
147
|
-
default:
|
|
148
|
-
return true; // Assume recoverable by default
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import { QRCodeSession, QRError, PairingStatus } from '../models/whatsapp.types.js';
|
|
3
|
-
|
|
4
|
-
export interface QREventMap {
|
|
5
|
-
'qr:generated': { qrData: string; expiresAt: Date };
|
|
6
|
-
'qr:expired': { sessionId: string };
|
|
7
|
-
'qr:refreshed': { qrData: string; refreshCount: number };
|
|
8
|
-
'pairing:started': { sessionId: string };
|
|
9
|
-
'pairing:scanning': { sessionId: string };
|
|
10
|
-
'pairing:completed': { sessionId: string };
|
|
11
|
-
'pairing:failed': { sessionId: string; error: QRError };
|
|
12
|
-
'status:changed': { status: PairingStatus };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class QREventsService extends EventEmitter {
|
|
16
|
-
private static instance: QREventsService;
|
|
17
|
-
|
|
18
|
-
private constructor() {
|
|
19
|
-
super();
|
|
20
|
-
this.setMaxListeners(50); // Allow more listeners for QR events
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Gets singleton instance
|
|
25
|
-
*/
|
|
26
|
-
static getInstance(): QREventsService {
|
|
27
|
-
if (!QREventsService.instance) {
|
|
28
|
-
QREventsService.instance = new QREventsService();
|
|
29
|
-
}
|
|
30
|
-
return QREventsService.instance;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Emit QR code generated event
|
|
35
|
-
*/
|
|
36
|
-
emitQRGenerated(qrData: string, expiresAt: Date): void {
|
|
37
|
-
this.emit('qr:generated', { qrData, expiresAt });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Emit QR code expired event
|
|
42
|
-
*/
|
|
43
|
-
emitQRExpired(sessionId: string): void {
|
|
44
|
-
this.emit('qr:expired', { sessionId });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Emit QR code refreshed event
|
|
49
|
-
*/
|
|
50
|
-
emitQRRefreshed(qrData: string, refreshCount: number): void {
|
|
51
|
-
this.emit('qr:refreshed', { qrData, refreshCount });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Emit pairing started event
|
|
56
|
-
*/
|
|
57
|
-
emitPairingStarted(sessionId: string): void {
|
|
58
|
-
this.emit('pairing:started', { sessionId });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Emit pairing scanning event
|
|
63
|
-
*/
|
|
64
|
-
emitPairingScanning(sessionId: string): void {
|
|
65
|
-
this.emit('pairing:scanning', { sessionId });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Emit pairing completed event
|
|
70
|
-
*/
|
|
71
|
-
emitPairingCompleted(sessionId: string): void {
|
|
72
|
-
this.emit('pairing:completed', { sessionId });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Emit pairing failed event
|
|
77
|
-
*/
|
|
78
|
-
emitPairingFailed(sessionId: string, error: QRError): void {
|
|
79
|
-
this.emit('pairing:failed', { sessionId, error });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Emit status changed event
|
|
84
|
-
*/
|
|
85
|
-
emitStatusChanged(status: PairingStatus): void {
|
|
86
|
-
this.emit('status:changed', { status });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Type-safe event listener registration
|
|
91
|
-
*/
|
|
92
|
-
onQRGenerated(callback: (data: QREventMap['qr:generated']) => void): void {
|
|
93
|
-
this.on('qr:generated', callback);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
onQRExpired(callback: (data: QREventMap['qr:expired']) => void): void {
|
|
97
|
-
this.on('qr:expired', callback);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
onQRRefreshed(callback: (data: QREventMap['qr:refreshed']) => void): void {
|
|
101
|
-
this.on('qr:refreshed', callback);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
onPairingStarted(callback: (data: QREventMap['pairing:started']) => void): void {
|
|
105
|
-
this.on('pairing:started', callback);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
onPairingScanning(callback: (data: QREventMap['pairing:scanning']) => void): void {
|
|
109
|
-
this.on('pairing:scanning', callback);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
onPairingCompleted(callback: (data: QREventMap['pairing:completed']) => void): void {
|
|
113
|
-
this.on('pairing:completed', callback);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
onPairingFailed(callback: (data: QREventMap['pairing:failed']) => void): void {
|
|
117
|
-
this.on('pairing:failed', callback);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
onStatusChanged(callback: (data: QREventMap['status:changed']) => void): void {
|
|
121
|
-
this.on('status:changed', callback);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Type-safe event listener removal
|
|
126
|
-
*/
|
|
127
|
-
offQRGenerated(callback: (data: QREventMap['qr:generated']) => void): void {
|
|
128
|
-
this.off('qr:generated', callback);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
offQRExpired(callback: (data: QREventMap['qr:expired']) => void): void {
|
|
132
|
-
this.off('qr:expired', callback);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
offQRRefreshed(callback: (data: QREventMap['qr:refreshed']) => void): void {
|
|
136
|
-
this.off('qr:refreshed', callback);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
offPairingStarted(callback: (data: QREventMap['pairing:started']) => void): void {
|
|
140
|
-
this.off('pairing:started', callback);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
offPairingScanning(callback: (data: QREventMap['pairing:scanning']) => void): void {
|
|
144
|
-
this.off('pairing:scanning', callback);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
offPairingCompleted(callback: (data: QREventMap['pairing:completed']) => void): void {
|
|
148
|
-
this.off('pairing:completed', callback);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
offPairingFailed(callback: (data: QREventMap['pairing:failed']) => void): void {
|
|
152
|
-
this.off('pairing:failed', callback);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
offStatusChanged(callback: (data: QREventMap['status:changed']) => void): void {
|
|
156
|
-
this.off('status:changed', callback);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Clear all event listeners
|
|
161
|
-
*/
|
|
162
|
-
clearAllListeners(): void {
|
|
163
|
-
this.removeAllListeners();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Get event listener count for debugging
|
|
168
|
-
*/
|
|
169
|
-
getListenerCount(eventName: keyof QREventMap): number {
|
|
170
|
-
return this.listenerCount(eventName);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Get all event names with listeners
|
|
175
|
-
*/
|
|
176
|
-
getActiveEvents(): (keyof QREventMap)[] {
|
|
177
|
-
return Object.keys(this.eventNames()) as (keyof QREventMap)[];
|
|
178
|
-
}
|
|
179
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import qr from 'qrcode-terminal';
|
|
2
|
-
import { QRCodeDisplayOptions, QRError, QRErrorCode } from '../models/whatsapp.types.js';
|
|
3
|
-
|
|
4
|
-
export class QRRendererService {
|
|
5
|
-
private terminalWidth: number;
|
|
6
|
-
private supportsUTF8: boolean;
|
|
7
|
-
|
|
8
|
-
constructor() {
|
|
9
|
-
this.terminalWidth = this.detectTerminalWidth();
|
|
10
|
-
this.supportsUTF8 = this.detectUTF8Support();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Renders a QR code in the terminal
|
|
15
|
-
*/
|
|
16
|
-
async renderQR(qrData: string, options?: QRCodeDisplayOptions): Promise<void> {
|
|
17
|
-
try {
|
|
18
|
-
if (!this.supportsUTF8) {
|
|
19
|
-
throw new QRError(
|
|
20
|
-
QRErrorCode.TERMINAL_UNSUPPORTED,
|
|
21
|
-
'Terminal does not support UTF-8 characters required for QR code display',
|
|
22
|
-
'UTF-8 support detection failed',
|
|
23
|
-
false
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const displayOptions = {
|
|
28
|
-
small: true,
|
|
29
|
-
...options
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Clear any existing QR display
|
|
33
|
-
await this.clearQR();
|
|
34
|
-
|
|
35
|
-
// Render the QR code
|
|
36
|
-
qr.generate(qrData, displayOptions);
|
|
37
|
-
|
|
38
|
-
} catch (error) {
|
|
39
|
-
if (error instanceof QRError) {
|
|
40
|
-
throw error;
|
|
41
|
-
}
|
|
42
|
-
throw new QRError(
|
|
43
|
-
QRErrorCode.DISPLAY_ERROR,
|
|
44
|
-
'Failed to render QR code in terminal',
|
|
45
|
-
error instanceof Error ? error.message : 'Unknown error',
|
|
46
|
-
true
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Clears the terminal display
|
|
53
|
-
*/
|
|
54
|
-
async clearQR(): Promise<void> {
|
|
55
|
-
try {
|
|
56
|
-
// Clear screen and move cursor to top
|
|
57
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
58
|
-
} catch (error) {
|
|
59
|
-
// Non-critical error, continue
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Shows pairing instructions to the user
|
|
65
|
-
*/
|
|
66
|
-
showInstructions(): void {
|
|
67
|
-
const instructions = [
|
|
68
|
-
'',
|
|
69
|
-
'WhatsApp: Pairing...',
|
|
70
|
-
'Scan this QR code with your WhatsApp mobile app:',
|
|
71
|
-
'',
|
|
72
|
-
'1. Open WhatsApp on your phone',
|
|
73
|
-
'2. Go to Settings > Linked Devices',
|
|
74
|
-
'3. Tap "Link a device"',
|
|
75
|
-
'4. Point your camera at this QR code',
|
|
76
|
-
''
|
|
77
|
-
];
|
|
78
|
-
|
|
79
|
-
instructions.forEach(line => {
|
|
80
|
-
console.log(line);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Shows a warning message
|
|
86
|
-
*/
|
|
87
|
-
showWarning(message: string): void {
|
|
88
|
-
console.log(`⚠️ ${message}`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Shows an error message
|
|
93
|
-
*/
|
|
94
|
-
showError(error: QRError): void {
|
|
95
|
-
console.error(`❌ ${error.message}`);
|
|
96
|
-
if (error.technical && process.env.NODE_ENV === 'development') {
|
|
97
|
-
console.error(`Technical details: ${error.technical}`);
|
|
98
|
-
}
|
|
99
|
-
if (error.recoverable) {
|
|
100
|
-
console.log('💡 You can try again.');
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Detects terminal width
|
|
106
|
-
*/
|
|
107
|
-
getTerminalWidth(): number {
|
|
108
|
-
return this.terminalWidth;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Checks if terminal supports UTF-8
|
|
113
|
-
*/
|
|
114
|
-
supportsUTF8Display(): boolean {
|
|
115
|
-
return this.supportsUTF8;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Internal method to detect terminal width
|
|
120
|
-
*/
|
|
121
|
-
private detectTerminalWidth(): number {
|
|
122
|
-
try {
|
|
123
|
-
if (process.stdout.columns) {
|
|
124
|
-
return process.stdout.columns;
|
|
125
|
-
}
|
|
126
|
-
// Fallback to common terminal width
|
|
127
|
-
return 80;
|
|
128
|
-
} catch {
|
|
129
|
-
return 80;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Internal method to detect UTF-8 support
|
|
135
|
-
*/
|
|
136
|
-
private detectUTF8Support(): boolean {
|
|
137
|
-
try {
|
|
138
|
-
// Check for common UTF-8 environment variables
|
|
139
|
-
const lang = process.env.LANG || '';
|
|
140
|
-
const lcAll = process.env.LC_ALL || '';
|
|
141
|
-
const termProgram = process.env.TERM_PROGRAM || '';
|
|
142
|
-
|
|
143
|
-
return lang.includes('UTF-8') ||
|
|
144
|
-
lang.includes('utf8') ||
|
|
145
|
-
lcAll.includes('UTF-8') ||
|
|
146
|
-
lcAll.includes('utf8') ||
|
|
147
|
-
process.platform === 'darwin' || // macOS usually supports UTF-8
|
|
148
|
-
termProgram.includes('vscode') || // VS Code terminal
|
|
149
|
-
termProgram.includes('hyper'); // Hyper terminal
|
|
150
|
-
} catch {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|