whatsapp-pi 1.0.9 → 1.0.10
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 +98 -98
- package/package.json +45 -45
- package/src/models/whatsapp.types.ts +46 -46
- package/src/services/audio.service.ts +53 -53
- package/src/services/message.sender.ts +93 -93
- package/src/services/session.manager.ts +201 -196
- package/src/services/whatsapp.service.ts +250 -248
- package/src/ui/menu.handler.ts +161 -139
- package/whatsapp-pi.ts +214 -238
package/README.md
CHANGED
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="https://upload.wikimedia.org/wikipedia/commons/6/6b/WhatsApp.svg" alt="WhatsApp Logo" width="100">
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
# WhatsApp-Pi
|
|
6
|
-
|
|
7
|
-
A WhatsApp integration extension for the **[Pi Coding Agent](https://github.com/mariozechner/pi-coding-agent)**.
|
|
8
|
-
|
|
9
|
-
Pi is a powerful agentic AI coding assistant that operates in your terminal. This extension allows you to chat and pair-program with your Pi agent directly through WhatsApp, featuring message filtering, allow-listing, and reliable message delivery.
|
|
10
|
-
|
|
11
|
-
## Features
|
|
12
|
-
|
|
13
|
-
- **Manual WhatsApp Connection**: QR code-based authentication with session persistence
|
|
14
|
-
- **Allow List**: Control which numbers can interact with Pi
|
|
15
|
-
- Add contacts with optional names for easy identification
|
|
16
|
-
- View ignored numbers (not in allow list) and add them when needed
|
|
17
|
-
- **Reliable Messaging**: Queue-based message sending with retry logic
|
|
18
|
-
- **TUI Integration**: Menu-driven interface for managing connections and contacts
|
|
19
|
-
|
|
20
|
-
## Quick Start
|
|
21
|
-
|
|
22
|
-
1. Install the extension:
|
|
23
|
-
```bash
|
|
24
|
-
pi install npm:whatsapp-pi
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
2. Start Pi (the extension will load automatically once installed):
|
|
28
|
-
```bash
|
|
29
|
-
pi
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
To automatically connect to WhatsApp on startup (if you are already authenticated):
|
|
33
|
-
```bash
|
|
34
|
-
pi -w
|
|
35
|
-
# or
|
|
36
|
-
pi --whatsapp
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
3. Use the menu to connect WhatsApp and manage allowed/blocked numbers
|
|
40
|
-
|
|
41
|
-
## Development / Testing
|
|
42
|
-
|
|
43
|
-
If you are developing or testing the extension locally:
|
|
44
|
-
|
|
45
|
-
1. Install dependencies:
|
|
46
|
-
```bash
|
|
47
|
-
npm install
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
2. Run the extension:
|
|
51
|
-
```bash
|
|
52
|
-
pi -e whatsapp-pi.ts
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
For verbose mode (shows Baileys trace logs for debugging):
|
|
56
|
-
```bash
|
|
57
|
-
pi -e whatsapp-pi.ts -v
|
|
58
|
-
# or
|
|
59
|
-
pi -e whatsapp-pi.ts --verbose
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Commands
|
|
63
|
-
|
|
64
|
-
- `/whatsapp` - Open the WhatsApp management menu
|
|
65
|
-
- **Allow Numbers**: Manage contacts that can interact with Pi
|
|
66
|
-
- **Blocked Numbers**: View ignored numbers (not in allow list) and add them to allow list
|
|
67
|
-
|
|
68
|
-
## Project Structure
|
|
69
|
-
|
|
70
|
-
```
|
|
71
|
-
src/
|
|
72
|
-
├── models/ # Type definitions
|
|
73
|
-
├── services/ # Core services (WhatsApp, Session, MessageSender)
|
|
74
|
-
└── ui/ # Menu handlers
|
|
75
|
-
|
|
76
|
-
specs/ # Feature specifications
|
|
77
|
-
tests/ # Unit and integration tests
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Documentation
|
|
81
|
-
|
|
82
|
-
See `specs/` directory for detailed feature documentation:
|
|
83
|
-
- `001-whatsapp-tui-integration/` - TUI menu system
|
|
84
|
-
- `002-manual-whatsapp-connection/` - Connection management
|
|
85
|
-
- `003-whatsapp-messaging-refactor/` - Reliable messaging
|
|
86
|
-
- `004-blocked-numbers-management/` - Block list feature
|
|
87
|
-
|
|
88
|
-
## Development
|
|
89
|
-
|
|
90
|
-
Run tests:
|
|
91
|
-
```bash
|
|
92
|
-
npm test
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Lint:
|
|
96
|
-
```bash
|
|
97
|
-
npm run lint
|
|
98
|
-
```
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://upload.wikimedia.org/wikipedia/commons/6/6b/WhatsApp.svg" alt="WhatsApp Logo" width="100">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# WhatsApp-Pi
|
|
6
|
+
|
|
7
|
+
A WhatsApp integration extension for the **[Pi Coding Agent](https://github.com/mariozechner/pi-coding-agent)**.
|
|
8
|
+
|
|
9
|
+
Pi is a powerful agentic AI coding assistant that operates in your terminal. This extension allows you to chat and pair-program with your Pi agent directly through WhatsApp, featuring message filtering, allow-listing, and reliable message delivery.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Manual WhatsApp Connection**: QR code-based authentication with session persistence
|
|
14
|
+
- **Allow List**: Control which numbers can interact with Pi
|
|
15
|
+
- Add contacts with optional names for easy identification
|
|
16
|
+
- View ignored numbers (not in allow list) and add them when needed
|
|
17
|
+
- **Reliable Messaging**: Queue-based message sending with retry logic
|
|
18
|
+
- **TUI Integration**: Menu-driven interface for managing connections and contacts
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
1. Install the extension:
|
|
23
|
+
```bash
|
|
24
|
+
pi install npm:whatsapp-pi
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. Start Pi (the extension will load automatically once installed):
|
|
28
|
+
```bash
|
|
29
|
+
pi
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
To automatically connect to WhatsApp on startup (if you are already authenticated):
|
|
33
|
+
```bash
|
|
34
|
+
pi -w
|
|
35
|
+
# or
|
|
36
|
+
pi --whatsapp
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
3. Use the menu to connect WhatsApp and manage allowed/blocked numbers
|
|
40
|
+
|
|
41
|
+
## Development / Testing
|
|
42
|
+
|
|
43
|
+
If you are developing or testing the extension locally:
|
|
44
|
+
|
|
45
|
+
1. Install dependencies:
|
|
46
|
+
```bash
|
|
47
|
+
npm install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
2. Run the extension:
|
|
51
|
+
```bash
|
|
52
|
+
pi -e whatsapp-pi.ts
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For verbose mode (shows Baileys trace logs for debugging):
|
|
56
|
+
```bash
|
|
57
|
+
pi -e whatsapp-pi.ts -v
|
|
58
|
+
# or
|
|
59
|
+
pi -e whatsapp-pi.ts --verbose
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Commands
|
|
63
|
+
|
|
64
|
+
- `/whatsapp` - Open the WhatsApp management menu
|
|
65
|
+
- **Allow Numbers**: Manage contacts that can interact with Pi
|
|
66
|
+
- **Blocked Numbers**: View ignored numbers (not in allow list) and add them to allow list
|
|
67
|
+
|
|
68
|
+
## Project Structure
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
src/
|
|
72
|
+
├── models/ # Type definitions
|
|
73
|
+
├── services/ # Core services (WhatsApp, Session, MessageSender)
|
|
74
|
+
└── ui/ # Menu handlers
|
|
75
|
+
|
|
76
|
+
specs/ # Feature specifications
|
|
77
|
+
tests/ # Unit and integration tests
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
See `specs/` directory for detailed feature documentation:
|
|
83
|
+
- `001-whatsapp-tui-integration/` - TUI menu system
|
|
84
|
+
- `002-manual-whatsapp-connection/` - Connection management
|
|
85
|
+
- `003-whatsapp-messaging-refactor/` - Reliable messaging
|
|
86
|
+
- `004-blocked-numbers-management/` - Block list feature
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
Run tests:
|
|
91
|
+
```bash
|
|
92
|
+
npm test
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Lint:
|
|
96
|
+
```bash
|
|
97
|
+
npm run lint
|
|
98
|
+
```
|
package/package.json
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "whatsapp-pi",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "WhatsApp integration extension for Pi",
|
|
6
|
-
"main": "whatsapp-pi.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
"whatsapp-pi.ts",
|
|
9
|
-
"src"
|
|
10
|
-
],
|
|
11
|
-
"keywords": [
|
|
12
|
-
"pi",
|
|
13
|
-
"pi-package",
|
|
14
|
-
"pi-extension",
|
|
15
|
-
"whatsapp",
|
|
16
|
-
"baileys",
|
|
17
|
-
"agent"
|
|
18
|
-
],
|
|
19
|
-
"author": "Rapha",
|
|
20
|
-
"license": "MIT",
|
|
21
|
-
"scripts": {
|
|
22
|
-
"test": "vitest run",
|
|
23
|
-
"typecheck": "tsc --noEmit"
|
|
24
|
-
},
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"@whiskeysockets/baileys": "^6.11.0",
|
|
27
|
-
"pino": "^10.3.1",
|
|
28
|
-
"qrcode-terminal": "^0.12.0"
|
|
29
|
-
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@mariozechner/pi-coding-agent": "latest",
|
|
32
|
-
"@types/node": "^20.11.0",
|
|
33
|
-
"@types/qrcode-terminal": "^0.12.2",
|
|
34
|
-
"ts-node": "^10.9.2",
|
|
35
|
-
"tsx": "^4.7.0",
|
|
36
|
-
"typescript": "^5.3.0",
|
|
37
|
-
"vitest": "^1.2.0"
|
|
38
|
-
},
|
|
39
|
-
"pi": {
|
|
40
|
-
"extensions": [
|
|
41
|
-
"./whatsapp-pi.ts"
|
|
42
|
-
],
|
|
43
|
-
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/WhatsApp.svg/512px-WhatsApp.svg.png"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "whatsapp-pi",
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "WhatsApp integration extension for Pi",
|
|
6
|
+
"main": "whatsapp-pi.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"whatsapp-pi.ts",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"pi",
|
|
13
|
+
"pi-package",
|
|
14
|
+
"pi-extension",
|
|
15
|
+
"whatsapp",
|
|
16
|
+
"baileys",
|
|
17
|
+
"agent"
|
|
18
|
+
],
|
|
19
|
+
"author": "Rapha",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@whiskeysockets/baileys": "^6.11.0",
|
|
27
|
+
"pino": "^10.3.1",
|
|
28
|
+
"qrcode-terminal": "^0.12.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@mariozechner/pi-coding-agent": "latest",
|
|
32
|
+
"@types/node": "^20.11.0",
|
|
33
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
34
|
+
"ts-node": "^10.9.2",
|
|
35
|
+
"tsx": "^4.7.0",
|
|
36
|
+
"typescript": "^5.3.0",
|
|
37
|
+
"vitest": "^1.2.0"
|
|
38
|
+
},
|
|
39
|
+
"pi": {
|
|
40
|
+
"extensions": [
|
|
41
|
+
"./whatsapp-pi.ts"
|
|
42
|
+
],
|
|
43
|
+
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/WhatsApp.svg/512px-WhatsApp.svg.png"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
export type SessionStatus = 'logged-out' | 'pairing' | 'connected' | 'disconnected';
|
|
2
|
-
|
|
3
|
-
export interface WhatsAppSession {
|
|
4
|
-
id: string;
|
|
5
|
-
status: SessionStatus;
|
|
6
|
-
credentialsPath: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface AllowList {
|
|
10
|
-
numbers: string[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface IncomingMessage {
|
|
14
|
-
id: string;
|
|
15
|
-
remoteJid: string;
|
|
16
|
-
pushName?: string;
|
|
17
|
-
text?: string;
|
|
18
|
-
timestamp: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface MessageRequest {
|
|
22
|
-
recipientJid: string;
|
|
23
|
-
text: string;
|
|
24
|
-
options?: {
|
|
25
|
-
maxRetries?: number;
|
|
26
|
-
priority?: 'high' | 'normal';
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface MessageResult {
|
|
31
|
-
success: boolean;
|
|
32
|
-
messageId?: string;
|
|
33
|
-
error?: string;
|
|
34
|
-
attempts: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export class WhatsAppError extends Error {
|
|
38
|
-
constructor(public code: string, message: string) {
|
|
39
|
-
super(message);
|
|
40
|
-
this.name = 'WhatsAppError';
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function validatePhoneNumber(number: string): boolean {
|
|
45
|
-
return /^\+[1-9]\d{1,14}$/.test(number);
|
|
46
|
-
}
|
|
1
|
+
export type SessionStatus = 'logged-out' | 'pairing' | 'connected' | 'disconnected';
|
|
2
|
+
|
|
3
|
+
export interface WhatsAppSession {
|
|
4
|
+
id: string;
|
|
5
|
+
status: SessionStatus;
|
|
6
|
+
credentialsPath: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AllowList {
|
|
10
|
+
numbers: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface IncomingMessage {
|
|
14
|
+
id: string;
|
|
15
|
+
remoteJid: string;
|
|
16
|
+
pushName?: string;
|
|
17
|
+
text?: string;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MessageRequest {
|
|
22
|
+
recipientJid: string;
|
|
23
|
+
text: string;
|
|
24
|
+
options?: {
|
|
25
|
+
maxRetries?: number;
|
|
26
|
+
priority?: 'high' | 'normal';
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MessageResult {
|
|
31
|
+
success: boolean;
|
|
32
|
+
messageId?: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
attempts: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class WhatsAppError extends Error {
|
|
38
|
+
constructor(public code: string, message: string) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'WhatsAppError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function validatePhoneNumber(number: string): boolean {
|
|
45
|
+
return /^\+[1-9]\d{1,14}$/.test(number);
|
|
46
|
+
}
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
import { downloadContentFromMessage } from '@whiskeysockets/baileys';
|
|
2
|
-
import { exec } from 'node:child_process';
|
|
3
|
-
import { promisify } from 'node:util';
|
|
4
|
-
import { writeFile, mkdir } from 'node:fs/promises';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
-
|
|
8
|
-
const execAsync = promisify(exec);
|
|
9
|
-
|
|
10
|
-
export class AudioService {
|
|
11
|
-
private readonly mediaDir = '/home/opc/.pi/whatsapp-medias';
|
|
12
|
-
private readonly whisperPath = '/home/opc/.local/bin/whisper';
|
|
13
|
-
|
|
14
|
-
constructor() {
|
|
15
|
-
if (!existsSync(this.mediaDir)) {
|
|
16
|
-
mkdir(this.mediaDir, { recursive: true }).catch(() => {});
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async transcribe(audioMessage: any): Promise<string> {
|
|
21
|
-
try {
|
|
22
|
-
const filename = `audio_${Date.now()}`;
|
|
23
|
-
const inputPath = join(this.mediaDir, `${filename}.ogg`);
|
|
24
|
-
|
|
25
|
-
// Download audio content
|
|
26
|
-
const stream = await downloadContentFromMessage(audioMessage, 'audio');
|
|
27
|
-
let buffer = Buffer.from([]);
|
|
28
|
-
for await (const chunk of stream) {
|
|
29
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
await writeFile(inputPath, buffer);
|
|
33
|
-
|
|
34
|
-
// Transcribe using Whisper
|
|
35
|
-
// Using small model for better accuracy
|
|
36
|
-
const command = `${this.whisperPath} "${inputPath}" --model small --language pt --output_format txt --output_dir "${this.mediaDir}" --fp16 False`;
|
|
37
|
-
|
|
38
|
-
await execAsync(command);
|
|
39
|
-
|
|
40
|
-
const txtPath = join(this.mediaDir, `${filename}.txt`);
|
|
41
|
-
if (existsSync(txtPath)) {
|
|
42
|
-
const fs = await import('node:fs/promises');
|
|
43
|
-
const text = await fs.readFile(txtPath, 'utf8');
|
|
44
|
-
return text.trim();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return '[Transcrição vazia]';
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.error('[AudioService] Transcription error:', error);
|
|
50
|
-
return `[Erro na transcrição: ${error instanceof Error ? error.message : String(error)}]`;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
import { downloadContentFromMessage } from '@whiskeysockets/baileys';
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
export class AudioService {
|
|
11
|
+
private readonly mediaDir = '/home/opc/.pi/whatsapp-medias';
|
|
12
|
+
private readonly whisperPath = '/home/opc/.local/bin/whisper';
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
if (!existsSync(this.mediaDir)) {
|
|
16
|
+
mkdir(this.mediaDir, { recursive: true }).catch(() => {});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async transcribe(audioMessage: any): Promise<string> {
|
|
21
|
+
try {
|
|
22
|
+
const filename = `audio_${Date.now()}`;
|
|
23
|
+
const inputPath = join(this.mediaDir, `${filename}.ogg`);
|
|
24
|
+
|
|
25
|
+
// Download audio content
|
|
26
|
+
const stream = await downloadContentFromMessage(audioMessage, 'audio');
|
|
27
|
+
let buffer = Buffer.from([]);
|
|
28
|
+
for await (const chunk of stream) {
|
|
29
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await writeFile(inputPath, buffer);
|
|
33
|
+
|
|
34
|
+
// Transcribe using Whisper
|
|
35
|
+
// Using small model for better accuracy
|
|
36
|
+
const command = `${this.whisperPath} "${inputPath}" --model small --language pt --output_format txt --output_dir "${this.mediaDir}" --fp16 False`;
|
|
37
|
+
|
|
38
|
+
await execAsync(command);
|
|
39
|
+
|
|
40
|
+
const txtPath = join(this.mediaDir, `${filename}.txt`);
|
|
41
|
+
if (existsSync(txtPath)) {
|
|
42
|
+
const fs = await import('node:fs/promises');
|
|
43
|
+
const text = await fs.readFile(txtPath, 'utf8');
|
|
44
|
+
return text.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return '[Transcrição vazia]';
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('[AudioService] Transcription error:', error);
|
|
50
|
+
return `[Erro na transcrição: ${error instanceof Error ? error.message : String(error)}]`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
import { WhatsAppService } from './whatsapp.service.js';
|
|
2
|
-
import { MessageRequest, MessageResult, WhatsAppError } from '../models/whatsapp.types.js';
|
|
3
|
-
|
|
4
|
-
export class MessageSender {
|
|
5
|
-
private whatsappService: WhatsAppService;
|
|
6
|
-
|
|
7
|
-
constructor(whatsappService: WhatsAppService) {
|
|
8
|
-
this.whatsappService = whatsappService;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Pauses execution for the specified time.
|
|
13
|
-
* @param ms Milliseconds to sleep.
|
|
14
|
-
*/
|
|
15
|
-
private async sleep(ms: number) {
|
|
16
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Waits for the WhatsApp connection to be active.
|
|
21
|
-
* @param timeoutMs Maximum time to wait in milliseconds.
|
|
22
|
-
* @throws {WhatsAppError} If connection is not established within timeout.
|
|
23
|
-
*/
|
|
24
|
-
private async waitIfOffline(timeoutMs: number = 30000): Promise<void> {
|
|
25
|
-
const start = Date.now();
|
|
26
|
-
while (this.whatsappService.getStatus() !== 'connected') {
|
|
27
|
-
if (Date.now() - start > timeoutMs) {
|
|
28
|
-
throw new WhatsAppError('TIMEOUT', 'Timed out waiting for WhatsApp connection');
|
|
29
|
-
}
|
|
30
|
-
await this.sleep(1000);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Sends a message with retry logic and connection awareness.
|
|
36
|
-
* @param request The message recipient and content.
|
|
37
|
-
* @returns Promise resolving to a result object indicating success or failure.
|
|
38
|
-
*/
|
|
39
|
-
public async send(request: MessageRequest): Promise<MessageResult> {
|
|
40
|
-
const maxRetries = request.options?.maxRetries ?? 3;
|
|
41
|
-
let attempts = 0;
|
|
42
|
-
let lastError: any = null;
|
|
43
|
-
|
|
44
|
-
while (attempts < maxRetries) {
|
|
45
|
-
attempts++;
|
|
46
|
-
try {
|
|
47
|
-
// 1. Ensure we are online
|
|
48
|
-
await this.waitIfOffline();
|
|
49
|
-
|
|
50
|
-
// 2. Get active socket
|
|
51
|
-
const socket = this.whatsappService.getSocket();
|
|
52
|
-
if (!socket) {
|
|
53
|
-
throw new WhatsAppError('SOCKET_NOT_INIT', 'WhatsApp socket not initialized');
|
|
54
|
-
}
|
|
55
|
-
|
|
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} π`
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
success: true,
|
|
64
|
-
messageId: response.key.id,
|
|
65
|
-
attempts
|
|
66
|
-
};
|
|
67
|
-
} catch (error: any) {
|
|
68
|
-
lastError = error;
|
|
69
|
-
console.error(`[MessageSender] Attempt ${attempts} failed for ${request.recipientJid}: ${error.message}`);
|
|
70
|
-
|
|
71
|
-
// Specific handling for non-retryable errors
|
|
72
|
-
if (error.code === 'TIMEOUT') {
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 4. Backoff before retry
|
|
77
|
-
if (attempts < maxRetries) {
|
|
78
|
-
const backoff = Math.pow(2, attempts) * 1000;
|
|
79
|
-
if (this.whatsappService.isVerbose()) {
|
|
80
|
-
console.log(`[MessageSender] Retrying in ${backoff}ms...`);
|
|
81
|
-
}
|
|
82
|
-
await this.sleep(backoff);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
success: false,
|
|
89
|
-
error: lastError?.message || 'Unknown error',
|
|
90
|
-
attempts
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}
|
|
1
|
+
import { WhatsAppService } from './whatsapp.service.js';
|
|
2
|
+
import { MessageRequest, MessageResult, WhatsAppError } from '../models/whatsapp.types.js';
|
|
3
|
+
|
|
4
|
+
export class MessageSender {
|
|
5
|
+
private whatsappService: WhatsAppService;
|
|
6
|
+
|
|
7
|
+
constructor(whatsappService: WhatsAppService) {
|
|
8
|
+
this.whatsappService = whatsappService;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Pauses execution for the specified time.
|
|
13
|
+
* @param ms Milliseconds to sleep.
|
|
14
|
+
*/
|
|
15
|
+
private async sleep(ms: number) {
|
|
16
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Waits for the WhatsApp connection to be active.
|
|
21
|
+
* @param timeoutMs Maximum time to wait in milliseconds.
|
|
22
|
+
* @throws {WhatsAppError} If connection is not established within timeout.
|
|
23
|
+
*/
|
|
24
|
+
private async waitIfOffline(timeoutMs: number = 30000): Promise<void> {
|
|
25
|
+
const start = Date.now();
|
|
26
|
+
while (this.whatsappService.getStatus() !== 'connected') {
|
|
27
|
+
if (Date.now() - start > timeoutMs) {
|
|
28
|
+
throw new WhatsAppError('TIMEOUT', 'Timed out waiting for WhatsApp connection');
|
|
29
|
+
}
|
|
30
|
+
await this.sleep(1000);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Sends a message with retry logic and connection awareness.
|
|
36
|
+
* @param request The message recipient and content.
|
|
37
|
+
* @returns Promise resolving to a result object indicating success or failure.
|
|
38
|
+
*/
|
|
39
|
+
public async send(request: MessageRequest): Promise<MessageResult> {
|
|
40
|
+
const maxRetries = request.options?.maxRetries ?? 3;
|
|
41
|
+
let attempts = 0;
|
|
42
|
+
let lastError: any = null;
|
|
43
|
+
|
|
44
|
+
while (attempts < maxRetries) {
|
|
45
|
+
attempts++;
|
|
46
|
+
try {
|
|
47
|
+
// 1. Ensure we are online
|
|
48
|
+
await this.waitIfOffline();
|
|
49
|
+
|
|
50
|
+
// 2. Get active socket
|
|
51
|
+
const socket = this.whatsappService.getSocket();
|
|
52
|
+
if (!socket) {
|
|
53
|
+
throw new WhatsAppError('SOCKET_NOT_INIT', 'WhatsApp socket not initialized');
|
|
54
|
+
}
|
|
55
|
+
|
|
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} π`
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
messageId: response.key.id,
|
|
65
|
+
attempts
|
|
66
|
+
};
|
|
67
|
+
} catch (error: any) {
|
|
68
|
+
lastError = error;
|
|
69
|
+
console.error(`[MessageSender] Attempt ${attempts} failed for ${request.recipientJid}: ${error.message}`);
|
|
70
|
+
|
|
71
|
+
// Specific handling for non-retryable errors
|
|
72
|
+
if (error.code === 'TIMEOUT') {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 4. Backoff before retry
|
|
77
|
+
if (attempts < maxRetries) {
|
|
78
|
+
const backoff = Math.pow(2, attempts) * 1000;
|
|
79
|
+
if (this.whatsappService.isVerbose()) {
|
|
80
|
+
console.log(`[MessageSender] Retrying in ${backoff}ms...`);
|
|
81
|
+
}
|
|
82
|
+
await this.sleep(backoff);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
error: lastError?.message || 'Unknown error',
|
|
90
|
+
attempts
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|