whatsapp-pi 1.0.44 → 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.44",
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 {
@@ -136,10 +136,11 @@ export class SessionManager {
136
136
  public async saveConfig() {
137
137
  const tempPath = `${this.configPath}.${process.pid}.${Date.now()}.tmp`;
138
138
  try {
139
+ this.hasAuthState = this.hasAuthState || await this.hasCredentialsFile();
139
140
  const config = {
140
141
  allowList: this.allowList,
141
- blockList: this.blockList,
142
- ignoredNumbers: this.ignoredNumbers,
142
+ blockList: this.blockList,
143
+ ignoredNumbers: this.ignoredNumbers,
143
144
  status: this.status,
144
145
  hasAuthState: this.hasAuthState,
145
146
  openaiKey: this.openaiKey,
@@ -72,10 +72,14 @@ interface BoomLikeError {
72
72
  }
73
73
 
74
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
+
75
78
  private socket?: WhatsAppSocketLike;
76
79
  private sessionManager: SessionManager;
77
80
  private messageSender: MessageSender;
78
81
  private isReconnecting = false;
82
+ private reconnectAttempts = 0;
79
83
  private verboseMode = false;
80
84
  private onIncomingMessageRecorded?: (message: IncomingMessage) => void | Promise<void>;
81
85
  private saveCreds?: () => Promise<void>;
@@ -171,6 +175,11 @@ export class WhatsAppService {
171
175
  }
172
176
  }
173
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
+
174
183
  private cleanupSocket() {
175
184
  this.clearReconnectTimeout();
176
185
 
@@ -295,7 +304,7 @@ export class WhatsAppService {
295
304
  private async handlePairingQr(qr: string) {
296
305
  this.sessionManager.setStatus('pairing');
297
306
  this.onQRCode?.(qr);
298
- this.onStatusUpdate?.('| WhatsApp: Disconnected');
307
+ this.onStatusUpdate?.('| WhatsApp: Disconnected');
299
308
  }
300
309
 
301
310
  private async handleConnectionOpen() {
@@ -304,6 +313,7 @@ export class WhatsAppService {
304
313
  }
305
314
 
306
315
  this.isReconnecting = false;
316
+ this.reconnectAttempts = 0;
307
317
  this.clearReconnectTimeout();
308
318
  await this.saveCreds?.();
309
319
  await this.sessionManager.markAuthStateAvailable();
@@ -339,45 +349,55 @@ export class WhatsAppService {
339
349
  console.error(`Connection closed [${statusCode}]. Reconnecting: ${shouldReconnect}`);
340
350
  }
341
351
 
342
- if (shouldTreatAsLoggedOut) {
343
- if (this.verboseMode) {
344
- console.error(`Session rejected [${statusCode}] - preserving auth state`);
345
- }
346
- if (isBadMac) {
347
- if (this.verboseMode) {
348
- console.error('[WhatsApp-Pi] Bad MAC error detected. Your session keys are corrupted.');
349
- console.error('[WhatsApp-Pi] Use Logoff (Delete Session) only if reconnect keeps failing.');
350
- }
351
- this.onStatusUpdate?.('| WhatsApp: Session Error (Bad MAC)');
352
- } else if (isAuthRejected && allowPairingOnAuthFailure) {
353
- this.onStatusUpdate?.('| WhatsApp: Session Preserved (Reconnect Failed)');
354
- }
355
- this.cleanupSocket();
356
- this.isReconnecting = false;
357
- await this.sessionManager.setStatus('disconnected');
358
- if (!isBadMac) {
359
- this.onStatusUpdate?.('| WhatsApp: Disconnected');
360
- }
361
- return;
362
- }
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
+ }
363
374
 
364
375
  if (statusCode === DisconnectReason.connectionReplaced) {
365
376
  if (this.verboseMode) {
366
377
  console.error('Connection replaced - another instance connected');
367
378
  }
379
+ this.cleanupSocket();
380
+ this.isReconnecting = false;
381
+ this.reconnectAttempts = 0;
382
+ await this.sessionManager.setStatus('disconnected');
368
383
  this.onStatusUpdate?.('| WhatsApp: Conflict (Another Instance)');
369
384
  return;
370
385
  }
371
386
 
372
387
  if (shouldReconnect && !this.isReconnecting) {
373
388
  this.isReconnecting = true;
389
+ this.reconnectAttempts++;
390
+ const reconnectDelayMs = this.getReconnectDelayMs();
374
391
  this.onStatusUpdate?.('| WhatsApp: Reconnecting...');
375
392
  this.clearReconnectTimeout();
393
+ await this.saveCreds?.();
394
+ this.cleanupSocket();
376
395
  this.reconnectTimeout = setTimeout(() => {
377
396
  this.isReconnecting = false;
378
397
  void this.start(options);
379
- }, 3000);
398
+ }, reconnectDelayMs);
380
399
  } else if (!shouldReconnect) {
400
+ this.reconnectAttempts = 0;
381
401
  this.sessionManager.setStatus('logged-out');
382
402
  this.onStatusUpdate?.('| WhatsApp: Disconnected');
383
403
  }
@@ -477,13 +497,14 @@ export class WhatsAppService {
477
497
  return this.socket;
478
498
  }
479
499
 
480
- async sendMessage(jid: string, text: string) {
500
+ async sendMessage(jid: string, text: string, origin: 'agent' | 'menu' = 'agent') {
481
501
  // Ensure we show the typing indicator before sending
482
502
  await this.sendPresence(jid, 'composing');
483
503
 
484
504
  const result = await this.messageSender.send({
485
505
  recipientJid: jid,
486
- text: text
506
+ text,
507
+ origin
487
508
  });
488
509
 
489
510
  // After sending, we can stop the typing indicator
@@ -498,35 +519,7 @@ export class WhatsAppService {
498
519
 
499
520
  async sendMenuMessage(jid: string, text: string) {
500
521
  const normalizedJid = this.normalizeRecipientJid(jid);
501
- const socket = this.getActiveSocket();
502
-
503
- if (!socket) {
504
- return {
505
- success: false,
506
- error: 'WhatsApp is not connected',
507
- attempts: 0
508
- };
509
- }
510
-
511
- try {
512
- await this.sendPresence(normalizedJid, 'composing');
513
- const response = await socket.sendMessage(normalizedJid, { text });
514
- await this.sendPresence(normalizedJid, 'paused');
515
-
516
- return {
517
- success: true,
518
- messageId: response?.key?.id,
519
- attempts: 1
520
- };
521
- } catch (error: unknown) {
522
- await this.sendPresence(normalizedJid, 'paused');
523
- console.error(`Failed to send menu message to ${normalizedJid}:`, error);
524
- return {
525
- success: false,
526
- error: error instanceof Error ? error.message : 'Unknown error',
527
- attempts: 1
528
- };
529
- }
522
+ return this.sendMessage(normalizedJid, text, 'menu');
530
523
  }
531
524
 
532
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
  });
package/whatsapp-pi.ts CHANGED
@@ -60,7 +60,7 @@ export default function (pi: ExtensionAPI) {
60
60
  };
61
61
 
62
62
  // Initial status setup
63
- pi.on("session_start", async (_event, ctx) => {
63
+ pi.on("session_start", async (_event, ctx) => {
64
64
  _ctx = ctx;
65
65
  // Check verbose mode
66
66
  const isVerboseFlagSet = process.argv.includes("--verbose");
@@ -86,10 +86,10 @@ export default function (pi: ExtensionAPI) {
86
86
  await whatsappService.stop();
87
87
  }
88
88
  };
89
- whatsappService.setIncomingMessageRecorder(async (message) => {
90
- await recentsService.recordMessage({
91
- messageId: message.id,
92
- senderNumber: `+${message.remoteJid.split('@')[0]}`,
89
+ whatsappService.setIncomingMessageRecorder(async (message) => {
90
+ await recentsService.recordMessage({
91
+ messageId: message.id,
92
+ senderNumber: `+${message.remoteJid.split('@')[0]}`,
93
93
  senderName: message.pushName,
94
94
  text: message.text || '',
95
95
  direction: 'incoming',
@@ -97,20 +97,20 @@ export default function (pi: ExtensionAPI) {
97
97
  });
98
98
  });
99
99
 
100
- const savedStateEntry = [...ctx.sessionManager.getEntries()]
101
- .reverse()
102
- .find(entry => entry.type === "custom" && entry.customType === "whatsapp-state");
103
- const isWhatsappPiOn = pi.getFlag("whatsapp-pi-online") === true;
104
- const registered = await sessionManager.isRegistered();
105
-
106
- if (savedStateEntry) {
107
- const data = (savedStateEntry as { data?: any }).data;
108
- if (data.status) {
109
- const restoredStatus = data.status === 'connected' && !(isWhatsappPiOn && registered)
110
- ? 'disconnected'
111
- : data.status;
112
- await sessionManager.setStatus(restoredStatus);
113
- }
100
+ const savedStateEntry = [...ctx.sessionManager.getEntries()]
101
+ .reverse()
102
+ .find(entry => entry.type === "custom" && entry.customType === "whatsapp-state");
103
+ const isWhatsappPiOn = pi.getFlag("whatsapp-pi-online") === true;
104
+ const registered = await sessionManager.isRegistered();
105
+
106
+ if (savedStateEntry) {
107
+ const data = (savedStateEntry as { data?: any }).data;
108
+ if (data.status) {
109
+ const restoredStatus = data.status === 'connected' && !(isWhatsappPiOn && registered)
110
+ ? 'disconnected'
111
+ : data.status;
112
+ await sessionManager.setStatus(restoredStatus);
113
+ }
114
114
  if (Array.isArray(data.allowList)) {
115
115
  for (const n of data.allowList) {
116
116
  const num = typeof n === "string" ? n : n.number;
@@ -118,10 +118,10 @@ export default function (pi: ExtensionAPI) {
118
118
  await sessionManager.addNumber(num, name);
119
119
  }
120
120
  }
121
- }
122
-
123
- if (isWhatsappPiOn && registered) {
124
- ctx.ui.setStatus('whatsapp', '| WhatsApp: Auto-connecting...');
121
+ }
122
+
123
+ if (isWhatsappPiOn && registered) {
124
+ ctx.ui.setStatus('whatsapp', '| WhatsApp: Auto-connecting...');
125
125
 
126
126
  // Retry logic (max 3 attempts, 3s delay)
127
127
  let attempts = 0;
@@ -131,7 +131,7 @@ export default function (pi: ExtensionAPI) {
131
131
  attempts++;
132
132
  try {
133
133
  await whatsappService.start({ allowPairingOnAuthFailure: false });
134
- } catch {
134
+ } catch {
135
135
  if (attempts < maxAttempts) {
136
136
  ctx.ui.notify(`WhatsApp: Connection attempt ${attempts} failed. Retrying...`, 'warning');
137
137
  setTimeout(tryConnect, 3000);
@@ -143,11 +143,11 @@ export default function (pi: ExtensionAPI) {
143
143
  };
144
144
 
145
145
  await tryConnect();
146
- } else if (isWhatsappPiOn) {
147
- ctx.ui.notify('WhatsApp: Auto-connect requested, but no saved WhatsApp credentials were found. Use Connect WhatsApp once to scan the QR code.', 'warning');
148
- } else {
149
- ctx.ui.notify('WhatsApp: Use Connect / Reconnect WhatsApp. QR code will appear only if pairing is needed.', 'info');
150
- }
146
+ } else if (isWhatsappPiOn) {
147
+ ctx.ui.notify('WhatsApp: Auto-connect requested, but no saved WhatsApp credentials were found. Use Connect WhatsApp once to scan the QR code.', 'warning');
148
+ } else {
149
+ ctx.ui.notify('WhatsApp: Use Connect / Reconnect WhatsApp. QR code will appear only if pairing is needed.', 'info');
150
+ }
151
151
 
152
152
  ctx.ui.notify('WhatsApp: Session reset via /new is now fully supported.', 'info');
153
153
 
@@ -157,7 +157,7 @@ export default function (pi: ExtensionAPI) {
157
157
  if (code !== 0 && code !== 99) { // 99 is a common exit code for -v in some versions
158
158
  throw new Error(`pdftotext returned code ${code}`);
159
159
  }
160
- } catch {
160
+ } catch {
161
161
  ctx.ui.notify('WhatsApp: pdftotext not found. PDF document support will be limited to storage only.', 'warning');
162
162
  logger.warn('[WhatsApp-Pi] Warning: pdftotext not found in system PATH.');
163
163
  }
@@ -333,7 +333,7 @@ export default function (pi: ExtensionAPI) {
333
333
  } else {
334
334
  ctx.ui.notify(`Failed to send WhatsApp reply`, 'error');
335
335
  }
336
- } catch {
336
+ } catch {
337
337
  ctx.ui.notify(`Failed to send WhatsApp reply`, 'error');
338
338
  }
339
339
  }