whatsapp-pi 1.0.45 → 1.0.46
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/README.md +13 -8
- package/package.json +1 -1
- package/src/models/whatsapp.types.ts +3 -0
- package/src/services/message.sender.ts +3 -3
- package/src/services/whatsapp.service.ts +83 -110
- package/src/ui/menu.handler.ts +13 -17
package/README.md
CHANGED
|
@@ -46,10 +46,10 @@ pi install npm:whatsapp-pi
|
|
|
46
46
|
pi
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
pi --whatsapp-pi-online
|
|
52
|
-
```
|
|
49
|
+
After connecting WhatsApp once from the menu and scanning the QR code, you can start Pi with auto-connect enabled:
|
|
50
|
+
```bash
|
|
51
|
+
pi --whatsapp-pi-online
|
|
52
|
+
```
|
|
53
53
|
|
|
54
54
|
3. Use the menu to connect WhatsApp and manage allowed/blocked numbers
|
|
55
55
|
|
|
@@ -69,10 +69,15 @@ npm install
|
|
|
69
69
|
pi -e whatsapp-pi.ts
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
For verbose mode (shows Baileys trace logs for debugging):
|
|
73
|
-
```bash
|
|
74
|
-
pi -e whatsapp-pi.ts --verbose
|
|
75
|
-
```
|
|
72
|
+
For verbose mode (shows Baileys trace logs for debugging):
|
|
73
|
+
```bash
|
|
74
|
+
pi -e whatsapp-pi.ts --verbose
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
To test startup auto-connect locally after you have already paired WhatsApp:
|
|
78
|
+
```bash
|
|
79
|
+
pi -e whatsapp-pi.ts --whatsapp-pi-online
|
|
80
|
+
```
|
|
76
81
|
|
|
77
82
|
## Commands
|
|
78
83
|
|
package/package.json
CHANGED
|
@@ -18,9 +18,12 @@ export interface IncomingMessage {
|
|
|
18
18
|
timestamp: number;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export type MessageOrigin = 'agent' | 'menu';
|
|
22
|
+
|
|
21
23
|
export interface MessageRequest {
|
|
22
24
|
recipientJid: string;
|
|
23
25
|
text: string;
|
|
26
|
+
origin?: MessageOrigin;
|
|
24
27
|
options?: {
|
|
25
28
|
maxRetries?: number;
|
|
26
29
|
priority?: 'high' | 'normal';
|
|
@@ -54,9 +54,9 @@ export class MessageSender {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// 3. Send the message
|
|
57
|
-
|
|
58
|
-
const response = await socket.sendMessage(request.recipientJid, {
|
|
59
|
-
text: `${request.text} π`
|
|
57
|
+
const shouldAppendPi = request.origin !== 'menu';
|
|
58
|
+
const response = await socket.sendMessage(request.recipientJid, {
|
|
59
|
+
text: shouldAppendPi ? `${request.text} π` : request.text
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
return {
|
|
@@ -71,16 +71,16 @@ interface BoomLikeError {
|
|
|
71
71
|
message?: string;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export class WhatsAppService {
|
|
75
|
-
private static readonly INITIAL_RECONNECT_DELAY_MS = 5_000;
|
|
76
|
-
private static readonly MAX_RECONNECT_DELAY_MS = 120_000;
|
|
77
|
-
|
|
78
|
-
private socket?: WhatsAppSocketLike;
|
|
79
|
-
private sessionManager: SessionManager;
|
|
80
|
-
private messageSender: MessageSender;
|
|
81
|
-
private isReconnecting = false;
|
|
82
|
-
private reconnectAttempts = 0;
|
|
83
|
-
private verboseMode = false;
|
|
74
|
+
export class WhatsAppService {
|
|
75
|
+
private static readonly INITIAL_RECONNECT_DELAY_MS = 5_000;
|
|
76
|
+
private static readonly MAX_RECONNECT_DELAY_MS = 120_000;
|
|
77
|
+
|
|
78
|
+
private socket?: WhatsAppSocketLike;
|
|
79
|
+
private sessionManager: SessionManager;
|
|
80
|
+
private messageSender: MessageSender;
|
|
81
|
+
private isReconnecting = false;
|
|
82
|
+
private reconnectAttempts = 0;
|
|
83
|
+
private verboseMode = false;
|
|
84
84
|
private onIncomingMessageRecorded?: (message: IncomingMessage) => void | Promise<void>;
|
|
85
85
|
private saveCreds?: () => Promise<void>;
|
|
86
86
|
private restoreBaileysConsoleFilter?: () => void;
|
|
@@ -168,17 +168,17 @@ export class WhatsAppService {
|
|
|
168
168
|
return '';
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
private clearReconnectTimeout() {
|
|
172
|
-
if (this.reconnectTimeout) {
|
|
173
|
-
clearTimeout(this.reconnectTimeout);
|
|
174
|
-
this.reconnectTimeout = undefined;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private getReconnectDelayMs(): number {
|
|
179
|
-
const delay = WhatsAppService.INITIAL_RECONNECT_DELAY_MS * (2 ** Math.max(0, this.reconnectAttempts - 1));
|
|
180
|
-
return Math.min(delay, WhatsAppService.MAX_RECONNECT_DELAY_MS);
|
|
181
|
-
}
|
|
171
|
+
private clearReconnectTimeout() {
|
|
172
|
+
if (this.reconnectTimeout) {
|
|
173
|
+
clearTimeout(this.reconnectTimeout);
|
|
174
|
+
this.reconnectTimeout = undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private getReconnectDelayMs(): number {
|
|
179
|
+
const delay = WhatsAppService.INITIAL_RECONNECT_DELAY_MS * (2 ** Math.max(0, this.reconnectAttempts - 1));
|
|
180
|
+
return Math.min(delay, WhatsAppService.MAX_RECONNECT_DELAY_MS);
|
|
181
|
+
}
|
|
182
182
|
|
|
183
183
|
private cleanupSocket() {
|
|
184
184
|
this.clearReconnectTimeout();
|
|
@@ -304,7 +304,7 @@ export class WhatsAppService {
|
|
|
304
304
|
private async handlePairingQr(qr: string) {
|
|
305
305
|
this.sessionManager.setStatus('pairing');
|
|
306
306
|
this.onQRCode?.(qr);
|
|
307
|
-
this.onStatusUpdate?.('| WhatsApp: Disconnected');
|
|
307
|
+
this.onStatusUpdate?.('| WhatsApp: Disconnected');
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
private async handleConnectionOpen() {
|
|
@@ -312,11 +312,11 @@ export class WhatsAppService {
|
|
|
312
312
|
console.log('WhatsApp connection successfully opened');
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
this.isReconnecting = false;
|
|
316
|
-
this.reconnectAttempts = 0;
|
|
317
|
-
this.clearReconnectTimeout();
|
|
318
|
-
await this.saveCreds?.();
|
|
319
|
-
await this.sessionManager.markAuthStateAvailable();
|
|
315
|
+
this.isReconnecting = false;
|
|
316
|
+
this.reconnectAttempts = 0;
|
|
317
|
+
this.clearReconnectTimeout();
|
|
318
|
+
await this.saveCreds?.();
|
|
319
|
+
await this.sessionManager.markAuthStateAvailable();
|
|
320
320
|
this.sessionManager.setStatus('connected');
|
|
321
321
|
this.onStatusUpdate?.('| WhatsApp: Connected');
|
|
322
322
|
}
|
|
@@ -349,58 +349,58 @@ export class WhatsAppService {
|
|
|
349
349
|
console.error(`Connection closed [${statusCode}]. Reconnecting: ${shouldReconnect}`);
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
if (shouldTreatAsLoggedOut) {
|
|
353
|
-
if (this.verboseMode) {
|
|
354
|
-
console.error(`Session rejected [${statusCode}] - preserving auth state`);
|
|
355
|
-
}
|
|
356
|
-
if (isBadMac) {
|
|
357
|
-
if (this.verboseMode) {
|
|
358
|
-
console.error('[WhatsApp-Pi] Bad MAC error detected. Your session keys are corrupted.');
|
|
359
|
-
console.error('[WhatsApp-Pi] Use Logoff (Delete Session) only if reconnect keeps failing.');
|
|
360
|
-
}
|
|
361
|
-
this.onStatusUpdate?.('| WhatsApp: Session Error (Bad MAC)');
|
|
362
|
-
} else if (isAuthRejected && allowPairingOnAuthFailure) {
|
|
363
|
-
this.onStatusUpdate?.('| WhatsApp: Session Preserved (Reconnect Failed)');
|
|
364
|
-
}
|
|
365
|
-
this.cleanupSocket();
|
|
366
|
-
this.isReconnecting = false;
|
|
367
|
-
this.reconnectAttempts = 0;
|
|
368
|
-
await this.sessionManager.setStatus('disconnected');
|
|
369
|
-
if (!isBadMac) {
|
|
370
|
-
this.onStatusUpdate?.('| WhatsApp: Disconnected');
|
|
371
|
-
}
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (statusCode === DisconnectReason.connectionReplaced) {
|
|
376
|
-
if (this.verboseMode) {
|
|
377
|
-
console.error('Connection replaced - another instance connected');
|
|
378
|
-
}
|
|
379
|
-
this.cleanupSocket();
|
|
380
|
-
this.isReconnecting = false;
|
|
381
|
-
this.reconnectAttempts = 0;
|
|
382
|
-
await this.sessionManager.setStatus('disconnected');
|
|
383
|
-
this.onStatusUpdate?.('| WhatsApp: Conflict (Another Instance)');
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (shouldReconnect && !this.isReconnecting) {
|
|
388
|
-
this.isReconnecting = true;
|
|
389
|
-
this.reconnectAttempts++;
|
|
390
|
-
const reconnectDelayMs = this.getReconnectDelayMs();
|
|
391
|
-
this.onStatusUpdate?.('| WhatsApp: Reconnecting...');
|
|
392
|
-
this.clearReconnectTimeout();
|
|
393
|
-
await this.saveCreds?.();
|
|
394
|
-
this.cleanupSocket();
|
|
395
|
-
this.reconnectTimeout = setTimeout(() => {
|
|
396
|
-
this.isReconnecting = false;
|
|
397
|
-
void this.start(options);
|
|
398
|
-
}, reconnectDelayMs);
|
|
399
|
-
} else if (!shouldReconnect) {
|
|
400
|
-
this.reconnectAttempts = 0;
|
|
401
|
-
this.sessionManager.setStatus('logged-out');
|
|
402
|
-
this.onStatusUpdate?.('| WhatsApp: Disconnected');
|
|
403
|
-
}
|
|
352
|
+
if (shouldTreatAsLoggedOut) {
|
|
353
|
+
if (this.verboseMode) {
|
|
354
|
+
console.error(`Session rejected [${statusCode}] - preserving auth state`);
|
|
355
|
+
}
|
|
356
|
+
if (isBadMac) {
|
|
357
|
+
if (this.verboseMode) {
|
|
358
|
+
console.error('[WhatsApp-Pi] Bad MAC error detected. Your session keys are corrupted.');
|
|
359
|
+
console.error('[WhatsApp-Pi] Use Logoff (Delete Session) only if reconnect keeps failing.');
|
|
360
|
+
}
|
|
361
|
+
this.onStatusUpdate?.('| WhatsApp: Session Error (Bad MAC)');
|
|
362
|
+
} else if (isAuthRejected && allowPairingOnAuthFailure) {
|
|
363
|
+
this.onStatusUpdate?.('| WhatsApp: Session Preserved (Reconnect Failed)');
|
|
364
|
+
}
|
|
365
|
+
this.cleanupSocket();
|
|
366
|
+
this.isReconnecting = false;
|
|
367
|
+
this.reconnectAttempts = 0;
|
|
368
|
+
await this.sessionManager.setStatus('disconnected');
|
|
369
|
+
if (!isBadMac) {
|
|
370
|
+
this.onStatusUpdate?.('| WhatsApp: Disconnected');
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (statusCode === DisconnectReason.connectionReplaced) {
|
|
376
|
+
if (this.verboseMode) {
|
|
377
|
+
console.error('Connection replaced - another instance connected');
|
|
378
|
+
}
|
|
379
|
+
this.cleanupSocket();
|
|
380
|
+
this.isReconnecting = false;
|
|
381
|
+
this.reconnectAttempts = 0;
|
|
382
|
+
await this.sessionManager.setStatus('disconnected');
|
|
383
|
+
this.onStatusUpdate?.('| WhatsApp: Conflict (Another Instance)');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (shouldReconnect && !this.isReconnecting) {
|
|
388
|
+
this.isReconnecting = true;
|
|
389
|
+
this.reconnectAttempts++;
|
|
390
|
+
const reconnectDelayMs = this.getReconnectDelayMs();
|
|
391
|
+
this.onStatusUpdate?.('| WhatsApp: Reconnecting...');
|
|
392
|
+
this.clearReconnectTimeout();
|
|
393
|
+
await this.saveCreds?.();
|
|
394
|
+
this.cleanupSocket();
|
|
395
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
396
|
+
this.isReconnecting = false;
|
|
397
|
+
void this.start(options);
|
|
398
|
+
}, reconnectDelayMs);
|
|
399
|
+
} else if (!shouldReconnect) {
|
|
400
|
+
this.reconnectAttempts = 0;
|
|
401
|
+
this.sessionManager.setStatus('logged-out');
|
|
402
|
+
this.onStatusUpdate?.('| WhatsApp: Disconnected');
|
|
403
|
+
}
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
private extractText(message: IncomingMessageContent | undefined): string {
|
|
@@ -497,13 +497,14 @@ export class WhatsAppService {
|
|
|
497
497
|
return this.socket;
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
-
async sendMessage(jid: string, text: string) {
|
|
500
|
+
async sendMessage(jid: string, text: string, origin: 'agent' | 'menu' = 'agent') {
|
|
501
501
|
// Ensure we show the typing indicator before sending
|
|
502
502
|
await this.sendPresence(jid, 'composing');
|
|
503
503
|
|
|
504
504
|
const result = await this.messageSender.send({
|
|
505
505
|
recipientJid: jid,
|
|
506
|
-
text
|
|
506
|
+
text,
|
|
507
|
+
origin
|
|
507
508
|
});
|
|
508
509
|
|
|
509
510
|
// After sending, we can stop the typing indicator
|
|
@@ -518,35 +519,7 @@ export class WhatsAppService {
|
|
|
518
519
|
|
|
519
520
|
async sendMenuMessage(jid: string, text: string) {
|
|
520
521
|
const normalizedJid = this.normalizeRecipientJid(jid);
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
if (!socket) {
|
|
524
|
-
return {
|
|
525
|
-
success: false,
|
|
526
|
-
error: 'WhatsApp is not connected',
|
|
527
|
-
attempts: 0
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
try {
|
|
532
|
-
await this.sendPresence(normalizedJid, 'composing');
|
|
533
|
-
const response = await socket.sendMessage(normalizedJid, { text });
|
|
534
|
-
await this.sendPresence(normalizedJid, 'paused');
|
|
535
|
-
|
|
536
|
-
return {
|
|
537
|
-
success: true,
|
|
538
|
-
messageId: response?.key?.id,
|
|
539
|
-
attempts: 1
|
|
540
|
-
};
|
|
541
|
-
} catch (error: unknown) {
|
|
542
|
-
await this.sendPresence(normalizedJid, 'paused');
|
|
543
|
-
console.error(`Failed to send menu message to ${normalizedJid}:`, error);
|
|
544
|
-
return {
|
|
545
|
-
success: false,
|
|
546
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
547
|
-
attempts: 1
|
|
548
|
-
};
|
|
549
|
-
}
|
|
522
|
+
return this.sendMessage(normalizedJid, text, 'menu');
|
|
550
523
|
}
|
|
551
524
|
|
|
552
525
|
async sendPresence(jid: string, presence: 'composing' | 'recording' | 'paused') {
|
package/src/ui/menu.handler.ts
CHANGED
|
@@ -63,14 +63,14 @@ export class MenuHandler {
|
|
|
63
63
|
await this.whatsappService.stop();
|
|
64
64
|
ctx.ui.notify('WhatsApp Agent Disconnected', 'warning');
|
|
65
65
|
break;
|
|
66
|
-
case 'Logoff (Delete Session)': {
|
|
67
|
-
const confirmLogoff = await ctx.ui.confirm('Logoff', 'Delete all credentials?');
|
|
68
|
-
if (confirmLogoff) {
|
|
69
|
-
await this.whatsappService.logout();
|
|
70
|
-
ctx.ui.notify('Logged off and credentials deleted', 'info');
|
|
71
|
-
}
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
66
|
+
case 'Logoff (Delete Session)': {
|
|
67
|
+
const confirmLogoff = await ctx.ui.confirm('Logoff', 'Delete all credentials?');
|
|
68
|
+
if (confirmLogoff) {
|
|
69
|
+
await this.whatsappService.logout();
|
|
70
|
+
ctx.ui.notify('Logged off and credentials deleted', 'info');
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
74
|
case 'Allowed Numbers':
|
|
75
75
|
await this.manageAllowList(ctx);
|
|
76
76
|
break;
|
|
@@ -325,8 +325,7 @@ export class MenuHandler {
|
|
|
325
325
|
await this.sendPromptedMenuMessage(ctx, {
|
|
326
326
|
displayName: this.getConversationDisplayName(conversation),
|
|
327
327
|
senderNumber: conversation.senderNumber,
|
|
328
|
-
senderName: conversation.senderName
|
|
329
|
-
appendPiSuffix: false
|
|
328
|
+
senderName: conversation.senderName
|
|
330
329
|
});
|
|
331
330
|
}
|
|
332
331
|
|
|
@@ -334,8 +333,7 @@ export class MenuHandler {
|
|
|
334
333
|
await this.sendPromptedMenuMessage(ctx, {
|
|
335
334
|
displayName: this.formatAllowedContactOption(contact),
|
|
336
335
|
senderNumber: contact.number,
|
|
337
|
-
senderName: contact.name
|
|
338
|
-
appendPiSuffix: true
|
|
336
|
+
senderName: contact.name
|
|
339
337
|
});
|
|
340
338
|
}
|
|
341
339
|
|
|
@@ -345,10 +343,9 @@ export class MenuHandler {
|
|
|
345
343
|
displayName: string;
|
|
346
344
|
senderNumber: string;
|
|
347
345
|
senderName?: string;
|
|
348
|
-
appendPiSuffix: boolean;
|
|
349
346
|
}
|
|
350
347
|
) {
|
|
351
|
-
const { displayName, senderNumber, senderName
|
|
348
|
+
const { displayName, senderNumber, senderName } = options;
|
|
352
349
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
353
350
|
const inputText = (await ctx.ui.input(`Send a message to ${displayName}:`))?.trim() || '';
|
|
354
351
|
|
|
@@ -357,14 +354,13 @@ export class MenuHandler {
|
|
|
357
354
|
continue;
|
|
358
355
|
}
|
|
359
356
|
|
|
360
|
-
const
|
|
361
|
-
const result = await this.whatsappService.sendMenuMessage(this.toJid(senderNumber), messageText);
|
|
357
|
+
const result = await this.whatsappService.sendMenuMessage(this.toJid(senderNumber), inputText);
|
|
362
358
|
if (result.success) {
|
|
363
359
|
await this.recentsService.recordMessage({
|
|
364
360
|
messageId: result.messageId ?? `${Date.now()}`,
|
|
365
361
|
senderNumber,
|
|
366
362
|
senderName,
|
|
367
|
-
text:
|
|
363
|
+
text: inputText,
|
|
368
364
|
direction: 'outgoing',
|
|
369
365
|
timestamp: Date.now()
|
|
370
366
|
});
|