whatsapp-pi 1.0.51 → 1.0.53
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 +8 -7
- package/package.json +2 -1
- package/src/i18n.ts +14 -0
- package/src/models/whatsapp.types.ts +2 -0
- package/src/services/incoming-media.service.ts +42 -0
- package/src/services/session.manager.ts +40 -7
- package/src/services/whatsapp.service.ts +63 -1
- package/src/ui/menu.handler.ts +32 -1
- package/whatsapp-pi.ts +0 -11
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Pi is a powerful agentic AI coding assistant that operates in your terminal. Thi
|
|
|
19
19
|
- Manage aliases and print allowed contacts from the menu
|
|
20
20
|
- **Allowed Groups**: Control which WhatsApp groups can interact with Pi
|
|
21
21
|
- Add group JIDs with optional aliases
|
|
22
|
+
- Choose reaction mode per group: **Active** (reply to all allowed group messages) or **Passive** (reply only when mentioned with @)
|
|
22
23
|
- Only groups in Allowed Groups are processed by the agent
|
|
23
24
|
- **Recents & History**: Browse recent conversations, inspect full message history, and reply from message detail view
|
|
24
25
|
- **Reliable Messaging**: Queue-based message sending with retry logic
|
|
@@ -27,7 +28,7 @@ Pi is a powerful agentic AI coding assistant that operates in your terminal. Thi
|
|
|
27
28
|
- **Media Support**:
|
|
28
29
|
- **Vision Analysis**: Automatically forwards WhatsApp images to Pi for analysis.
|
|
29
30
|
- **Audio Transcription**: Transcribes voice notes when Whisper is installed.
|
|
30
|
-
- **Document Handling**: Downloads and stores documents (PDF, text) for agent access.
|
|
31
|
+
- **Document Handling**: Downloads and stores documents (PDF, text) for agent access; PDFs include a bounded text preview when readable.
|
|
31
32
|
|
|
32
33
|
## Prerequisites
|
|
33
34
|
|
|
@@ -36,10 +37,8 @@ To enable audio transcription features:
|
|
|
36
37
|
python -m pip install -U openai-whisper
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- **macOS**: `brew install poppler`
|
|
42
|
-
- **Windows**: Install `poppler` (e.g., via Scoop) and add to PATH.
|
|
40
|
+
PDF documents are parsed locally and do not require extra system utilities.
|
|
41
|
+
If a PDF cannot be parsed automatically, it is still saved and forwarded with a clear fallback notice.
|
|
43
42
|
|
|
44
43
|
## Quick Start
|
|
45
44
|
|
|
@@ -126,7 +125,8 @@ pi -e whatsapp-pi.ts --whatsapp-pi-online
|
|
|
126
125
|
|
|
127
126
|
### Allowed Groups Management
|
|
128
127
|
- **Add Group** - Add a WhatsApp group JID to the allowed groups list (format: 120363012345@g.us)
|
|
129
|
-
- **Select a group** - Open a submenu with **History**, **Send Message**, **Print Group JID**, alias actions, **Remove Group**, and **Back**
|
|
128
|
+
- **Select a group** - Open a submenu with **History**, **Send Message**, **Print Group JID**, **Reaction Mode**, alias actions, **Remove Group**, and **Back**
|
|
129
|
+
- **Reaction Mode** - Switch between **Active** and **Passive** behavior for that group
|
|
130
130
|
- **Back** - Return to main menu
|
|
131
131
|
|
|
132
132
|
### Recents Management
|
|
@@ -162,9 +162,10 @@ npm test
|
|
|
162
162
|
|
|
163
163
|
- **Auto-Connect Support**: Use the `--whatsapp-pi-online` flag to connect on startup when credentials already exist.
|
|
164
164
|
- **Group-Only Mode**: Use `--whatsapp-group <jid>` to bind Pi to a single WhatsApp group. The group must also be present in Allowed Groups.
|
|
165
|
+
- **Allowed Group Reaction Mode**: Each allowed group can be set to Active or Passive. Passive mode only replies when the bot is directly mentioned with @.
|
|
165
166
|
- **Recents Store**: Recent conversations and message history are persisted in `~/.pi/whatsapp-pi/recents/recents.json`.
|
|
166
167
|
- **Message Detail / Reply**: Open a message from history to inspect full content and reply with `R`.
|
|
167
|
-
- **Media Support**: Images are forwarded for vision analysis, audio is transcribed with Whisper, and
|
|
168
|
+
- **Media Support**: Images are forwarded for vision analysis, audio is transcribed with Whisper, and PDFs are saved under `./.pi-data/whatsapp/documents/` with local text preview when available.
|
|
168
169
|
- **Session Handling**: Saved state, allow list, and startup reconnects are restored automatically when available.
|
|
169
170
|
- **Intelligent Message Filtering**: Messages ending with `π` are ignored to prevent bot loops.
|
|
170
171
|
- **Storage Management**: Persistent data lives under `.pi-data/` plus the recents store in the user home directory.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whatsapp-pi",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.53",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WhatsApp integration extension for Pi",
|
|
6
6
|
"main": "whatsapp-pi.ts",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"typecheck": "tsc --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@llamaindex/liteparse": "^1.5.3",
|
|
35
36
|
"baileys": "^6.7.21",
|
|
36
37
|
"pino": "^10.3.1",
|
|
37
38
|
"qrcode-terminal": "^0.12.0"
|
package/src/i18n.ts
CHANGED
|
@@ -102,6 +102,11 @@ const fallback = {
|
|
|
102
102
|
"menu.allowedGroups.group.history": "History",
|
|
103
103
|
"menu.allowedGroups.group.sendMessage": "Send Message",
|
|
104
104
|
"menu.allowedGroups.group.printGroup": "Print Group JID",
|
|
105
|
+
"menu.allowedGroups.group.reactionMode": "Reaction Mode",
|
|
106
|
+
"menu.allowedGroups.group.reactionMode.title": "Reaction Mode • {displayName}",
|
|
107
|
+
"menu.allowedGroups.group.reactionMode.active": "Active",
|
|
108
|
+
"menu.allowedGroups.group.reactionMode.passive": "Passive",
|
|
109
|
+
"menu.allowedGroups.group.reactionMode.updated": "Reaction mode set to {mode} for {displayName}",
|
|
105
110
|
"menu.allowedGroups.group.removeAlias": "Remove Alias",
|
|
106
111
|
"menu.allowedGroups.group.addAlias": "Add Alias",
|
|
107
112
|
"menu.allowedGroups.group.removeGroup": "Remove Group",
|
|
@@ -197,6 +202,8 @@ const fallback = {
|
|
|
197
202
|
"incoming.media.documentSize": "Size: {size}",
|
|
198
203
|
"incoming.media.documentLocation": "Location: {relativePath}",
|
|
199
204
|
"incoming.media.documentDescription": "Description: {caption}",
|
|
205
|
+
"incoming.media.documentPdfPreviewHeading": "PDF text preview:",
|
|
206
|
+
"incoming.media.documentPdfFallbackNotice": "PDF text was not extracted automatically. The file is saved at the path above.",
|
|
200
207
|
"incoming.media.documentDownloadFailed": "[WhatsApp-Pi] Failed to download document:",
|
|
201
208
|
"incoming.media.documentDownloadFailedText": "[Document: {fileName} (download failed)]",
|
|
202
209
|
"audio.emptyTranscription": "[Empty transcription]",
|
|
@@ -304,6 +311,11 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
|
|
|
304
311
|
"menu.allowedGroups.group.history": "Histórico",
|
|
305
312
|
"menu.allowedGroups.group.sendMessage": "Enviar mensagem",
|
|
306
313
|
"menu.allowedGroups.group.printGroup": "Mostrar JID do grupo",
|
|
314
|
+
"menu.allowedGroups.group.reactionMode": "Modo de reação",
|
|
315
|
+
"menu.allowedGroups.group.reactionMode.title": "Modo de reação • {displayName}",
|
|
316
|
+
"menu.allowedGroups.group.reactionMode.active": "Ativo",
|
|
317
|
+
"menu.allowedGroups.group.reactionMode.passive": "Passivo",
|
|
318
|
+
"menu.allowedGroups.group.reactionMode.updated": "Modo de reação definido como {mode} para {displayName}",
|
|
307
319
|
"menu.allowedGroups.group.removeAlias": "Remover apelido",
|
|
308
320
|
"menu.allowedGroups.group.addAlias": "Adicionar apelido",
|
|
309
321
|
"menu.allowedGroups.group.removeGroup": "Remover grupo",
|
|
@@ -375,6 +387,8 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
|
|
|
375
387
|
"incoming.media.documentSize": "Tamanho: {size}",
|
|
376
388
|
"incoming.media.documentLocation": "Local: {relativePath}",
|
|
377
389
|
"incoming.media.documentDescription": "Descrição: {caption}",
|
|
390
|
+
"incoming.media.documentPdfPreviewHeading": "Prévia do texto do PDF:",
|
|
391
|
+
"incoming.media.documentPdfFallbackNotice": "O texto do PDF não pôde ser extraído automaticamente. O arquivo foi salvo no caminho acima.",
|
|
378
392
|
"incoming.media.documentDownloadFailed": "[WhatsApp-Pi] Falha ao baixar documento:",
|
|
379
393
|
"incoming.media.documentDownloadFailedText": "[Documento: {fileName} (download falhou)]",
|
|
380
394
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { downloadContentFromMessage } from 'baileys';
|
|
2
2
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { LiteParse } from '@llamaindex/liteparse';
|
|
4
5
|
import { AudioService } from './audio.service.js';
|
|
5
6
|
import type { IncomingResolution } from './incoming-message.resolver.js';
|
|
6
7
|
import { WhatsAppPiLogger } from './whatsapp-pi.logger.js';
|
|
@@ -12,7 +13,11 @@ export interface ProcessedIncomingContent {
|
|
|
12
13
|
imageMimeType?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
const PDF_PREVIEW_LIMIT = 1200;
|
|
17
|
+
|
|
15
18
|
export class IncomingMediaService {
|
|
19
|
+
private readonly pdfParser = new LiteParse({ ocrEnabled: true });
|
|
20
|
+
|
|
16
21
|
constructor(
|
|
17
22
|
private readonly audioService: AudioService,
|
|
18
23
|
private readonly logger = new WhatsAppPiLogger(false)
|
|
@@ -80,6 +85,15 @@ export class IncomingMediaService {
|
|
|
80
85
|
+ t('incoming.media.documentSize', { size: this.formatFileSize(fileSize) }) + '\n'
|
|
81
86
|
+ t('incoming.media.documentLocation', { relativePath });
|
|
82
87
|
|
|
88
|
+
if (this.isPdfDocument(fileName, mimeType)) {
|
|
89
|
+
const preview = await this.extractPdfPreview(buffer);
|
|
90
|
+
if (preview) {
|
|
91
|
+
text += `\n\n${t('incoming.media.documentPdfPreviewHeading')}\n${preview}`;
|
|
92
|
+
} else {
|
|
93
|
+
text += `\n\n${t('incoming.media.documentPdfFallbackNotice')}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
if (documentMessage.caption) {
|
|
84
98
|
text += `\n\n${t('incoming.media.documentDescription', { caption: documentMessage.caption })}`;
|
|
85
99
|
}
|
|
@@ -91,6 +105,34 @@ export class IncomingMediaService {
|
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
|
|
108
|
+
private async extractPdfPreview(buffer: Buffer): Promise<string | null> {
|
|
109
|
+
try {
|
|
110
|
+
const result = await this.pdfParser.parse(buffer);
|
|
111
|
+
return this.formatPdfPreview(result.text);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.logger.warn('[WhatsApp-Pi] PDF parsing failed, falling back to storage-only behavior.', error);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private formatPdfPreview(text: string | undefined | null): string | null {
|
|
119
|
+
const normalized = (text || '').replace(/\r\n/g, '\n').trim();
|
|
120
|
+
if (!normalized) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (normalized.length <= PDF_PREVIEW_LIMIT) {
|
|
125
|
+
return normalized;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return `${normalized.slice(0, PDF_PREVIEW_LIMIT)}…`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private isPdfDocument(fileName: string, mimeType: string): boolean {
|
|
132
|
+
const normalizedMimeType = mimeType.toLowerCase().split(';')[0].trim();
|
|
133
|
+
return normalizedMimeType === 'application/pdf' || fileName.toLowerCase().endsWith('.pdf');
|
|
134
|
+
}
|
|
135
|
+
|
|
94
136
|
private async downloadMessage(message: any, type: 'image' | 'document'): Promise<Buffer> {
|
|
95
137
|
const stream = await downloadContentFromMessage(message, type);
|
|
96
138
|
let buffer = Buffer.from([]);
|
|
@@ -2,12 +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 } from '../models/whatsapp.types.js';
|
|
5
|
+
import { SessionStatus, type ReactionMode } 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
|
+
reactionMode?: ReactionMode;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export class SessionManager {
|
|
@@ -72,7 +73,10 @@ export class SessionManager {
|
|
|
72
73
|
num = num.number;
|
|
73
74
|
}
|
|
74
75
|
if (typeof num === 'string') {
|
|
75
|
-
|
|
76
|
+
const reactionMode = item.reactionMode === 'active' || item.reactionMode === 'passive'
|
|
77
|
+
? item.reactionMode
|
|
78
|
+
: undefined;
|
|
79
|
+
return { number: num, name: item.name, reactionMode };
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
return null;
|
|
@@ -81,6 +85,8 @@ export class SessionManager {
|
|
|
81
85
|
const loadedAllowList = (config.allowList || []).map(cleanContact).filter(Boolean) as Contact[];
|
|
82
86
|
const loadedAllowedGroups = (config.allowedGroups || []).map(cleanContact).filter(Boolean) as Contact[];
|
|
83
87
|
const migratedGroups = loadedAllowList.filter(c => SessionManager.isGroupJid(c.number));
|
|
88
|
+
const needsReactionModeBackfill = loadedAllowedGroups.some(group => !group.reactionMode)
|
|
89
|
+
|| migratedGroups.some(group => !group.reactionMode);
|
|
84
90
|
this.allowList = loadedAllowList.filter(c => !SessionManager.isGroupJid(c.number));
|
|
85
91
|
this.allowedGroups = this.mergeContacts(loadedAllowedGroups, migratedGroups);
|
|
86
92
|
this.ignoredNumbers = (config.ignoredNumbers || []).map(cleanContact).filter(Boolean) as Contact[];
|
|
@@ -89,7 +95,7 @@ export class SessionManager {
|
|
|
89
95
|
this.openaiKey = config.openaiKey || '';
|
|
90
96
|
this.visionModel = config.visionModel || 'gpt-4o';
|
|
91
97
|
|
|
92
|
-
if (recovered) {
|
|
98
|
+
if (recovered || needsReactionModeBackfill) {
|
|
93
99
|
await this.saveConfig();
|
|
94
100
|
}
|
|
95
101
|
} catch {
|
|
@@ -240,7 +246,7 @@ export class SessionManager {
|
|
|
240
246
|
|
|
241
247
|
const existing = this.allowedGroups.find(c => c.number === groupJid);
|
|
242
248
|
if (!existing) {
|
|
243
|
-
this.allowedGroups.push({ number: groupJid, name });
|
|
249
|
+
this.allowedGroups.push({ number: groupJid, name, reactionMode: 'active' });
|
|
244
250
|
this.ignoredNumbers = this.ignoredNumbers.filter(c => c.number !== groupJid);
|
|
245
251
|
await this.saveConfig();
|
|
246
252
|
return;
|
|
@@ -250,6 +256,11 @@ export class SessionManager {
|
|
|
250
256
|
existing.name = name;
|
|
251
257
|
await this.saveConfig();
|
|
252
258
|
}
|
|
259
|
+
|
|
260
|
+
if (!existing.reactionMode) {
|
|
261
|
+
existing.reactionMode = 'active';
|
|
262
|
+
await this.saveConfig();
|
|
263
|
+
}
|
|
253
264
|
}
|
|
254
265
|
|
|
255
266
|
async removeAllowedGroup(groupJid: string) {
|
|
@@ -297,6 +308,20 @@ export class SessionManager {
|
|
|
297
308
|
await this.saveConfig();
|
|
298
309
|
}
|
|
299
310
|
|
|
311
|
+
getAllowedGroupReactionMode(groupJid: string): ReactionMode {
|
|
312
|
+
return this.getAllowedGroup(groupJid)?.reactionMode || 'active';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async setAllowedGroupReactionMode(groupJid: string, reactionMode: ReactionMode) {
|
|
316
|
+
const group = this.getAllowedGroup(groupJid);
|
|
317
|
+
if (!group) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
group.reactionMode = reactionMode;
|
|
322
|
+
await this.saveConfig();
|
|
323
|
+
}
|
|
324
|
+
|
|
300
325
|
async removeAllowedGroupAlias(groupJid: string) {
|
|
301
326
|
const group = this.getAllowedGroup(groupJid);
|
|
302
327
|
if (!group || !group.name) {
|
|
@@ -336,11 +361,19 @@ export class SessionManager {
|
|
|
336
361
|
const existing = merged.find(c => c.number === contact.number);
|
|
337
362
|
if (!existing) {
|
|
338
363
|
merged.push(contact);
|
|
339
|
-
} else
|
|
340
|
-
existing.name
|
|
364
|
+
} else {
|
|
365
|
+
if (!existing.name && contact.name) {
|
|
366
|
+
existing.name = contact.name;
|
|
367
|
+
}
|
|
368
|
+
if (!existing.reactionMode && contact.reactionMode) {
|
|
369
|
+
existing.reactionMode = contact.reactionMode;
|
|
370
|
+
}
|
|
341
371
|
}
|
|
342
372
|
}
|
|
343
|
-
return merged
|
|
373
|
+
return merged.map(contact => ({
|
|
374
|
+
...contact,
|
|
375
|
+
reactionMode: contact.reactionMode || 'active'
|
|
376
|
+
}));
|
|
344
377
|
}
|
|
345
378
|
|
|
346
379
|
public async isRegistered(): Promise<boolean> {
|
|
@@ -42,9 +42,16 @@ interface IncomingMessageKey {
|
|
|
42
42
|
participant?: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
interface IncomingMessageContextInfo {
|
|
46
|
+
mentionedJid?: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
45
49
|
interface IncomingMessageContent {
|
|
46
50
|
conversation?: string;
|
|
47
|
-
extendedTextMessage?: {
|
|
51
|
+
extendedTextMessage?: {
|
|
52
|
+
text?: string;
|
|
53
|
+
contextInfo?: IncomingMessageContextInfo;
|
|
54
|
+
};
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
interface IncomingMessageLike {
|
|
@@ -59,6 +66,7 @@ interface MessagesUpsertEvent {
|
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
interface WhatsAppSocketLike {
|
|
69
|
+
user?: { id?: string; lid?: string };
|
|
62
70
|
ev: {
|
|
63
71
|
on(event: 'connection.update', handler: (update: ConnectionUpdateEvent) => void | Promise<void>): void;
|
|
64
72
|
on(event: 'creds.update', handler: () => void | Promise<void>): void;
|
|
@@ -170,6 +178,48 @@ export class WhatsAppService {
|
|
|
170
178
|
return `${digits}@s.whatsapp.net`;
|
|
171
179
|
}
|
|
172
180
|
|
|
181
|
+
private normalizeJidForComparison(jid: string): string {
|
|
182
|
+
const [localPart, domain = ''] = jid.split('@');
|
|
183
|
+
const normalizedLocal = localPart.split(':')[0];
|
|
184
|
+
return domain ? `${normalizedLocal}@${domain}` : normalizedLocal;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private normalizeJidIdentity(jid: string): string {
|
|
188
|
+
return this.normalizeJidForComparison(jid).split('@')[0];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private getAgentJidCandidates(): string[] {
|
|
192
|
+
const user = this.socket?.user;
|
|
193
|
+
const rawJids = [user?.id, user?.lid].filter((jid): jid is string => Boolean(jid));
|
|
194
|
+
const candidates = new Set<string>();
|
|
195
|
+
|
|
196
|
+
for (const jid of rawJids) {
|
|
197
|
+
const normalized = this.normalizeJidForComparison(jid);
|
|
198
|
+
candidates.add(normalized);
|
|
199
|
+
candidates.add(this.normalizeJidIdentity(jid));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return [...candidates];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private messageHasDirectMention(message: IncomingMessageLike): boolean {
|
|
206
|
+
const mentionedJids = message.message?.extendedTextMessage?.contextInfo?.mentionedJid || [];
|
|
207
|
+
if (mentionedJids.length === 0) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const agentCandidates = this.getAgentJidCandidates();
|
|
212
|
+
if (agentCandidates.length === 0) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return mentionedJids.some(jid => {
|
|
217
|
+
const normalizedMention = this.normalizeJidForComparison(jid);
|
|
218
|
+
const mentionIdentity = this.normalizeJidIdentity(jid);
|
|
219
|
+
return agentCandidates.includes(normalizedMention) || agentCandidates.includes(mentionIdentity);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
173
223
|
private getDisconnectStatusCode(error: unknown): number | undefined {
|
|
174
224
|
if (!error || typeof error !== 'object') {
|
|
175
225
|
return undefined;
|
|
@@ -496,6 +546,10 @@ export class WhatsAppService {
|
|
|
496
546
|
void this.recordIncomingMessage(message, remoteJid, text);
|
|
497
547
|
|
|
498
548
|
const pushName = message.pushName || undefined;
|
|
549
|
+
const groupAllowed = isGroup && this.sessionManager.isAllowedGroup(remoteJid);
|
|
550
|
+
const passiveGroupBlocked = groupAllowed
|
|
551
|
+
&& this.sessionManager.getAllowedGroupReactionMode(remoteJid) === 'passive'
|
|
552
|
+
&& !this.messageHasDirectMention(message);
|
|
499
553
|
|
|
500
554
|
if (this.boundGroupJid) {
|
|
501
555
|
if (!this.sessionManager.isAllowedGroup(this.boundGroupJid)) {
|
|
@@ -503,6 +557,10 @@ export class WhatsAppService {
|
|
|
503
557
|
return;
|
|
504
558
|
}
|
|
505
559
|
|
|
560
|
+
if (this.sessionManager.getAllowedGroupReactionMode(this.boundGroupJid) === 'passive' && !this.messageHasDirectMention(message)) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
506
564
|
this.lastRemoteJid = remoteJid;
|
|
507
565
|
this.onMessage?.(payload);
|
|
508
566
|
return;
|
|
@@ -516,6 +574,10 @@ export class WhatsAppService {
|
|
|
516
574
|
return;
|
|
517
575
|
}
|
|
518
576
|
|
|
577
|
+
if (passiveGroupBlocked) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
519
581
|
this.lastRemoteJid = remoteJid;
|
|
520
582
|
this.onMessage?.(payload);
|
|
521
583
|
}
|
package/src/ui/menu.handler.ts
CHANGED
|
@@ -255,11 +255,12 @@ export class MenuHandler {
|
|
|
255
255
|
const historyLabel = t('menu.allowedGroups.group.history');
|
|
256
256
|
const sendMessageLabel = t('menu.allowedGroups.group.sendMessage');
|
|
257
257
|
const printGroupLabel = t('menu.allowedGroups.group.printGroup');
|
|
258
|
+
const reactionModeLabel = t('menu.allowedGroups.group.reactionMode');
|
|
258
259
|
const removeAliasLabel = t('menu.allowedGroups.group.removeAlias');
|
|
259
260
|
const addAliasLabel = t('menu.allowedGroups.group.addAlias');
|
|
260
261
|
const removeGroupLabel = t('menu.allowedGroups.group.removeGroup');
|
|
261
262
|
const backLabel = t('menu.allowedGroups.group.back');
|
|
262
|
-
const options = [historyLabel, sendMessageLabel, printGroupLabel];
|
|
263
|
+
const options = [historyLabel, sendMessageLabel, printGroupLabel, reactionModeLabel];
|
|
263
264
|
if (group.name) {
|
|
264
265
|
options.push(removeAliasLabel);
|
|
265
266
|
} else {
|
|
@@ -287,6 +288,12 @@ export class MenuHandler {
|
|
|
287
288
|
return;
|
|
288
289
|
}
|
|
289
290
|
|
|
291
|
+
if (choice === reactionModeLabel) {
|
|
292
|
+
await this.manageAllowedGroupReactionMode(ctx, group);
|
|
293
|
+
await this.manageAllowedGroup(ctx, group);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
290
297
|
if (choice === addAliasLabel) {
|
|
291
298
|
const alias = await ctx.ui.input(t('menu.allowedGroups.enterAlias', { groupJid: group.number }));
|
|
292
299
|
const trimmedAlias = alias?.trim() || '';
|
|
@@ -469,6 +476,30 @@ export class MenuHandler {
|
|
|
469
476
|
});
|
|
470
477
|
}
|
|
471
478
|
|
|
479
|
+
private async manageAllowedGroupReactionMode(ctx: ExtensionCommandContext, group: Contact) {
|
|
480
|
+
const displayName = this.formatAllowedGroupOption(group);
|
|
481
|
+
const title = t('menu.allowedGroups.group.reactionMode.title', { displayName });
|
|
482
|
+
const activeLabel = t('menu.allowedGroups.group.reactionMode.active');
|
|
483
|
+
const passiveLabel = t('menu.allowedGroups.group.reactionMode.passive');
|
|
484
|
+
const backLabel = t('menu.allowedGroups.group.back');
|
|
485
|
+
const currentMode = this.sessionManager.getAllowedGroupReactionMode(group.number);
|
|
486
|
+
const options = [activeLabel, passiveLabel, backLabel];
|
|
487
|
+
|
|
488
|
+
const choice = await ctx.ui.select(title, options);
|
|
489
|
+
|
|
490
|
+
if (choice === backLabel || !choice) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (choice === activeLabel || choice === passiveLabel) {
|
|
495
|
+
const nextMode = choice === activeLabel ? 'active' : 'passive';
|
|
496
|
+
if (currentMode !== nextMode) {
|
|
497
|
+
await this.sessionManager.setAllowedGroupReactionMode(group.number, nextMode);
|
|
498
|
+
}
|
|
499
|
+
ctx.ui.notify(t('menu.allowedGroups.group.reactionMode.updated', { displayName, mode: choice }), 'info');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
472
503
|
private async sendPromptedMenuMessage(
|
|
473
504
|
ctx: ExtensionCommandContext,
|
|
474
505
|
options: {
|
package/whatsapp-pi.ts
CHANGED
|
@@ -183,17 +183,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
ctx.ui.notify('WhatsApp: Session reset via /new is now fully supported.', 'info');
|
|
186
|
-
|
|
187
|
-
// Verify pdftotext availability for document support
|
|
188
|
-
try {
|
|
189
|
-
const { code } = await pi.exec('pdftotext', ['-v']);
|
|
190
|
-
if (code !== 0 && code !== 99) { // 99 is a common exit code for -v in some versions
|
|
191
|
-
throw new Error(`pdftotext returned code ${code}`);
|
|
192
|
-
}
|
|
193
|
-
} catch {
|
|
194
|
-
ctx.ui.notify('WhatsApp: pdftotext not found. PDF document support will be limited to storage only.', 'warning');
|
|
195
|
-
logger.warn('[WhatsApp-Pi] Warning: pdftotext not found in system PATH.');
|
|
196
|
-
}
|
|
197
186
|
});
|
|
198
187
|
|
|
199
188
|
// Track whether send_wa_message tool already sent a reply this turn
|