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 CHANGED
@@ -46,10 +46,10 @@ pi install npm:whatsapp-pi
46
46
  pi
47
47
  ```
48
48
 
49
- To automatically connect to WhatsApp on startup (if you are already authenticated):
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whatsapp-pi",
3
- "version": "1.0.45",
3
+ "version": "1.0.46",
4
4
  "type": "module",
5
5
  "description": "WhatsApp integration extension for Pi",
6
6
  "main": "whatsapp-pi.ts",
@@ -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
- // Note: Branding π is applied here to ensure consistency
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: 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
- const socket = this.getActiveSocket();
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') {
@@ -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, appendPiSuffix } = options;
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 messageText = appendPiSuffix ? `${inputText} π` : inputText;
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: messageText,
363
+ text: inputText,
368
364
  direction: 'outgoing',
369
365
  timestamp: Date.now()
370
366
  });