whatsapp-pi 1.0.35 → 1.0.37
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 +3 -2
- package/src/services/baileys-console-filter.ts +66 -0
- package/src/services/incoming-media.service.ts +123 -0
- package/src/services/incoming-message.resolver.ts +129 -0
- package/src/services/recents.service.ts +0 -1
- package/src/services/whatsapp-pi.logger.ts +25 -0
- package/src/services/whatsapp.service.ts +61 -11
- package/src/ui/menu.handler.ts +66 -41
- package/whatsapp-pi.ts +30 -238
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whatsapp-pi",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WhatsApp integration extension for Pi",
|
|
6
6
|
"main": "whatsapp-pi.ts",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"@mariozechner/pi-coding-agent": "latest",
|
|
40
40
|
"@types/node": "^20.11.0",
|
|
41
41
|
"@types/qrcode-terminal": "^0.12.2",
|
|
42
|
+
"@vitest/coverage-v8": "^1.6.1",
|
|
42
43
|
"ts-node": "^10.9.2",
|
|
43
44
|
"tsx": "^4.7.0",
|
|
44
45
|
"typescript": "^5.3.0",
|
|
@@ -50,4 +51,4 @@
|
|
|
50
51
|
],
|
|
51
52
|
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/WhatsApp.svg/512px-WhatsApp.svg.png"
|
|
52
53
|
}
|
|
53
|
-
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
type ConsoleMethod = 'log' | 'info' | 'warn' | 'error';
|
|
2
|
+
|
|
3
|
+
const noisyBaileysPatterns = [
|
|
4
|
+
'Failed to decrypt message with any known session',
|
|
5
|
+
'Session error:',
|
|
6
|
+
'Bad MAC',
|
|
7
|
+
'Closing open session in favor of incoming prekey bundle',
|
|
8
|
+
'Closing session:'
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const stringifyConsolePart = (part: unknown): string => {
|
|
12
|
+
if (part instanceof Error) {
|
|
13
|
+
return `${part.name}: ${part.message}\n${part.stack ?? ''}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof part === 'string') {
|
|
17
|
+
return part;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(part);
|
|
22
|
+
} catch {
|
|
23
|
+
return String(part);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const shouldSuppressBaileysConsoleMessage = (args: unknown[]): boolean => {
|
|
28
|
+
const message = args.map(stringifyConsolePart).join(' ');
|
|
29
|
+
return noisyBaileysPatterns.some(pattern => message.includes(pattern));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const installBaileysConsoleFilter = (verbose: boolean): (() => void) => {
|
|
33
|
+
if (verbose) {
|
|
34
|
+
return () => {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const methods: ConsoleMethod[] = ['log', 'info', 'warn', 'error'];
|
|
38
|
+
const originals = new Map<ConsoleMethod, (...args: any[]) => void>();
|
|
39
|
+
const patched = new Map<ConsoleMethod, (...args: any[]) => void>();
|
|
40
|
+
|
|
41
|
+
for (const method of methods) {
|
|
42
|
+
const original = console[method].bind(console);
|
|
43
|
+
originals.set(method, original);
|
|
44
|
+
|
|
45
|
+
const replacement = (...args: unknown[]) => {
|
|
46
|
+
if (shouldSuppressBaileysConsoleMessage(args)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
original(...args);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
patched.set(method, replacement);
|
|
54
|
+
console[method] = replacement as typeof console[typeof method];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
for (const method of methods) {
|
|
59
|
+
const replacement = patched.get(method);
|
|
60
|
+
const original = originals.get(method);
|
|
61
|
+
if (replacement && original && console[method] === replacement) {
|
|
62
|
+
console[method] = original as typeof console[typeof method];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { downloadContentFromMessage } from '@whiskeysockets/baileys';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { AudioService } from './audio.service.js';
|
|
5
|
+
import type { IncomingResolution } from './incoming-message.resolver.js';
|
|
6
|
+
import { WhatsAppPiLogger } from './whatsapp-pi.logger.js';
|
|
7
|
+
|
|
8
|
+
export interface ProcessedIncomingContent {
|
|
9
|
+
text: string;
|
|
10
|
+
imageBuffer?: Buffer;
|
|
11
|
+
imageMimeType?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class IncomingMediaService {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly audioService: AudioService,
|
|
17
|
+
private readonly logger = new WhatsAppPiLogger(false)
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
async process(resolved: IncomingResolution, pushName: string): Promise<ProcessedIncomingContent> {
|
|
21
|
+
if (resolved.kind === 'audio') {
|
|
22
|
+
return this.processAudio(resolved.audioMessage, pushName);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (resolved.kind === 'image') {
|
|
26
|
+
return this.processImage(resolved.imageMessage, resolved.text, pushName);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (resolved.kind === 'document') {
|
|
30
|
+
return this.processDocument(resolved.documentMessage, pushName);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { text: resolved.text };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async processAudio(audioMessage: any, pushName: string): Promise<ProcessedIncomingContent> {
|
|
37
|
+
this.logger.log(`[WhatsApp-Pi] Transcribing audio from ${pushName}...`);
|
|
38
|
+
const transcription = await this.audioService.transcribe(audioMessage);
|
|
39
|
+
return { text: `[Transcribed Audio]: ${transcription}` };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async processImage(imageMessage: any, fallbackText: string, pushName: string): Promise<ProcessedIncomingContent> {
|
|
43
|
+
this.logger.log(`[WhatsApp-Pi] Downloading image from ${pushName}...`);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const imageBuffer = await this.downloadMessage(imageMessage, 'image');
|
|
47
|
+
const rawMime = imageMessage.mimetype || 'image/jpeg';
|
|
48
|
+
let imageMimeType = rawMime.toLowerCase().split(';')[0].trim();
|
|
49
|
+
if (imageMimeType === 'image/jpg') imageMimeType = 'image/jpeg';
|
|
50
|
+
|
|
51
|
+
this.logger.log(`[WhatsApp-Pi] Image downloaded. MIME: ${imageMimeType} (original: ${rawMime}), Size: ${imageBuffer.length} bytes`);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
text: fallbackText || '[Image]',
|
|
55
|
+
imageBuffer,
|
|
56
|
+
imageMimeType
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
this.logger.error('[WhatsApp-Pi] Failed to download image:', error);
|
|
60
|
+
return { text: '[Image (download failed)]' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async processDocument(documentMessage: any, pushName: string): Promise<ProcessedIncomingContent> {
|
|
65
|
+
const fileName = documentMessage.fileName || 'unnamed_document';
|
|
66
|
+
const mimeType = documentMessage.mimetype || 'application/octet-stream';
|
|
67
|
+
const fileSize = documentMessage.fileLength ? Number(documentMessage.fileLength) : 0;
|
|
68
|
+
|
|
69
|
+
this.logger.log(`[WhatsApp-Pi] Downloading document from ${pushName}: ${fileName}...`);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const buffer = await this.downloadMessage(documentMessage, 'document');
|
|
73
|
+
const relativePath = await this.saveDocument(fileName, buffer);
|
|
74
|
+
|
|
75
|
+
this.logger.log(`[WhatsApp-Pi] Document saved to ${relativePath} (${buffer.length} bytes)`);
|
|
76
|
+
|
|
77
|
+
let text = `[Document Received: ${fileName}]\n`
|
|
78
|
+
+ `MIME Type: ${mimeType}\n`
|
|
79
|
+
+ `Size: ${this.formatFileSize(fileSize)}\n`
|
|
80
|
+
+ `Location: ${relativePath}`;
|
|
81
|
+
|
|
82
|
+
if (documentMessage.caption) {
|
|
83
|
+
text += `\n\nDescription: ${documentMessage.caption}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { text };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
this.logger.error('[WhatsApp-Pi] Failed to download document:', error);
|
|
89
|
+
return { text: `[Document: ${fileName} (download failed)]` };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async downloadMessage(message: any, type: 'image' | 'document'): Promise<Buffer> {
|
|
94
|
+
const stream = await downloadContentFromMessage(message, type);
|
|
95
|
+
let buffer = Buffer.from([]);
|
|
96
|
+
|
|
97
|
+
for await (const chunk of stream) {
|
|
98
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return buffer;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async saveDocument(fileName: string, buffer: Buffer): Promise<string> {
|
|
105
|
+
const sanitized = fileName.replace(/[^a-z0-9\._-]/gi, '_');
|
|
106
|
+
const savedFileName = `${Date.now()}_${sanitized}`;
|
|
107
|
+
const documentDir = join(process.cwd(), '.pi-data', 'whatsapp', 'documents');
|
|
108
|
+
const absolutePath = join(documentDir, savedFileName);
|
|
109
|
+
|
|
110
|
+
await mkdir(documentDir, { recursive: true });
|
|
111
|
+
await writeFile(absolutePath, buffer);
|
|
112
|
+
|
|
113
|
+
return `./.pi-data/whatsapp/documents/${savedFileName}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private formatFileSize(fileSize: number): string {
|
|
117
|
+
if (fileSize > 1024 * 1024) {
|
|
118
|
+
return `${(fileSize / (1024 * 1024)).toFixed(1)} MB`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return `${(fileSize / 1024).toFixed(1)} KB`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { extractMessageContent } from '@whiskeysockets/baileys';
|
|
2
|
+
|
|
3
|
+
export type IncomingResolution =
|
|
4
|
+
| { kind: 'text'; text: string }
|
|
5
|
+
| { kind: 'audio'; text: string; audioMessage: any }
|
|
6
|
+
| { kind: 'image'; text: string; imageMessage: any }
|
|
7
|
+
| { kind: 'document'; text: string; documentMessage: any }
|
|
8
|
+
| { kind: 'contact'; text: string }
|
|
9
|
+
| { kind: 'location'; text: string }
|
|
10
|
+
| { kind: 'system'; text: string }
|
|
11
|
+
| { kind: 'unsupported'; text: string };
|
|
12
|
+
|
|
13
|
+
const protocolTypes: Record<number, string> = {
|
|
14
|
+
0: 'Message Deleted',
|
|
15
|
+
3: 'Disappearing Messages Updated',
|
|
16
|
+
4: 'Disappearing Message Sync Response',
|
|
17
|
+
5: 'History Sync Notification',
|
|
18
|
+
6: 'App State Sync Key Share',
|
|
19
|
+
7: 'App State Sync Key Request',
|
|
20
|
+
8: 'Message Backfill Request',
|
|
21
|
+
9: 'Security Notification Sync',
|
|
22
|
+
10: 'Fatal App State Sync Notification',
|
|
23
|
+
11: 'Phone Number Shared',
|
|
24
|
+
14: 'Message Edited',
|
|
25
|
+
16: 'Peer Data Request',
|
|
26
|
+
17: 'Peer Data Response',
|
|
27
|
+
18: 'Welcome Message Request',
|
|
28
|
+
19: 'Bot Feedback',
|
|
29
|
+
20: 'Media Notification'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const unwrapMessageContent = (content: any): any => extractMessageContent(content) ?? content;
|
|
33
|
+
|
|
34
|
+
const getTypeName = (payload: any): string => {
|
|
35
|
+
if (!payload || typeof payload !== 'object') return 'unknown';
|
|
36
|
+
return Object.keys(payload)[0] || 'unknown';
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const formatProtocolMessage = (protocolMessage: any): string => {
|
|
40
|
+
const typeLabel = protocolTypes[Number(protocolMessage?.type)] || 'System Update';
|
|
41
|
+
const editedText = protocolMessage?.editedMessage?.conversation
|
|
42
|
+
|| protocolMessage?.editedMessage?.extendedTextMessage?.text;
|
|
43
|
+
|
|
44
|
+
if (editedText) {
|
|
45
|
+
return `[${typeLabel}: ${editedText}]`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return `[${typeLabel}]`;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const extractIncomingText = (message: any): IncomingResolution => {
|
|
52
|
+
const content = unwrapMessageContent(message);
|
|
53
|
+
const inner = content?.ephemeralMessage?.message
|
|
54
|
+
|| content?.viewOnceMessage?.message
|
|
55
|
+
|| content?.viewOnceMessageV2?.message
|
|
56
|
+
|| content?.viewOnceMessageV2Extension?.message
|
|
57
|
+
|| content?.message;
|
|
58
|
+
|
|
59
|
+
const resolved = inner ? unwrapMessageContent(inner) : content;
|
|
60
|
+
const typeName = getTypeName(resolved);
|
|
61
|
+
const protocolMessage = resolved?.protocolMessage
|
|
62
|
+
|| (typeName === 'protocolMessage' ? resolved : undefined)
|
|
63
|
+
|| content?.protocolMessage;
|
|
64
|
+
|
|
65
|
+
if (protocolMessage) {
|
|
66
|
+
return { kind: 'system', text: formatProtocolMessage(protocolMessage) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (resolved?.conversation) {
|
|
70
|
+
return { kind: 'text', text: resolved.conversation };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (resolved?.extendedTextMessage?.text) {
|
|
74
|
+
return { kind: 'text', text: resolved.extendedTextMessage.text };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (resolved?.imageMessage) {
|
|
78
|
+
return {
|
|
79
|
+
kind: 'image',
|
|
80
|
+
text: resolved.imageMessage.caption || '[Image]',
|
|
81
|
+
imageMessage: resolved.imageMessage
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (resolved?.videoMessage) {
|
|
86
|
+
return {
|
|
87
|
+
kind: 'text',
|
|
88
|
+
text: resolved.videoMessage.caption || '[Video]'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (resolved?.audioMessage) {
|
|
93
|
+
return {
|
|
94
|
+
kind: 'audio',
|
|
95
|
+
text: '[Audio Message]',
|
|
96
|
+
audioMessage: resolved.audioMessage
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (resolved?.documentMessage) {
|
|
101
|
+
return {
|
|
102
|
+
kind: 'document',
|
|
103
|
+
text: resolved.documentMessage.caption || '[Document]',
|
|
104
|
+
documentMessage: resolved.documentMessage
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (resolved?.contactMessage || resolved?.contactsArrayMessage) {
|
|
109
|
+
return { kind: 'contact', text: '[Contact]' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (resolved?.locationMessage) {
|
|
113
|
+
return { kind: 'location', text: '[Location]' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (resolved?.buttonsResponseMessage?.selectedDisplayText) {
|
|
117
|
+
return { kind: 'text', text: resolved.buttonsResponseMessage.selectedDisplayText };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (resolved?.listResponseMessage?.title) {
|
|
121
|
+
return { kind: 'text', text: resolved.listResponseMessage.title };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (resolved?.templateButtonReplyMessage?.selectedDisplayText) {
|
|
125
|
+
return { kind: 'text', text: resolved.templateButtonReplyMessage.selectedDisplayText };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { kind: 'unsupported', text: `[Unsupported Message Type: ${typeName}]` };
|
|
129
|
+
};
|
|
@@ -191,7 +191,6 @@ export class RecentsService {
|
|
|
191
191
|
const normalizedNumber = this.normalizeNumber(senderNumber);
|
|
192
192
|
const messages = this.store.messagesBySender[normalizedNumber] ?? [];
|
|
193
193
|
return [...messages]
|
|
194
|
-
.map(message => ({ ...message, timestamp: this.normalizeTimestamp(message.timestamp) }))
|
|
195
194
|
.sort((left, right) => left.timestamp - right.timestamp)
|
|
196
195
|
.slice(-20);
|
|
197
196
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class WhatsAppPiLogger {
|
|
2
|
+
constructor(private verbose = false) {}
|
|
3
|
+
|
|
4
|
+
setVerbose(verbose: boolean) {
|
|
5
|
+
this.verbose = verbose;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
log(message: string, ...args: unknown[]) {
|
|
9
|
+
if (this.verbose) {
|
|
10
|
+
console.log(message, ...args);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
warn(message: string, ...args: unknown[]) {
|
|
15
|
+
if (this.verbose) {
|
|
16
|
+
console.warn(message, ...args);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
error(message: string, ...args: unknown[]) {
|
|
21
|
+
if (this.verbose) {
|
|
22
|
+
console.error(message, ...args);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -10,6 +10,11 @@ import { Boom } from '@hapi/boom';
|
|
|
10
10
|
import { SessionManager } from './session.manager.js';
|
|
11
11
|
import { IncomingMessage, WhatsAppSession, SessionStatus } from '../models/whatsapp.types.js';
|
|
12
12
|
import { MessageSender } from './message.sender.js';
|
|
13
|
+
import { installBaileysConsoleFilter } from './baileys-console-filter.js';
|
|
14
|
+
|
|
15
|
+
export interface WhatsAppStartOptions {
|
|
16
|
+
allowPairingOnAuthFailure?: boolean;
|
|
17
|
+
}
|
|
13
18
|
|
|
14
19
|
export class WhatsAppService {
|
|
15
20
|
private socket: any;
|
|
@@ -19,6 +24,7 @@ export class WhatsAppService {
|
|
|
19
24
|
private verboseMode = false;
|
|
20
25
|
private onIncomingMessageRecorded?: (message: IncomingMessage) => void | Promise<void>;
|
|
21
26
|
private saveCreds?: () => Promise<void>;
|
|
27
|
+
private restoreBaileysConsoleFilter?: () => void;
|
|
22
28
|
|
|
23
29
|
constructor(sessionManager: SessionManager) {
|
|
24
30
|
this.sessionManager = sessionManager;
|
|
@@ -29,6 +35,15 @@ export class WhatsAppService {
|
|
|
29
35
|
return this.sessionManager.getStatus();
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
public getEffectiveStatus(): SessionStatus {
|
|
39
|
+
const status = this.sessionManager.getStatus();
|
|
40
|
+
if (status === 'connected' && !this.socket) {
|
|
41
|
+
return 'disconnected';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return status;
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
public setIncomingMessageRecorder(callback: (message: IncomingMessage) => void | Promise<void>) {
|
|
33
48
|
this.onIncomingMessageRecorded = callback;
|
|
34
49
|
}
|
|
@@ -43,6 +58,10 @@ export class WhatsAppService {
|
|
|
43
58
|
|
|
44
59
|
public setVerboseMode(verbose: boolean) {
|
|
45
60
|
this.verboseMode = verbose;
|
|
61
|
+
if (verbose) {
|
|
62
|
+
this.restoreBaileysConsoleFilter?.();
|
|
63
|
+
this.restoreBaileysConsoleFilter = undefined;
|
|
64
|
+
}
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
private normalizeContactNumber(value: string): string {
|
|
@@ -57,7 +76,8 @@ export class WhatsAppService {
|
|
|
57
76
|
return value;
|
|
58
77
|
}
|
|
59
78
|
|
|
60
|
-
async start() {
|
|
79
|
+
async start(options: WhatsAppStartOptions = {}) {
|
|
80
|
+
const allowPairingOnAuthFailure = options.allowPairingOnAuthFailure ?? true;
|
|
61
81
|
if (this.isReconnecting) return;
|
|
62
82
|
this.onStatusUpdate?.('| WhatsApp: Connecting...');
|
|
63
83
|
|
|
@@ -67,6 +87,8 @@ export class WhatsAppService {
|
|
|
67
87
|
|
|
68
88
|
// Cleanup existing socket if any
|
|
69
89
|
if (this.socket) {
|
|
90
|
+
this.restoreBaileysConsoleFilter?.();
|
|
91
|
+
this.restoreBaileysConsoleFilter = undefined;
|
|
70
92
|
this.socket.ev.removeAllListeners('connection.update');
|
|
71
93
|
this.socket.ev.removeAllListeners('creds.update');
|
|
72
94
|
this.socket.ev.removeAllListeners('messages.upsert');
|
|
@@ -127,32 +149,57 @@ export class WhatsAppService {
|
|
|
127
149
|
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
|
|
128
150
|
const errorMessage = lastDisconnect?.error?.message || '';
|
|
129
151
|
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
130
|
-
const
|
|
152
|
+
const isBadMac = errorMessage.includes('Bad MAC');
|
|
153
|
+
const isAuthRejected =
|
|
131
154
|
errorMessage.includes('bad-request') ||
|
|
132
|
-
errorMessage.includes('Bad MAC') ||
|
|
133
155
|
statusCode === 400 ||
|
|
134
156
|
statusCode === 401 ||
|
|
135
157
|
statusCode === DisconnectReason.loggedOut ||
|
|
136
158
|
statusCode === DisconnectReason.badSession;
|
|
159
|
+
const shouldTreatAsLoggedOut = isBadMac || isAuthRejected;
|
|
137
160
|
|
|
138
|
-
|
|
161
|
+
if (this.verboseMode) {
|
|
162
|
+
console.error(`Connection closed [${statusCode}]. Reconnecting: ${shouldReconnect}`);
|
|
163
|
+
}
|
|
139
164
|
|
|
140
165
|
if (shouldTreatAsLoggedOut) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
if (isAuthRejected && !isBadMac && allowPairingOnAuthFailure) {
|
|
167
|
+
if (this.verboseMode) {
|
|
168
|
+
console.error(`Session rejected [${statusCode}] - clearing auth state and starting pairing`);
|
|
169
|
+
}
|
|
170
|
+
await this.sessionManager.deleteAuthState();
|
|
171
|
+
this.socket.ev.removeAllListeners('connection.update');
|
|
172
|
+
this.socket.ev.removeAllListeners('creds.update');
|
|
173
|
+
this.socket.ev.removeAllListeners('messages.upsert');
|
|
174
|
+
try {
|
|
175
|
+
this.socket.end(undefined);
|
|
176
|
+
} catch (e) {}
|
|
177
|
+
this.socket = undefined;
|
|
178
|
+
await this.start({ allowPairingOnAuthFailure: false });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this.verboseMode) {
|
|
183
|
+
console.error(`Session invalid or logged out [${statusCode}] - preserving auth state and requiring re-auth`);
|
|
184
|
+
}
|
|
185
|
+
if (isBadMac) {
|
|
186
|
+
if (this.verboseMode) {
|
|
187
|
+
console.error('[WhatsApp-Pi] Bad MAC error detected. Your session keys are corrupted.');
|
|
188
|
+
console.error('[WhatsApp-Pi] Run /whatsapp-logout to clear auth state, then reconnect with /whatsapp-connect');
|
|
189
|
+
}
|
|
145
190
|
this.onStatusUpdate?.('| WhatsApp: Session Error (Bad MAC)');
|
|
146
191
|
}
|
|
147
192
|
this.sessionManager.setStatus('logged-out');
|
|
148
|
-
if (!
|
|
193
|
+
if (!isBadMac) {
|
|
149
194
|
this.onStatusUpdate?.('| WhatsApp: Logged out');
|
|
150
195
|
}
|
|
151
196
|
return;
|
|
152
197
|
}
|
|
153
198
|
|
|
154
199
|
if (statusCode === DisconnectReason.connectionReplaced) {
|
|
155
|
-
|
|
200
|
+
if (this.verboseMode) {
|
|
201
|
+
console.error('Connection replaced - another instance connected');
|
|
202
|
+
}
|
|
156
203
|
this.onStatusUpdate?.('| WhatsApp: Conflict (Another Instance)');
|
|
157
204
|
return;
|
|
158
205
|
}
|
|
@@ -162,7 +209,7 @@ export class WhatsAppService {
|
|
|
162
209
|
this.onStatusUpdate?.('| WhatsApp: Reconnecting...');
|
|
163
210
|
setTimeout(() => {
|
|
164
211
|
this.isReconnecting = false;
|
|
165
|
-
this.start();
|
|
212
|
+
this.start(options);
|
|
166
213
|
}, 3000);
|
|
167
214
|
} else if (!shouldReconnect) {
|
|
168
215
|
this.sessionManager.setStatus('logged-out');
|
|
@@ -187,6 +234,7 @@ export class WhatsAppService {
|
|
|
187
234
|
console.log = originalConsoleLog;
|
|
188
235
|
console.warn = originalConsoleWarn;
|
|
189
236
|
console.error = originalConsoleError;
|
|
237
|
+
this.restoreBaileysConsoleFilter = installBaileysConsoleFilter(this.verboseMode);
|
|
190
238
|
}
|
|
191
239
|
}
|
|
192
240
|
|
|
@@ -354,6 +402,8 @@ export class WhatsAppService {
|
|
|
354
402
|
}
|
|
355
403
|
|
|
356
404
|
if (this.socket) {
|
|
405
|
+
this.restoreBaileysConsoleFilter?.();
|
|
406
|
+
this.restoreBaileysConsoleFilter = undefined;
|
|
357
407
|
this.socket.ev.removeAllListeners('connection.update');
|
|
358
408
|
this.socket.ev.removeAllListeners('creds.update');
|
|
359
409
|
this.socket.ev.removeAllListeners('messages.upsert');
|
package/src/ui/menu.handler.ts
CHANGED
|
@@ -13,7 +13,7 @@ export class MenuHandler {
|
|
|
13
13
|
) {}
|
|
14
14
|
|
|
15
15
|
async handleCommand(ctx: ExtensionCommandContext) {
|
|
16
|
-
const status = this.
|
|
16
|
+
const status = this.whatsappService.getEffectiveStatus();
|
|
17
17
|
const registered = await this.sessionManager.isRegistered();
|
|
18
18
|
const options: string[] = [];
|
|
19
19
|
|
|
@@ -178,12 +178,7 @@ export class MenuHandler {
|
|
|
178
178
|
const choice = await ctx.ui.select('Blocked Numbers (Select to Manage)', options);
|
|
179
179
|
|
|
180
180
|
if (choice && choice !== 'Back') {
|
|
181
|
-
|
|
182
|
-
if (num.includes('(')) {
|
|
183
|
-
const match = num.match(/\((.*?)\)/);
|
|
184
|
-
if (match) num = match[1];
|
|
185
|
-
}
|
|
186
|
-
await this.manageBlockedNumber(ctx, num);
|
|
181
|
+
await this.manageBlockedNumber(ctx, this.parseContactNumberOption(choice));
|
|
187
182
|
} else {
|
|
188
183
|
await this.handleCommand(ctx);
|
|
189
184
|
}
|
|
@@ -301,36 +296,33 @@ export class MenuHandler {
|
|
|
301
296
|
}
|
|
302
297
|
|
|
303
298
|
private async sendMessageFromRecents(ctx: ExtensionCommandContext, conversation: RecentConversationSummary) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
ctx.ui.notify('Please enter a message before sending.', 'error');
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const result = await this.whatsappService.sendMenuMessage(this.toJid(conversation.senderNumber), trimmed);
|
|
315
|
-
if (result.success) {
|
|
316
|
-
await this.recentsService.recordMessage({
|
|
317
|
-
messageId: result.messageId ?? `${Date.now()}`,
|
|
318
|
-
senderNumber: conversation.senderNumber,
|
|
319
|
-
senderName: conversation.senderName,
|
|
320
|
-
text: trimmed,
|
|
321
|
-
direction: 'outgoing',
|
|
322
|
-
timestamp: Date.now()
|
|
323
|
-
});
|
|
324
|
-
ctx.ui.notify(`Sent message to ${displayName}`, 'info');
|
|
325
|
-
} else {
|
|
326
|
-
ctx.ui.notify(`Failed to send message to ${displayName}: ${result.error ?? 'Unknown error'}`, 'error');
|
|
327
|
-
}
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
299
|
+
await this.sendPromptedMenuMessage(ctx, {
|
|
300
|
+
displayName: this.getConversationDisplayName(conversation),
|
|
301
|
+
senderNumber: conversation.senderNumber,
|
|
302
|
+
senderName: conversation.senderName,
|
|
303
|
+
appendPiSuffix: false
|
|
304
|
+
});
|
|
330
305
|
}
|
|
331
306
|
|
|
332
307
|
private async sendMessageToAllowedNumber(ctx: ExtensionCommandContext, contact: Contact) {
|
|
333
|
-
|
|
308
|
+
await this.sendPromptedMenuMessage(ctx, {
|
|
309
|
+
displayName: this.formatAllowedContactOption(contact),
|
|
310
|
+
senderNumber: contact.number,
|
|
311
|
+
senderName: contact.name,
|
|
312
|
+
appendPiSuffix: true
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async sendPromptedMenuMessage(
|
|
317
|
+
ctx: ExtensionCommandContext,
|
|
318
|
+
options: {
|
|
319
|
+
displayName: string;
|
|
320
|
+
senderNumber: string;
|
|
321
|
+
senderName?: string;
|
|
322
|
+
appendPiSuffix: boolean;
|
|
323
|
+
}
|
|
324
|
+
) {
|
|
325
|
+
const { displayName, senderNumber, senderName, appendPiSuffix } = options;
|
|
334
326
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
335
327
|
const inputText = (await ctx.ui.input(`Send a message to ${displayName}:`))?.trim() || '';
|
|
336
328
|
|
|
@@ -339,15 +331,14 @@ export class MenuHandler {
|
|
|
339
331
|
continue;
|
|
340
332
|
}
|
|
341
333
|
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
const result = await this.whatsappService.sendMenuMessage(this.toJid(contact.number), inputTextWithPiSuffix);
|
|
334
|
+
const messageText = appendPiSuffix ? `${inputText} π` : inputText;
|
|
335
|
+
const result = await this.whatsappService.sendMenuMessage(this.toJid(senderNumber), messageText);
|
|
345
336
|
if (result.success) {
|
|
346
337
|
await this.recentsService.recordMessage({
|
|
347
338
|
messageId: result.messageId ?? `${Date.now()}`,
|
|
348
|
-
senderNumber
|
|
349
|
-
senderName
|
|
350
|
-
text:
|
|
339
|
+
senderNumber,
|
|
340
|
+
senderName,
|
|
341
|
+
text: messageText,
|
|
351
342
|
direction: 'outgoing',
|
|
352
343
|
timestamp: Date.now()
|
|
353
344
|
});
|
|
@@ -372,7 +363,8 @@ export class MenuHandler {
|
|
|
372
363
|
}
|
|
373
364
|
|
|
374
365
|
const options = [
|
|
375
|
-
...
|
|
366
|
+
...this.sortHistoryByMostRecent(history)
|
|
367
|
+
.map(message => this.formatHistoryOption(message.timestamp, message.direction, message.text)),
|
|
376
368
|
'Back'
|
|
377
369
|
];
|
|
378
370
|
|
|
@@ -404,6 +396,39 @@ export class MenuHandler {
|
|
|
404
396
|
return contact.name ? `${contact.name} ${contact.number}` : contact.number;
|
|
405
397
|
}
|
|
406
398
|
|
|
399
|
+
private parseContactNumberOption(choice: string): string {
|
|
400
|
+
if (!choice.includes('(')) {
|
|
401
|
+
return choice;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const match = choice.match(/\((.*?)\)/);
|
|
405
|
+
return match?.[1] ?? choice;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private sortHistoryByMostRecent<T extends { timestamp: number }>(history: T[]): T[] {
|
|
409
|
+
return [...history].sort((left, right) => {
|
|
410
|
+
const dayComparison = this.getDayStart(right.timestamp) - this.getDayStart(left.timestamp);
|
|
411
|
+
if (dayComparison !== 0) {
|
|
412
|
+
return dayComparison;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return this.getTimeOfDay(right.timestamp) - this.getTimeOfDay(left.timestamp);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private getTimeOfDay(timestamp: number): number {
|
|
420
|
+
const date = new Date(timestamp);
|
|
421
|
+
return date.getHours() * 60 * 60 * 1000
|
|
422
|
+
+ date.getMinutes() * 60 * 1000
|
|
423
|
+
+ date.getSeconds() * 1000
|
|
424
|
+
+ date.getMilliseconds();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private getDayStart(timestamp: number): number {
|
|
428
|
+
const date = new Date(timestamp);
|
|
429
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
430
|
+
}
|
|
431
|
+
|
|
407
432
|
private formatHistoryOption(timestamp: number, direction: string, text: string): string {
|
|
408
433
|
const marker = direction === 'outgoing' ? 'Sent' : 'Received';
|
|
409
434
|
const displayText = this.truncate(text, 60) || '[No text]';
|
package/whatsapp-pi.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import type { ExtensionAPI,
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import { extractMessageContent } from '@whiskeysockets/baileys';
|
|
4
3
|
import { SessionManager } from './src/services/session.manager.js';
|
|
5
4
|
import { WhatsAppService } from './src/services/whatsapp.service.js';
|
|
6
5
|
import { MenuHandler } from './src/ui/menu.handler.js';
|
|
7
6
|
import { RecentsService } from './src/services/recents.service.js';
|
|
8
7
|
import { AudioService } from './src/services/audio.service.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
console.log("[WhatsApp-Pi] Extension file loaded by Pi...");
|
|
8
|
+
import { extractIncomingText } from './src/services/incoming-message.resolver.js';
|
|
9
|
+
import { IncomingMediaService } from './src/services/incoming-media.service.js';
|
|
10
|
+
import { WhatsAppPiLogger } from './src/services/whatsapp-pi.logger.js';
|
|
13
11
|
|
|
14
12
|
const shutdownState = globalThis as typeof globalThis & {
|
|
15
13
|
__whatsappPiShutdown?: {
|
|
@@ -36,6 +34,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
36
34
|
const whatsappService = new WhatsAppService(sessionManager);
|
|
37
35
|
const recentsService = new RecentsService(sessionManager);
|
|
38
36
|
const audioService = new AudioService();
|
|
37
|
+
const logger = new WhatsAppPiLogger(false);
|
|
38
|
+
const incomingMediaService = new IncomingMediaService(audioService, logger);
|
|
39
39
|
const menuHandler = new MenuHandler(whatsappService, sessionManager, recentsService);
|
|
40
40
|
let _ctx: ExtensionContext | undefined;
|
|
41
41
|
|
|
@@ -46,11 +46,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
shutdownState.__whatsappPiShutdown.installed = true;
|
|
49
|
+
|
|
49
50
|
const shutdown = async (reason: string) => {
|
|
50
51
|
try {
|
|
51
52
|
await shutdownState.__whatsappPiShutdown?.stop?.();
|
|
52
53
|
} catch (error) {
|
|
53
|
-
|
|
54
|
+
logger.error(`[WhatsApp-Pi] Graceful shutdown failed during ${reason}:`, error);
|
|
54
55
|
}
|
|
55
56
|
};
|
|
56
57
|
|
|
@@ -59,7 +60,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
// Initial status setup
|
|
62
|
-
pi.on("session_start", async (
|
|
63
|
+
pi.on("session_start", async (event, ctx) => {
|
|
63
64
|
_ctx = ctx;
|
|
64
65
|
// Check verbose mode
|
|
65
66
|
const isVerboseFlagSet = process.argv.includes("--verbose");
|
|
@@ -67,9 +68,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
67
68
|
const isVerbose = isVerboseFlagSet;
|
|
68
69
|
|
|
69
70
|
whatsappService.setVerboseMode(isVerbose);
|
|
71
|
+
logger.setVerbose(isVerbose);
|
|
70
72
|
|
|
71
73
|
if (isVerbose) {
|
|
72
|
-
|
|
74
|
+
logger.log('[WhatsApp-Pi] Verbose mode enabled - Baileys trace logs will be shown');
|
|
73
75
|
}
|
|
74
76
|
ctx.ui.setStatus('whatsapp', '| WhatsApp: Disconnected');
|
|
75
77
|
whatsappService.setStatusCallback((status) => {
|
|
@@ -98,10 +100,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
98
100
|
const savedStateEntry = [...ctx.sessionManager.getEntries()]
|
|
99
101
|
.reverse()
|
|
100
102
|
.find(entry => entry.type === "custom" && entry.customType === "whatsapp-state");
|
|
103
|
+
const isWhatsappPiOn = event.reason === "startup" && pi.getFlag("whatsapp-pi-online") === true;
|
|
101
104
|
|
|
102
105
|
if (savedStateEntry) {
|
|
103
106
|
const data = (savedStateEntry as { data?: any }).data;
|
|
104
|
-
if (data.status)
|
|
107
|
+
if (data.status) {
|
|
108
|
+
const restoredStatus = data.status === 'connected' && !isWhatsappPiOn
|
|
109
|
+
? 'disconnected'
|
|
110
|
+
: data.status;
|
|
111
|
+
await sessionManager.setStatus(restoredStatus);
|
|
112
|
+
}
|
|
105
113
|
if (Array.isArray(data.allowList)) {
|
|
106
114
|
for (const n of data.allowList) {
|
|
107
115
|
const num = typeof n === "string" ? n : n.number;
|
|
@@ -111,12 +119,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
111
119
|
}
|
|
112
120
|
}
|
|
113
121
|
|
|
114
|
-
// Check whatsapp flag
|
|
115
|
-
const isWhatsappPiOn = process.argv.includes("--whatsapp-pi-online");
|
|
122
|
+
// Check whatsapp flag — only auto-connect on initial startup, not reloads/new sessions
|
|
116
123
|
const registered = await sessionManager.isRegistered();
|
|
117
124
|
|
|
118
|
-
if (
|
|
119
|
-
ctx.ui.setStatus('whatsapp',
|
|
125
|
+
if (isWhatsappPiOn && registered) {
|
|
126
|
+
ctx.ui.setStatus('whatsapp', '| WhatsApp: Auto-connecting...');
|
|
120
127
|
|
|
121
128
|
// Retry logic (max 3 attempts, 3s delay)
|
|
122
129
|
let attempts = 0;
|
|
@@ -125,7 +132,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
125
132
|
const tryConnect = async () => {
|
|
126
133
|
attempts++;
|
|
127
134
|
try {
|
|
128
|
-
await whatsappService.start();
|
|
135
|
+
await whatsappService.start({ allowPairingOnAuthFailure: false });
|
|
129
136
|
} catch (error) {
|
|
130
137
|
if (attempts < maxAttempts) {
|
|
131
138
|
ctx.ui.notify(`WhatsApp: Connection attempt ${attempts} failed. Retrying...`, 'warning');
|
|
@@ -152,141 +159,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
152
159
|
}
|
|
153
160
|
} catch (e) {
|
|
154
161
|
ctx.ui.notify('WhatsApp: pdftotext not found. PDF document support will be limited to storage only.', 'warning');
|
|
155
|
-
|
|
162
|
+
logger.warn('[WhatsApp-Pi] Warning: pdftotext not found in system PATH.');
|
|
156
163
|
}
|
|
157
164
|
});
|
|
158
165
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
type IncomingResolution =
|
|
162
|
-
| { kind: 'text'; text: string }
|
|
163
|
-
| { kind: 'audio'; text: string; audioMessage: any }
|
|
164
|
-
| { kind: 'image'; text: string; imageMessage: any }
|
|
165
|
-
| { kind: 'document'; text: string; documentMessage: any }
|
|
166
|
-
| { kind: 'contact'; text: string }
|
|
167
|
-
| { kind: 'location'; text: string }
|
|
168
|
-
| { kind: 'system'; text: string }
|
|
169
|
-
| { kind: 'unsupported'; text: string };
|
|
170
|
-
|
|
171
|
-
const extractIncomingText = (message: any, pushName: string): IncomingResolution => {
|
|
172
|
-
const unwrap = (content: any): any => extractMessageContent(content) ?? content;
|
|
173
|
-
const content = unwrap(message);
|
|
174
|
-
|
|
175
|
-
const getTypeName = (payload: any): string => {
|
|
176
|
-
if (!payload || typeof payload !== 'object') return 'unknown';
|
|
177
|
-
const keys = Object.keys(payload);
|
|
178
|
-
return keys[0] || 'unknown';
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const formatProtocolMessage = (protocolMessage: any): string => {
|
|
182
|
-
const protocolTypes: Record<number, string> = {
|
|
183
|
-
0: 'Message Deleted',
|
|
184
|
-
3: 'Disappearing Messages Updated',
|
|
185
|
-
4: 'Disappearing Message Sync Response',
|
|
186
|
-
5: 'History Sync Notification',
|
|
187
|
-
6: 'App State Sync Key Share',
|
|
188
|
-
7: 'App State Sync Key Request',
|
|
189
|
-
8: 'Message Backfill Request',
|
|
190
|
-
9: 'Security Notification Sync',
|
|
191
|
-
10: 'Fatal App State Sync Notification',
|
|
192
|
-
11: 'Phone Number Shared',
|
|
193
|
-
14: 'Message Edited',
|
|
194
|
-
16: 'Peer Data Request',
|
|
195
|
-
17: 'Peer Data Response',
|
|
196
|
-
18: 'Welcome Message Request',
|
|
197
|
-
19: 'Bot Feedback',
|
|
198
|
-
20: 'Media Notification'
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const typeLabel = protocolTypes[Number(protocolMessage?.type)] || 'System Update';
|
|
202
|
-
if (protocolMessage?.editedMessage?.conversation || protocolMessage?.editedMessage?.extendedTextMessage?.text) {
|
|
203
|
-
const editedText = protocolMessage.editedMessage.conversation
|
|
204
|
-
|| protocolMessage.editedMessage.extendedTextMessage?.text
|
|
205
|
-
|| '[Edited message]';
|
|
206
|
-
return `[${typeLabel}: ${editedText}]`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return `[${typeLabel}]`;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const inner = content?.ephemeralMessage?.message
|
|
213
|
-
|| content?.viewOnceMessage?.message
|
|
214
|
-
|| content?.viewOnceMessageV2?.message
|
|
215
|
-
|| content?.viewOnceMessageV2Extension?.message
|
|
216
|
-
|| content?.message;
|
|
217
|
-
|
|
218
|
-
const resolved = inner ? unwrap(inner) : content;
|
|
219
|
-
const typeName = getTypeName(resolved);
|
|
220
|
-
const protocolMessage = resolved?.protocolMessage
|
|
221
|
-
|| (typeName === 'protocolMessage' ? resolved : undefined)
|
|
222
|
-
|| content?.protocolMessage;
|
|
223
|
-
|
|
224
|
-
if (protocolMessage) {
|
|
225
|
-
return { kind: 'system', text: formatProtocolMessage(protocolMessage) };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (resolved?.conversation) {
|
|
229
|
-
return { kind: 'text', text: resolved.conversation };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (resolved?.extendedTextMessage?.text) {
|
|
233
|
-
return { kind: 'text', text: resolved.extendedTextMessage.text };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (resolved?.imageMessage) {
|
|
237
|
-
return {
|
|
238
|
-
kind: 'image',
|
|
239
|
-
text: resolved.imageMessage.caption || '[Image]',
|
|
240
|
-
imageMessage: resolved.imageMessage
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (resolved?.videoMessage) {
|
|
245
|
-
return {
|
|
246
|
-
kind: 'text',
|
|
247
|
-
text: resolved.videoMessage.caption || '[Video]'
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (resolved?.audioMessage) {
|
|
252
|
-
return {
|
|
253
|
-
kind: 'audio',
|
|
254
|
-
text: '[Audio Message]',
|
|
255
|
-
audioMessage: resolved.audioMessage
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (resolved?.documentMessage) {
|
|
260
|
-
return {
|
|
261
|
-
kind: 'document',
|
|
262
|
-
text: resolved.documentMessage.caption || '[Document]',
|
|
263
|
-
documentMessage: resolved.documentMessage
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (resolved?.contactMessage || resolved?.contactsArrayMessage) {
|
|
268
|
-
return { kind: 'contact', text: '[Contact]' };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (resolved?.locationMessage) {
|
|
272
|
-
return { kind: 'location', text: '[Location]' };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (resolved?.buttonsResponseMessage?.selectedDisplayText) {
|
|
276
|
-
return { kind: 'text', text: resolved.buttonsResponseMessage.selectedDisplayText };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (resolved?.listResponseMessage?.title) {
|
|
280
|
-
return { kind: 'text', text: resolved.listResponseMessage.title };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (resolved?.templateButtonReplyMessage?.selectedDisplayText) {
|
|
284
|
-
return { kind: 'text', text: resolved.templateButtonReplyMessage.selectedDisplayText };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return { kind: 'unsupported', text: `[Unsupported Message Type: ${typeName}]` };
|
|
288
|
-
};
|
|
289
|
-
|
|
290
166
|
// Handle incoming messages by injecting them as user prompts
|
|
291
167
|
whatsappService.setMessageCallback(async (m) => {
|
|
292
168
|
const msg = m.messages[0];
|
|
@@ -302,99 +178,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
302
178
|
whatsappService.sendPresence(remoteJid, 'composing');
|
|
303
179
|
}
|
|
304
180
|
|
|
305
|
-
const resolved = extractIncomingText(msg.message
|
|
181
|
+
const resolved = extractIncomingText(msg.message);
|
|
306
182
|
if (resolved.kind === 'system') {
|
|
307
|
-
|
|
183
|
+
logger.log(`[WhatsApp-Pi] ${pushName} (${sender}): ${resolved.text}`);
|
|
308
184
|
return;
|
|
309
185
|
}
|
|
310
186
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// Handle media types
|
|
314
|
-
let imageBuffer: Buffer | undefined;
|
|
315
|
-
let imageMimeType: string | undefined;
|
|
316
|
-
|
|
317
|
-
if (resolved.kind === 'audio') {
|
|
318
|
-
console.log(`[WhatsApp-Pi] Transcribing audio from ${pushName}...`);
|
|
319
|
-
const transcription = await audioService.transcribe(resolved.audioMessage);
|
|
320
|
-
text = `[Transcribed Audio]: ${transcription}`;
|
|
321
|
-
} else if (resolved.kind === 'image') {
|
|
322
|
-
console.log(`[WhatsApp-Pi] Downloading image from ${pushName}...`);
|
|
323
|
-
try {
|
|
324
|
-
const { downloadContentFromMessage } = await import('@whiskeysockets/baileys');
|
|
325
|
-
const stream = await downloadContentFromMessage(resolved.imageMessage, 'image');
|
|
326
|
-
let buffer = Buffer.from([]);
|
|
327
|
-
for await (const chunk of stream) {
|
|
328
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
329
|
-
}
|
|
330
|
-
imageBuffer = buffer;
|
|
331
|
-
|
|
332
|
-
// Normalize mime type for Cloud Code Assist / Gemini
|
|
333
|
-
let rawMime = resolved.imageMessage.mimetype || 'image/jpeg';
|
|
334
|
-
imageMimeType = rawMime.toLowerCase().split(';')[0].trim();
|
|
335
|
-
if (imageMimeType === 'image/jpg') imageMimeType = 'image/jpeg';
|
|
336
|
-
|
|
337
|
-
console.log(`[WhatsApp-Pi] Image downloaded. MIME: ${imageMimeType} (original: ${rawMime}), Size: ${imageBuffer.length} bytes`);
|
|
338
|
-
|
|
339
|
-
text = resolved.text || "[Image]";
|
|
340
|
-
} catch (e) {
|
|
341
|
-
console.error(`[WhatsApp-Pi] Failed to download image:`, e);
|
|
342
|
-
text = "[Image (download failed)]";
|
|
343
|
-
}
|
|
344
|
-
} else if (resolved.kind === 'document') {
|
|
345
|
-
const doc = resolved.documentMessage;
|
|
346
|
-
const fileName = doc.fileName || 'unnamed_document';
|
|
347
|
-
const mimeType = doc.mimetype || 'application/octet-stream';
|
|
348
|
-
const fileSize = doc.fileLength ? Number(doc.fileLength) : 0;
|
|
349
|
-
|
|
350
|
-
console.log(`[WhatsApp-Pi] Downloading document from ${pushName}: ${fileName}...`);
|
|
351
|
-
|
|
352
|
-
try {
|
|
353
|
-
const { downloadContentFromMessage } = await import('@whiskeysockets/baileys');
|
|
354
|
-
const stream = await downloadContentFromMessage(doc, 'document');
|
|
355
|
-
let buffer = Buffer.from([]);
|
|
356
|
-
for await (const chunk of stream) {
|
|
357
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Sanitize filename
|
|
361
|
-
const sanitized = fileName.replace(/[^a-z0-9\._-]/gi, '_');
|
|
362
|
-
const savedFileName = `${Date.now()}_${sanitized}`;
|
|
363
|
-
const relativePath = `./.pi-data/whatsapp/documents/${savedFileName}`;
|
|
364
|
-
const absolutePath = join(process.cwd(), '.pi-data', 'whatsapp', 'documents', savedFileName);
|
|
365
|
-
|
|
366
|
-
// Ensure directory exists (T001 handles it at startup, but let's be safe)
|
|
367
|
-
await mkdir(join(process.cwd(), '.pi-data', 'whatsapp', 'documents'), { recursive: true });
|
|
368
|
-
await writeFile(absolutePath, buffer);
|
|
369
|
-
|
|
370
|
-
console.log(`[WhatsApp-Pi] Document saved to ${relativePath} (${buffer.length} bytes)`);
|
|
371
|
-
|
|
372
|
-
const sizeFormatted = fileSize > 1024 * 1024
|
|
373
|
-
? `${(fileSize / (1024 * 1024)).toFixed(1)} MB`
|
|
374
|
-
: `${(fileSize / 1024).toFixed(1)} KB`;
|
|
375
|
-
|
|
376
|
-
text = `[Document Received: ${fileName}]\n` +
|
|
377
|
-
`MIME Type: ${mimeType}\n` +
|
|
378
|
-
`Size: ${sizeFormatted}\n` +
|
|
379
|
-
`Location: ${relativePath}`;
|
|
380
|
-
|
|
381
|
-
if (doc.caption) {
|
|
382
|
-
text += `\n\nDescription: ${doc.caption}`;
|
|
383
|
-
}
|
|
384
|
-
} catch (e) {
|
|
385
|
-
console.error(`[WhatsApp-Pi] Failed to download document:`, e);
|
|
386
|
-
text = `[Document: ${fileName} (download failed)]`;
|
|
387
|
-
}
|
|
388
|
-
} else if (resolved.kind === 'contact') {
|
|
389
|
-
text = resolved.text;
|
|
390
|
-
} else if (resolved.kind === 'location') {
|
|
391
|
-
text = resolved.text;
|
|
392
|
-
} else if (resolved.kind === 'unsupported') {
|
|
393
|
-
text = resolved.text;
|
|
394
|
-
}
|
|
187
|
+
const { text, imageBuffer, imageMimeType } = await incomingMediaService.process(resolved, pushName);
|
|
395
188
|
|
|
396
|
-
|
|
397
|
-
console.log(`[WhatsApp-Pi] ${pushName} (${sender}): ${text}`);
|
|
189
|
+
logger.log(`[WhatsApp-Pi] ${pushName} (${sender}): ${text}`);
|
|
398
190
|
|
|
399
191
|
// Use a standard delivery for ALL messages to ensure TUI consistency
|
|
400
192
|
if (imageBuffer && imageMimeType) {
|
|
@@ -408,7 +200,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
408
200
|
|
|
409
201
|
// Handle commands
|
|
410
202
|
if (text.trim().toLowerCase().startsWith('/compact')) {
|
|
411
|
-
|
|
203
|
+
logger.log(`[WhatsApp-Pi] Session compact requested by ${pushName}.`);
|
|
412
204
|
|
|
413
205
|
if (_ctx) {
|
|
414
206
|
_ctx.compact();
|
|
@@ -418,7 +210,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
418
210
|
}
|
|
419
211
|
|
|
420
212
|
if (text.trim().toLowerCase().startsWith('/abort')) {
|
|
421
|
-
|
|
213
|
+
logger.log(`[WhatsApp-Pi] Abort requested by ${pushName}.`);
|
|
422
214
|
if (_ctx) {
|
|
423
215
|
_ctx.abort();
|
|
424
216
|
await whatsappService.sendMessage(remoteJid!, "Aborted! ✅");
|
|
@@ -524,7 +316,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
524
316
|
});
|
|
525
317
|
|
|
526
318
|
pi.on("session_shutdown", async () => {
|
|
527
|
-
|
|
319
|
+
logger.log("[WhatsApp-Pi] Session shutdown detected. Stopping WhatsApp service...");
|
|
528
320
|
await whatsappService.stop();
|
|
529
321
|
});
|
|
530
322
|
}
|