whatsapp-pi 1.0.56 → 1.0.58

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whatsapp-pi",
3
- "version": "1.0.56",
3
+ "version": "1.0.58",
4
4
  "type": "module",
5
5
  "description": "WhatsApp integration extension for Pi",
6
6
  "main": "whatsapp-pi.ts",
package/src/i18n.ts CHANGED
@@ -31,6 +31,8 @@ const fallback = {
31
31
  "service.whatsapp.connecting": "| WhatsApp: Connecting...",
32
32
  "service.whatsapp.typeToConnect": "| WhatsApp: type /whatsapp to connect",
33
33
  "service.whatsapp.connected": "| WhatsApp: Connected",
34
+ "service.whatsapp.qrConnected": "WhatsApp connected",
35
+ "service.whatsapp.qrWelcomeMessage": "👋 To get started, send a message, enter in /whatsapp > Recents to Allow Contact with LID code, then add the whatsapp number in the Allowed Contact list.",
34
36
  "service.whatsapp.sessionErrorBadMac": "| WhatsApp: Session Error (Bad MAC)",
35
37
  "service.whatsapp.loggedOut": "| WhatsApp: Logged out",
36
38
  "service.whatsapp.conflict": "| WhatsApp: Conflict (Another Instance)",
@@ -107,11 +109,6 @@ const fallback = {
107
109
  "menu.allowedGroups.group.history": "History",
108
110
  "menu.allowedGroups.group.sendMessage": "Send Message",
109
111
  "menu.allowedGroups.group.printGroup": "Print Group JID",
110
- "menu.allowedGroups.group.reactionMode": "Reaction Mode",
111
- "menu.allowedGroups.group.reactionMode.title": "Reaction Mode • {displayName}",
112
- "menu.allowedGroups.group.reactionMode.active": "Active",
113
- "menu.allowedGroups.group.reactionMode.passive": "Passive",
114
- "menu.allowedGroups.group.reactionMode.updated": "Reaction mode set to {mode} for {displayName}",
115
112
  "menu.allowedGroups.group.removeAlias": "Remove Alias",
116
113
  "menu.allowedGroups.group.addAlias": "Add Alias",
117
114
  "menu.allowedGroups.group.removeGroup": "Remove Group",
@@ -247,6 +244,8 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
247
244
  "service.whatsapp.connecting": "| WhatsApp: Conectando...",
248
245
  "service.whatsapp.typeToConnect": "| WhatsApp: digite /whatsapp para conectar",
249
246
  "service.whatsapp.connected": "| WhatsApp: Conectado",
247
+ "service.whatsapp.qrConnected": "WhatsApp conectado",
248
+ "service.whatsapp.qrWelcomeMessage": "👋 Para começar, envie uma mensagem, entre em /whatsapp > Recentes para Permitir o Contato com o código LID, depois adicione o número do whatsapp na lista de Contatos Permitidos.",
250
249
  "service.whatsapp.sessionErrorBadMac": "| WhatsApp: Erro de sessão (Bad MAC)",
251
250
  "service.whatsapp.loggedOut": "| WhatsApp: Desconectado",
252
251
  "service.whatsapp.conflict": "| WhatsApp: Conflito (Outra instância)",
@@ -323,11 +322,6 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
323
322
  "menu.allowedGroups.group.history": "Histórico",
324
323
  "menu.allowedGroups.group.sendMessage": "Enviar mensagem",
325
324
  "menu.allowedGroups.group.printGroup": "Mostrar JID do grupo",
326
- "menu.allowedGroups.group.reactionMode": "Modo de reação",
327
- "menu.allowedGroups.group.reactionMode.title": "Modo de reação • {displayName}",
328
- "menu.allowedGroups.group.reactionMode.active": "Ativo",
329
- "menu.allowedGroups.group.reactionMode.passive": "Passivo",
330
- "menu.allowedGroups.group.reactionMode.updated": "Modo de reação definido como {mode} para {displayName}",
331
325
  "menu.allowedGroups.group.removeAlias": "Remover apelido",
332
326
  "menu.allowedGroups.group.addAlias": "Adicionar apelido",
333
327
  "menu.allowedGroups.group.removeGroup": "Remover grupo",
@@ -429,6 +423,8 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
429
423
  "service.whatsapp.connecting": "| WhatsApp: Conectando...",
430
424
  "service.whatsapp.typeToConnect": "| WhatsApp: escribe /whatsapp para conectar",
431
425
  "service.whatsapp.connected": "| WhatsApp: Conectado",
426
+ "service.whatsapp.qrConnected": "WhatsApp conectado",
427
+ "service.whatsapp.qrWelcomeMessage": "👋 Para empezar, envía un mensaje, entra en /whatsapp > Recientes para Permitir el Contacto con el código LID, luego añade el número de whatsapp en la lista de Contactos Permitidos.",
432
428
  "service.whatsapp.sessionErrorBadMac": "| WhatsApp: Error de sesión (Bad MAC)",
433
429
  "service.whatsapp.loggedOut": "| WhatsApp: Cerrado sesión",
434
430
  "service.whatsapp.conflict": "| WhatsApp: Conflicto (Otra instancia)",
@@ -563,6 +559,8 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
563
559
  "service.whatsapp.connecting": "| WhatsApp: Connexion...",
564
560
  "service.whatsapp.typeToConnect": "| WhatsApp: tapez /whatsapp pour connecter",
565
561
  "service.whatsapp.connected": "| WhatsApp: Connecté",
562
+ "service.whatsapp.qrConnected": "WhatsApp connecté",
563
+ "service.whatsapp.qrWelcomeMessage": "👋 Pour commencer, envoyez un message, allez dans /whatsapp > Récents pour Autoriser le Contact avec le code LID, puis ajoutez le numéro whatsapp dans la liste des Contacts Autorisés.",
566
564
  "service.whatsapp.sessionErrorBadMac": "| WhatsApp: Erreur de session (Bad MAC)",
567
565
  "service.whatsapp.loggedOut": "| WhatsApp: Déconnecté",
568
566
  "service.whatsapp.conflict": "| WhatsApp: Conflit (Autre instance)",
@@ -10,8 +10,6 @@ export interface AllowList {
10
10
  numbers: string[];
11
11
  }
12
12
 
13
- export type ReactionMode = 'active' | 'passive';
14
-
15
13
  export interface IncomingMessage {
16
14
  id: string;
17
15
  remoteJid: string;
@@ -2,14 +2,13 @@ import { useMultiFileAuthState } from 'baileys';
2
2
  import { join } from 'path';
3
3
  import { readFile, writeFile, mkdir, rm, rename } from 'fs/promises';
4
4
  import { homedir } from 'os';
5
- import { SessionStatus, type ReactionMode } from '../models/whatsapp.types.js';
5
+ import { SessionStatus } from '../models/whatsapp.types.js';
6
6
  import { t } from '../i18n.js';
7
7
 
8
8
  export interface Contact {
9
9
  number: string;
10
10
  name?: string;
11
11
  sendNumber?: string;
12
- reactionMode?: ReactionMode;
13
12
  }
14
13
 
15
14
  export class SessionManager {
@@ -38,6 +37,7 @@ export class SessionManager {
38
37
  private hasAuthState = false;
39
38
  private openaiKey: string = '';
40
39
  private visionModel: string = 'gpt-4o';
40
+ private operatorJid: string = '';
41
41
 
42
42
  constructor(baseDir = join(homedir(), '.pi', 'whatsapp-pi')) {
43
43
  this.baseDir = baseDir;
@@ -74,11 +74,8 @@ export class SessionManager {
74
74
  num = num.number;
75
75
  }
76
76
  if (typeof num === 'string') {
77
- const reactionMode = item.reactionMode === 'active' || item.reactionMode === 'passive'
78
- ? item.reactionMode
79
- : undefined;
80
77
  const sendNumber = typeof item.sendNumber === 'string' ? item.sendNumber : undefined;
81
- return { number: num, name: item.name, sendNumber, reactionMode };
78
+ return { number: num, name: item.name, sendNumber };
82
79
  }
83
80
  }
84
81
  return null;
@@ -87,8 +84,6 @@ export class SessionManager {
87
84
  const loadedAllowList = (config.allowList || []).map(cleanContact).filter(Boolean) as Contact[];
88
85
  const loadedAllowedGroups = (config.allowedGroups || []).map(cleanContact).filter(Boolean) as Contact[];
89
86
  const migratedGroups = loadedAllowList.filter(c => SessionManager.isGroupJid(c.number));
90
- const needsReactionModeBackfill = loadedAllowedGroups.some(group => !group.reactionMode)
91
- || migratedGroups.some(group => !group.reactionMode);
92
87
  this.allowList = loadedAllowList.filter(c => !SessionManager.isGroupJid(c.number));
93
88
  this.allowedGroups = this.mergeContacts(loadedAllowedGroups, migratedGroups);
94
89
  this.ignoredNumbers = (config.ignoredNumbers || []).map(cleanContact).filter(Boolean) as Contact[];
@@ -96,8 +91,9 @@ export class SessionManager {
96
91
  this.hasAuthState = Boolean(config.hasAuthState);
97
92
  this.openaiKey = config.openaiKey || '';
98
93
  this.visionModel = config.visionModel || 'gpt-4o';
94
+ this.operatorJid = config.operatorJid || '';
99
95
 
100
- if (recovered || needsReactionModeBackfill) {
96
+ if (recovered) {
101
97
  await this.saveConfig();
102
98
  }
103
99
  } catch {
@@ -169,11 +165,19 @@ export class SessionManager {
169
165
  status: this.status,
170
166
  hasAuthState: this.hasAuthState,
171
167
  openaiKey: this.openaiKey,
172
- visionModel: this.visionModel
168
+ visionModel: this.visionModel,
169
+ operatorJid: this.operatorJid
173
170
  };
174
171
  await mkdir(this.baseDir, { recursive: true });
175
- await writeFile(tempPath, JSON.stringify(config, null, 2));
176
- await rename(tempPath, this.configPath);
172
+ const serialized = JSON.stringify(config, null, 2);
173
+ await writeFile(tempPath, serialized);
174
+ try {
175
+ await rename(tempPath, this.configPath);
176
+ } catch {
177
+ // Windows EPERM: atomic rename failed (file locked). Fall back to direct write.
178
+ await writeFile(this.configPath, serialized);
179
+ await rm(tempPath, { force: true }).catch(() => {});
180
+ }
177
181
  } catch (error) {
178
182
  await rm(tempPath, { force: true }).catch(() => {});
179
183
  console.error(t('session.manager.failedSaveConfig'), error);
@@ -248,7 +252,7 @@ export class SessionManager {
248
252
 
249
253
  const existing = this.allowedGroups.find(c => c.number === groupJid);
250
254
  if (!existing) {
251
- this.allowedGroups.push({ number: groupJid, name, reactionMode: 'active' });
255
+ this.allowedGroups.push({ number: groupJid, name });
252
256
  this.ignoredNumbers = this.ignoredNumbers.filter(c => c.number !== groupJid);
253
257
  await this.saveConfig();
254
258
  return;
@@ -258,11 +262,6 @@ export class SessionManager {
258
262
  existing.name = name;
259
263
  await this.saveConfig();
260
264
  }
261
-
262
- if (!existing.reactionMode) {
263
- existing.reactionMode = 'active';
264
- await this.saveConfig();
265
- }
266
265
  }
267
266
 
268
267
  async removeAllowedGroup(groupJid: string) {
@@ -324,20 +323,6 @@ export class SessionManager {
324
323
  await this.saveConfig();
325
324
  }
326
325
 
327
- getAllowedGroupReactionMode(groupJid: string): ReactionMode {
328
- return this.getAllowedGroup(groupJid)?.reactionMode || 'active';
329
- }
330
-
331
- async setAllowedGroupReactionMode(groupJid: string, reactionMode: ReactionMode) {
332
- const group = this.getAllowedGroup(groupJid);
333
- if (!group) {
334
- return;
335
- }
336
-
337
- group.reactionMode = reactionMode;
338
- await this.saveConfig();
339
- }
340
-
341
326
  async removeAllowedGroupAlias(groupJid: string) {
342
327
  const group = this.getAllowedGroup(groupJid);
343
328
  if (!group || !group.name) {
@@ -381,15 +366,9 @@ export class SessionManager {
381
366
  if (!existing.name && contact.name) {
382
367
  existing.name = contact.name;
383
368
  }
384
- if (!existing.reactionMode && contact.reactionMode) {
385
- existing.reactionMode = contact.reactionMode;
386
- }
387
369
  }
388
370
  }
389
- return merged.map(contact => ({
390
- ...contact,
391
- reactionMode: contact.reactionMode || 'active'
392
- }));
371
+ return merged;
393
372
  }
394
373
 
395
374
  public async isRegistered(): Promise<boolean> {
@@ -470,6 +449,15 @@ export class SessionManager {
470
449
  await this.saveConfig();
471
450
  }
472
451
 
452
+ getOperatorJid(): string {
453
+ return this.operatorJid;
454
+ }
455
+
456
+ async setOperatorJid(jid: string) {
457
+ this.operatorJid = jid;
458
+ await this.saveConfig();
459
+ }
460
+
473
461
  getAuthStateDir(): string {
474
462
  return this.authStateDir;
475
463
  }
@@ -46,12 +46,23 @@ interface IncomingMessageContextInfo {
46
46
  mentionedJid?: string[];
47
47
  }
48
48
 
49
+ interface IncomingMessageWithContext {
50
+ contextInfo?: IncomingMessageContextInfo;
51
+ }
52
+
49
53
  interface IncomingMessageContent {
50
54
  conversation?: string;
51
55
  extendedTextMessage?: {
52
56
  text?: string;
53
57
  contextInfo?: IncomingMessageContextInfo;
54
58
  };
59
+ imageMessage?: IncomingMessageWithContext;
60
+ videoMessage?: IncomingMessageWithContext;
61
+ documentMessage?: IncomingMessageWithContext;
62
+ audioMessage?: IncomingMessageWithContext;
63
+ stickerMessage?: IncomingMessageWithContext;
64
+ buttonsMessage?: IncomingMessageWithContext;
65
+ templateMessage?: IncomingMessageWithContext;
55
66
  }
56
67
 
57
68
  interface IncomingMessageLike {
@@ -112,6 +123,7 @@ export class WhatsAppService {
112
123
  private onMessage?: (m: MessagesUpsertEvent) => void;
113
124
  private onStatusUpdate?: (status: string) => void;
114
125
  private lastRemoteJid: string | null = null;
126
+ private qrWasShown = false;
115
127
  private boundGroupJid: string | null = null;
116
128
  private groupMetadataCache: Map<string, { id: string; subject: string; participants: Array<{ id: string }> }> = new Map();
117
129
 
@@ -203,24 +215,6 @@ export class WhatsAppService {
203
215
  return [...candidates];
204
216
  }
205
217
 
206
- private messageHasDirectMention(message: IncomingMessageLike): boolean {
207
- const mentionedJids = message.message?.extendedTextMessage?.contextInfo?.mentionedJid || [];
208
- if (mentionedJids.length === 0) {
209
- return false;
210
- }
211
-
212
- const agentCandidates = this.getAgentJidCandidates();
213
- if (agentCandidates.length === 0) {
214
- return false;
215
- }
216
-
217
- return mentionedJids.some(jid => {
218
- const normalizedMention = this.normalizeJidForComparison(jid);
219
- const mentionIdentity = this.normalizeJidIdentity(jid);
220
- return agentCandidates.includes(normalizedMention) || agentCandidates.includes(mentionIdentity);
221
- });
222
- }
223
-
224
218
  private getDisconnectStatusCode(error: unknown): number | undefined {
225
219
  if (!error || typeof error !== 'object') {
226
220
  return undefined;
@@ -406,6 +400,7 @@ export class WhatsAppService {
406
400
  this.sessionManager.setStatus('pairing');
407
401
  this.onQRCode?.(qr);
408
402
  this.onStatusUpdate?.(t('service.whatsapp.typeToConnect'));
403
+ this.qrWasShown = true;
409
404
  }
410
405
 
411
406
  private async handleConnectionOpen() {
@@ -420,6 +415,29 @@ export class WhatsAppService {
420
415
  await this.sessionManager.markAuthStateAvailable();
421
416
  this.sessionManager.setStatus('connected');
422
417
  this.onStatusUpdate?.(t('service.whatsapp.connected'));
418
+
419
+ if (this.qrWasShown) {
420
+ this.qrWasShown = false;
421
+ console.log(t('service.whatsapp.qrConnected'));
422
+ console.log(t('service.whatsapp.qrWelcomeMessage'));
423
+ void this.sendQrWelcome();
424
+ }
425
+ }
426
+
427
+ private async sendQrWelcome(): Promise<void> {
428
+ const rawId = this.socket?.user?.id;
429
+ if (!rawId) return;
430
+ const selfJid = this.normalizeJidForComparison(rawId);
431
+ await this.sessionManager.setOperatorJid(selfJid);
432
+ try {
433
+ await this.socket?.sendMessage(selfJid, { text: t('service.whatsapp.qrWelcomeMessage') });
434
+ } catch {
435
+ // Best-effort — welcome send failure must not abort the session.
436
+ }
437
+ }
438
+
439
+ public getOperatorJid(): string {
440
+ return this.sessionManager.getOperatorJid();
423
441
  }
424
442
 
425
443
  private isBadMacError(errorMessage: string): boolean {
@@ -564,10 +582,6 @@ export class WhatsAppService {
564
582
  void this.recordIncomingMessage(message, remoteJid, text);
565
583
 
566
584
  const pushName = message.pushName || undefined;
567
- const groupAllowed = isGroup && this.sessionManager.isAllowedGroup(remoteJid);
568
- const passiveGroupBlocked = groupAllowed
569
- && this.sessionManager.getAllowedGroupReactionMode(remoteJid) === 'passive'
570
- && !this.messageHasDirectMention(message);
571
585
 
572
586
  if (this.boundGroupJid) {
573
587
  if (!this.sessionManager.isAllowedGroup(this.boundGroupJid)) {
@@ -575,10 +589,6 @@ export class WhatsAppService {
575
589
  return;
576
590
  }
577
591
 
578
- if (this.sessionManager.getAllowedGroupReactionMode(this.boundGroupJid) === 'passive' && !this.messageHasDirectMention(message)) {
579
- return;
580
- }
581
-
582
592
  this.lastRemoteJid = remoteJid;
583
593
  this.onMessage?.(payload);
584
594
  return;
@@ -592,10 +602,6 @@ export class WhatsAppService {
592
602
  return;
593
603
  }
594
604
 
595
- if (passiveGroupBlocked) {
596
- return;
597
- }
598
-
599
605
  this.lastRemoteJid = remoteJid;
600
606
  this.onMessage?.(payload);
601
607
  }
@@ -291,12 +291,11 @@ export class MenuHandler {
291
291
  const historyLabel = t('menu.allowedGroups.group.history');
292
292
  const sendMessageLabel = t('menu.allowedGroups.group.sendMessage');
293
293
  const printGroupLabel = t('menu.allowedGroups.group.printGroup');
294
- const reactionModeLabel = t('menu.allowedGroups.group.reactionMode');
295
294
  const removeAliasLabel = t('menu.allowedGroups.group.removeAlias');
296
295
  const addAliasLabel = t('menu.allowedGroups.group.addAlias');
297
296
  const removeGroupLabel = t('menu.allowedGroups.group.removeGroup');
298
297
  const backLabel = t('menu.allowedGroups.group.back');
299
- const options = [historyLabel, sendMessageLabel, printGroupLabel, reactionModeLabel];
298
+ const options = [historyLabel, sendMessageLabel, printGroupLabel];
300
299
  if (group.name) {
301
300
  options.push(removeAliasLabel);
302
301
  } else {
@@ -324,12 +323,6 @@ export class MenuHandler {
324
323
  return;
325
324
  }
326
325
 
327
- if (choice === reactionModeLabel) {
328
- await this.manageAllowedGroupReactionMode(ctx, group);
329
- await this.manageAllowedGroup(ctx, group);
330
- return;
331
- }
332
-
333
326
  if (choice === addAliasLabel) {
334
327
  const alias = await ctx.ui.input(t('menu.allowedGroups.enterAlias', { groupJid: group.number }));
335
328
  const trimmedAlias = alias?.trim() || '';
@@ -514,30 +507,6 @@ export class MenuHandler {
514
507
  });
515
508
  }
516
509
 
517
- private async manageAllowedGroupReactionMode(ctx: ExtensionCommandContext, group: Contact) {
518
- const displayName = this.formatAllowedGroupOption(group);
519
- const title = t('menu.allowedGroups.group.reactionMode.title', { displayName });
520
- const activeLabel = t('menu.allowedGroups.group.reactionMode.active');
521
- const passiveLabel = t('menu.allowedGroups.group.reactionMode.passive');
522
- const backLabel = t('menu.allowedGroups.group.back');
523
- const currentMode = this.sessionManager.getAllowedGroupReactionMode(group.number);
524
- const options = [activeLabel, passiveLabel, backLabel];
525
-
526
- const choice = await ctx.ui.select(title, options);
527
-
528
- if (choice === backLabel || !choice) {
529
- return;
530
- }
531
-
532
- if (choice === activeLabel || choice === passiveLabel) {
533
- const nextMode = choice === activeLabel ? 'active' : 'passive';
534
- if (currentMode !== nextMode) {
535
- await this.sessionManager.setAllowedGroupReactionMode(group.number, nextMode);
536
- }
537
- ctx.ui.notify(t('menu.allowedGroups.group.reactionMode.updated', { displayName, mode: choice }), 'info');
538
- }
539
- }
540
-
541
510
  private async sendPromptedMenuMessage(
542
511
  ctx: ExtensionCommandContext,
543
512
  options: {
package/whatsapp-pi.ts CHANGED
@@ -268,8 +268,8 @@ export default function (pi: ExtensionAPI) {
268
268
  message: Type.String({ minLength: 1, description: "Plain-text message content to send" })
269
269
  }),
270
270
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
271
- // Resolve JID: jid > recipient_jid > lastRemoteJid
272
- const resolvedJid = params.jid || params.recipient_jid || whatsappService.getLastRemoteJid();
271
+ // Resolve JID: jid > recipient_jid > lastRemoteJid > operatorJid (QR-scanned number)
272
+ const resolvedJid = params.jid || params.recipient_jid || whatsappService.getLastRemoteJid() || whatsappService.getOperatorJid();
273
273
  if (!resolvedJid) {
274
274
  return {
275
275
  isError: true,