whatsapp-rpc 0.0.4
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 +139 -0
- package/configs/config.yaml +7 -0
- package/package.json +64 -0
- package/scripts/clean.cjs +66 -0
- package/scripts/cli.js +171 -0
- package/scripts/download-binary.js +200 -0
- package/src/go/cmd/server/main.go +91 -0
- package/src/go/config/config.go +49 -0
- package/src/go/rpc/rpc.go +446 -0
- package/src/go/rpc/server.go +112 -0
- package/src/go/whatsapp/history.go +166 -0
- package/src/go/whatsapp/messages.go +390 -0
- package/src/go/whatsapp/service.go +2130 -0
- package/src/go/whatsapp/types.go +261 -0
- package/src/python/pyproject.toml +15 -0
- package/src/python/whatsapp_rpc/__init__.py +4 -0
- package/src/python/whatsapp_rpc/client.py +427 -0
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# WhatsApp RPC
|
|
2
|
+
|
|
3
|
+
WebSocket JSON-RPC 2.0 API for WhatsApp.
|
|
4
|
+
|
|
5
|
+
<img width="1280" height="676" alt="image" src="https://github.com/user-attachments/assets/ddc7324a-8c4d-4557-ac6f-cb091a8ce31f" />
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
11
|
+
│ Your App │ │ Web UI │ │ Python Client │
|
|
12
|
+
│ (Any Language) │ │ (Flask) │ │ Library │
|
|
13
|
+
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
|
14
|
+
│ │ │
|
|
15
|
+
│ WebSocket │ HTTP │
|
|
16
|
+
│ JSON-RPC 2.0 │ :5000 │
|
|
17
|
+
│ │ │
|
|
18
|
+
└───────────┬───────────┴───────────────────────┘
|
|
19
|
+
│
|
|
20
|
+
▼
|
|
21
|
+
┌───────────────────────┐
|
|
22
|
+
│ Go WebSocket API │
|
|
23
|
+
│ :9400 │
|
|
24
|
+
│ ┌─────────────────┐ │
|
|
25
|
+
│ │ RPC Handler │ │
|
|
26
|
+
│ │ Rate Limiter │ │
|
|
27
|
+
│ │ Event Emitter │ │
|
|
28
|
+
│ └────────┬────────┘ │
|
|
29
|
+
└───────────┼───────────┘
|
|
30
|
+
│
|
|
31
|
+
▼
|
|
32
|
+
┌───────────────────────┐
|
|
33
|
+
│ WhatsApp Service │
|
|
34
|
+
│ ┌─────────────────┐ │
|
|
35
|
+
│ │ whatsmeow │ │
|
|
36
|
+
│ │ (Go Library) │ │
|
|
37
|
+
│ └────────┬────────┘ │
|
|
38
|
+
└───────────┼───────────┘
|
|
39
|
+
│
|
|
40
|
+
▼
|
|
41
|
+
┌───────────────────────┐
|
|
42
|
+
│ WhatsApp Servers │
|
|
43
|
+
└───────────────────────┘
|
|
44
|
+
|
|
45
|
+
Data Storage:
|
|
46
|
+
┌─────────────────────────────────┐
|
|
47
|
+
│ data/ │
|
|
48
|
+
│ ├── whatsapp.db (SQLite) │
|
|
49
|
+
│ ├── qr/*.png (QR codes) │
|
|
50
|
+
│ └── groups.json (Group cache)│
|
|
51
|
+
└─────────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Setup
|
|
55
|
+
|
|
56
|
+
### Native
|
|
57
|
+
```bash
|
|
58
|
+
npm start # Start API + Web UI
|
|
59
|
+
npm stop # Stop all
|
|
60
|
+
npm run restart # Restart all
|
|
61
|
+
npm run status # Check if running
|
|
62
|
+
npm run api # Start API only
|
|
63
|
+
npm run web # Start Web UI only
|
|
64
|
+
npm run build # Build Go binary
|
|
65
|
+
npm run clean # Full cleanup (bin, data, node_modules)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
All commands auto-install dependencies if missing.
|
|
69
|
+
|
|
70
|
+
### Docker
|
|
71
|
+
```bash
|
|
72
|
+
docker-compose up -d # Start
|
|
73
|
+
docker-compose down # Stop
|
|
74
|
+
docker-compose logs -f # View logs
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Endpoints
|
|
78
|
+
|
|
79
|
+
| Port | Service |
|
|
80
|
+
|------|---------|
|
|
81
|
+
| 9400 | WebSocket API - `ws://localhost:9400/ws/rpc` |
|
|
82
|
+
| 5000 | Web UI - `http://localhost:5000` |
|
|
83
|
+
|
|
84
|
+
## API
|
|
85
|
+
|
|
86
|
+
Connect via WebSocket and send JSON-RPC 2.0 requests:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{"jsonrpc": "2.0", "id": 1, "method": "send", "params": {"phone": "1234567890", "type": "text", "message": "Hello"}}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Methods
|
|
93
|
+
|
|
94
|
+
| Method | Params | Description |
|
|
95
|
+
|--------|--------|-------------|
|
|
96
|
+
| `status` | - | Connection status |
|
|
97
|
+
| `start` | - | Start WhatsApp |
|
|
98
|
+
| `stop` | - | Stop WhatsApp |
|
|
99
|
+
| `restart` | - | Full reset (logout, delete DB, new QR) |
|
|
100
|
+
| `qr` | - | Get QR code (base64 PNG) |
|
|
101
|
+
| `send` | `phone/group_id`, `type`, `message/media_data` | Send message |
|
|
102
|
+
| `media` | `message_id` | Download received media |
|
|
103
|
+
| `groups` | - | List all groups |
|
|
104
|
+
| `group_info` | `group_id` | Get group details |
|
|
105
|
+
| `contact_check` | `phones[]` | Check if numbers are on WhatsApp |
|
|
106
|
+
|
|
107
|
+
Full schema: [schema.json](schema.json)
|
|
108
|
+
|
|
109
|
+
## Events
|
|
110
|
+
|
|
111
|
+
Server pushes events as JSON-RPC notifications (no `id`):
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{"jsonrpc": "2.0", "method": "event.message_received", "params": {...}}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Events: `connected`, `disconnected`, `qr_code`, `message_received`, `message_sent`
|
|
118
|
+
|
|
119
|
+
## Data Files
|
|
120
|
+
|
|
121
|
+
- `data/whatsapp.db` - SQLite database (session, contacts)
|
|
122
|
+
- `data/qr/*.png` - QR code images
|
|
123
|
+
- `data/groups.json` - Auto-generated on connection (indexed group data)
|
|
124
|
+
|
|
125
|
+
## Structure
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
src/go/ # Go backend
|
|
129
|
+
src/python/ # Python client library
|
|
130
|
+
web/ # Flask web UI
|
|
131
|
+
scripts/ # CLI tools
|
|
132
|
+
configs/ # YAML config
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Requirements
|
|
136
|
+
|
|
137
|
+
- Go 1.21+
|
|
138
|
+
- Python 3.8+
|
|
139
|
+
- Node.js 16+
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "whatsapp-rpc",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "WhatsApp WebSocket RPC Server with JSON-RPC 2.0 protocol",
|
|
6
|
+
"author": "Rohit G <trohitg@gmail.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/trohitg/whatsapp-rpc#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/trohitg/whatsapp-rpc.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/trohitg/whatsapp-rpc/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"whatsapp",
|
|
18
|
+
"rpc",
|
|
19
|
+
"websocket",
|
|
20
|
+
"json-rpc",
|
|
21
|
+
"messaging",
|
|
22
|
+
"automation"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"whatsapp-rpc": "./scripts/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin/",
|
|
29
|
+
"scripts/",
|
|
30
|
+
"src/",
|
|
31
|
+
"configs/",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"postinstall": "node scripts/download-binary.js",
|
|
42
|
+
"prestart": "node -e \"require('fs').existsSync('node_modules')||require('child_process').execSync('npm i',{stdio:'inherit'})\"",
|
|
43
|
+
"start": "node scripts/cli.js start",
|
|
44
|
+
"prestop": "node -e \"require('fs').existsSync('node_modules')||require('child_process').execSync('npm i',{stdio:'inherit'})\"",
|
|
45
|
+
"stop": "node scripts/cli.js stop",
|
|
46
|
+
"prerestart": "node -e \"require('fs').existsSync('node_modules')||require('child_process').execSync('npm i',{stdio:'inherit'})\"",
|
|
47
|
+
"restart": "node scripts/cli.js restart",
|
|
48
|
+
"prestatus": "node -e \"require('fs').existsSync('node_modules')||require('child_process').execSync('npm i',{stdio:'inherit'})\"",
|
|
49
|
+
"status": "node scripts/cli.js status",
|
|
50
|
+
"preapi": "node -e \"require('fs').existsSync('node_modules')||require('child_process').execSync('npm i',{stdio:'inherit'})\"",
|
|
51
|
+
"api": "node scripts/cli.js api --foreground",
|
|
52
|
+
"preweb": "node -e \"require('fs').existsSync('node_modules')||require('child_process').execSync('npm i',{stdio:'inherit'})\"",
|
|
53
|
+
"web": "node scripts/cli.js web",
|
|
54
|
+
"prebuild": "node -e \"require('fs').existsSync('node_modules')||require('child_process').execSync('npm i',{stdio:'inherit'})\"",
|
|
55
|
+
"build": "node -e \"const fs=require('fs'),p=process.platform==='win32'?'.exe':'',d='bin';fs.existsSync(d)||fs.mkdirSync(d);require('child_process').execSync('go build -v -o '+d+'/whatsapp-rpc-server'+p+' ./src/go/cmd/server',{stdio:'inherit'});console.log('Build complete: '+d+'/whatsapp-rpc-server'+p)\"",
|
|
56
|
+
"clean": "node scripts/clean.cjs"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"chalk": "^5.3.0",
|
|
60
|
+
"commander": "^11.1.0",
|
|
61
|
+
"execa": "^8.0.1",
|
|
62
|
+
"kill-port": "^2.0.1"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Standalone clean script - no npm dependencies required
|
|
3
|
+
const { existsSync, unlinkSync, readdirSync, rmSync } = require('fs');
|
|
4
|
+
const { join } = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const ROOT = join(__dirname, '..');
|
|
8
|
+
const BIN = process.platform === 'win32' ? 'whatsapp-rpc-server.exe' : 'whatsapp-rpc-server';
|
|
9
|
+
const BIN_DIR = join(ROOT, 'bin');
|
|
10
|
+
|
|
11
|
+
const log = (msg, color) => {
|
|
12
|
+
const colors = { green: '\x1b[32m', blue: '\x1b[34m', yellow: '\x1b[33m', reset: '\x1b[0m' };
|
|
13
|
+
console.log(`${colors[color] || ''}${msg}${colors.reset}`);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Kill process on port
|
|
17
|
+
const killPort = (port, name) => {
|
|
18
|
+
try {
|
|
19
|
+
if (process.platform === 'win32') {
|
|
20
|
+
execSync(`for /f "tokens=5" %a in ('netstat -aon ^| findstr :${port} ^| findstr LISTENING') do taskkill /F /PID %a`, { stdio: 'ignore', shell: 'cmd.exe' });
|
|
21
|
+
} else {
|
|
22
|
+
execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || true`, { stdio: 'ignore' });
|
|
23
|
+
}
|
|
24
|
+
log(`${name} stopped`, 'green');
|
|
25
|
+
} catch { log(`${name} not running`, 'yellow'); }
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
log('Stopping all processes...', 'blue');
|
|
29
|
+
killPort(9400, 'API');
|
|
30
|
+
killPort(5000, 'Web');
|
|
31
|
+
|
|
32
|
+
// Remove binary
|
|
33
|
+
const bin = join(BIN_DIR, BIN);
|
|
34
|
+
if (existsSync(bin)) { unlinkSync(bin); log(`Removed ${BIN}`, 'green'); }
|
|
35
|
+
|
|
36
|
+
// Remove bin directory if empty
|
|
37
|
+
if (existsSync(BIN_DIR) && readdirSync(BIN_DIR).length === 0) {
|
|
38
|
+
rmSync(BIN_DIR, { recursive: true }); log('Removed bin/', 'green');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Remove entire data directory (database, QR codes, etc.)
|
|
42
|
+
const dataDir = join(ROOT, 'data');
|
|
43
|
+
if (existsSync(dataDir)) {
|
|
44
|
+
rmSync(dataDir, { recursive: true });
|
|
45
|
+
log('Removed data/', 'green');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Remove any .db files in project root (legacy locations)
|
|
49
|
+
readdirSync(ROOT).filter(f => f.endsWith('.db') || f.endsWith('.db-wal') || f.endsWith('.db-shm')).forEach(f => {
|
|
50
|
+
unlinkSync(join(ROOT, f));
|
|
51
|
+
log(`Removed ${f}`, 'green');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Remove node_modules
|
|
55
|
+
const nodeModules = join(ROOT, 'node_modules');
|
|
56
|
+
if (existsSync(nodeModules)) { rmSync(nodeModules, { recursive: true }); log('Removed node_modules/', 'green'); }
|
|
57
|
+
|
|
58
|
+
// Remove Python cache
|
|
59
|
+
const pycache = join(ROOT, 'web', '__pycache__');
|
|
60
|
+
if (existsSync(pycache)) { rmSync(pycache, { recursive: true }); log('Removed web/__pycache__/', 'green'); }
|
|
61
|
+
|
|
62
|
+
// Remove package-lock.json
|
|
63
|
+
const lockFile = join(ROOT, 'package-lock.json');
|
|
64
|
+
if (existsSync(lockFile)) { unlinkSync(lockFile); log('Removed package-lock.json', 'green'); }
|
|
65
|
+
|
|
66
|
+
log('Clean complete', 'green');
|
package/scripts/cli.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import killPort from 'kill-port';
|
|
6
|
+
import { Socket } from 'net';
|
|
7
|
+
import { execSync, spawn } from 'child_process';
|
|
8
|
+
import { existsSync, statSync, unlinkSync, readdirSync, rmSync } from 'fs';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const ROOT = join(__dirname, '..');
|
|
14
|
+
const API_PORT = 9400, WEB_PORT = 5000;
|
|
15
|
+
const BIN = process.platform === 'win32' ? 'whatsapp-rpc-server.exe' : 'whatsapp-rpc-server';
|
|
16
|
+
const BIN_DIR = join(ROOT, 'bin');
|
|
17
|
+
|
|
18
|
+
const log = (m, c = 'blue') => console.log(chalk[c](m));
|
|
19
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
20
|
+
|
|
21
|
+
const portUp = port => new Promise(r => {
|
|
22
|
+
const s = new Socket();
|
|
23
|
+
s.setTimeout(2000);
|
|
24
|
+
s.on('connect', () => { s.destroy(); r(true); });
|
|
25
|
+
s.on('timeout', () => { s.destroy(); r(false); });
|
|
26
|
+
s.on('error', () => r(false));
|
|
27
|
+
s.connect(port, '127.0.0.1');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const wait = async (port, ms = 10000) => {
|
|
31
|
+
const t = Date.now();
|
|
32
|
+
while (Date.now() - t < ms) { if (await portUp(port)) return true; await sleep(500); }
|
|
33
|
+
return false;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const kill = async (port, name) => {
|
|
37
|
+
try { await killPort(port); log(`${name} stopped`, 'green'); }
|
|
38
|
+
catch { log(`${name} not running`, 'yellow'); }
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const py = () => {
|
|
42
|
+
try { execSync('python --version', { stdio: 'ignore' }); return 'python'; }
|
|
43
|
+
catch { try { execSync('python3 --version', { stdio: 'ignore' }); return 'python3'; } catch { return null; } }
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const hasGo = () => {
|
|
47
|
+
try { execSync('go version', { stdio: 'ignore' }); return true; }
|
|
48
|
+
catch { return false; }
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
async function api(foreground = false) {
|
|
52
|
+
if (await portUp(API_PORT)) { log(`API already on ${API_PORT}`, 'yellow'); return; }
|
|
53
|
+
const bin = join(BIN_DIR, BIN);
|
|
54
|
+
if (!existsSync(bin)) {
|
|
55
|
+
if (!hasGo()) {
|
|
56
|
+
log('Go is not installed. Please either:', 'red');
|
|
57
|
+
log(' 1. Install Go: https://go.dev/dl/', 'yellow');
|
|
58
|
+
log(' 2. Or run "npm run build" on a machine with Go installed', 'yellow');
|
|
59
|
+
log(' 3. Or copy the pre-built binary to: ' + bin, 'yellow');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
log('Building...', 'yellow');
|
|
63
|
+
await build();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (foreground) {
|
|
67
|
+
// Run in foreground - will receive Ctrl+C signals
|
|
68
|
+
const proc = spawn(bin, [], { cwd: ROOT, stdio: 'inherit' });
|
|
69
|
+
proc.on('close', (code) => process.exit(code || 0));
|
|
70
|
+
process.on('SIGINT', () => { proc.kill('SIGINT'); });
|
|
71
|
+
process.on('SIGTERM', () => { proc.kill('SIGTERM'); });
|
|
72
|
+
log(`API: ws://localhost:${API_PORT}/ws/rpc`, 'green');
|
|
73
|
+
} else {
|
|
74
|
+
// Run detached in background
|
|
75
|
+
spawn(bin, [], { cwd: ROOT, detached: true, stdio: 'ignore' }).unref();
|
|
76
|
+
if (await wait(API_PORT)) log(`API: ws://localhost:${API_PORT}/ws/rpc`, 'green');
|
|
77
|
+
else log('API failed to start', 'red');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function web() {
|
|
82
|
+
if (await portUp(WEB_PORT)) { log(`Web already on ${WEB_PORT}`, 'yellow'); return; }
|
|
83
|
+
const p = py(); if (!p) { log('Python not found', 'red'); return; }
|
|
84
|
+
if (!await portUp(API_PORT)) log('Warning: API not running', 'yellow');
|
|
85
|
+
spawn(p, ['app.py'], { cwd: join(ROOT, 'web'), detached: true, stdio: 'ignore' }).unref();
|
|
86
|
+
if (await wait(WEB_PORT)) log(`Web: http://localhost:${WEB_PORT}`, 'green');
|
|
87
|
+
else log('Web failed to start', 'red');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function start() { await api(); await sleep(1000); await web(); }
|
|
91
|
+
async function stop() { await kill(API_PORT, 'API'); await kill(WEB_PORT, 'Web'); }
|
|
92
|
+
|
|
93
|
+
async function status() {
|
|
94
|
+
const a = await portUp(API_PORT), w = await portUp(WEB_PORT);
|
|
95
|
+
log(`API (${API_PORT}): ${a ? 'UP' : 'DOWN'}`, a ? 'green' : 'red');
|
|
96
|
+
log(`Web (${WEB_PORT}): ${w ? 'UP' : 'DOWN'}`, w ? 'green' : 'red');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function build() {
|
|
100
|
+
const bin = join(BIN_DIR, BIN);
|
|
101
|
+
|
|
102
|
+
// Skip if binary already exists (e.g., downloaded from GitHub Releases)
|
|
103
|
+
if (existsSync(bin)) {
|
|
104
|
+
log(`Binary already exists: ${BIN} (${(statSync(bin).size / 1024 / 1024).toFixed(1)}MB)`, 'green');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Build from source requires Go
|
|
109
|
+
if (!hasGo()) {
|
|
110
|
+
log('Go is not installed and no pre-built binary found.', 'red');
|
|
111
|
+
log('Install Go from: https://go.dev/dl/', 'yellow');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!existsSync(BIN_DIR)) { await execa('mkdir', ['-p', BIN_DIR]); }
|
|
116
|
+
await execa('go', ['build', '-o', bin, './src/go/cmd/server'], { cwd: ROOT, stdio: 'inherit' });
|
|
117
|
+
log(`Built: ${BIN} (${(statSync(bin).size / 1024 / 1024).toFixed(1)}MB)`, 'green');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function clean() {
|
|
121
|
+
log('Stopping all processes...', 'blue');
|
|
122
|
+
await stop();
|
|
123
|
+
await sleep(1000);
|
|
124
|
+
|
|
125
|
+
// Remove binary
|
|
126
|
+
const bin = join(BIN_DIR, BIN);
|
|
127
|
+
if (existsSync(bin)) { unlinkSync(bin); log(`Removed ${BIN}`, 'green'); }
|
|
128
|
+
|
|
129
|
+
// Remove bin directory if empty
|
|
130
|
+
if (existsSync(BIN_DIR) && readdirSync(BIN_DIR).length === 0) {
|
|
131
|
+
rmSync(BIN_DIR, { recursive: true }); log('Removed bin/', 'green');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Remove entire data directory (database, QR codes, etc.)
|
|
135
|
+
const dataDir = join(ROOT, 'data');
|
|
136
|
+
if (existsSync(dataDir)) {
|
|
137
|
+
rmSync(dataDir, { recursive: true });
|
|
138
|
+
log('Removed data/', 'green');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Remove any .db files in project root (legacy locations)
|
|
142
|
+
readdirSync(ROOT).filter(f => f.endsWith('.db') || f.endsWith('.db-wal') || f.endsWith('.db-shm')).forEach(f => {
|
|
143
|
+
unlinkSync(join(ROOT, f));
|
|
144
|
+
log(`Removed ${f}`, 'green');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Remove node_modules
|
|
148
|
+
const nodeModules = join(ROOT, 'node_modules');
|
|
149
|
+
if (existsSync(nodeModules)) { rmSync(nodeModules, { recursive: true }); log('Removed node_modules/', 'green'); }
|
|
150
|
+
|
|
151
|
+
// Remove Python cache
|
|
152
|
+
const pycache = join(ROOT, 'web', '__pycache__');
|
|
153
|
+
if (existsSync(pycache)) { rmSync(pycache, { recursive: true }); log('Removed web/__pycache__/', 'green'); }
|
|
154
|
+
|
|
155
|
+
// Remove package-lock.json
|
|
156
|
+
const lockFile = join(ROOT, 'package-lock.json');
|
|
157
|
+
if (existsSync(lockFile)) { unlinkSync(lockFile); log('Removed package-lock.json', 'green'); }
|
|
158
|
+
|
|
159
|
+
log('Clean complete', 'green');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
program.name('wa').version('1.0.0');
|
|
163
|
+
program.command('start').description('Start all').action(start);
|
|
164
|
+
program.command('stop').description('Stop all').action(stop);
|
|
165
|
+
program.command('restart').description('Restart all').action(async () => { await stop(); await sleep(1000); await start(); });
|
|
166
|
+
program.command('status').description('Status').action(status);
|
|
167
|
+
program.command('api').description('Start API only').option('-f, --foreground', 'Run in foreground (receive signals)').action((opts) => api(opts.foreground));
|
|
168
|
+
program.command('web').description('Start Web only').action(web);
|
|
169
|
+
program.command('build').description('Build binary').action(build);
|
|
170
|
+
program.command('clean').description('Clean artifacts').action(clean);
|
|
171
|
+
program.parse();
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Downloads pre-built WhatsApp RPC server binary from GitHub releases.
|
|
4
|
+
* Called automatically during npm postinstall.
|
|
5
|
+
*
|
|
6
|
+
* Skip conditions:
|
|
7
|
+
* - WHATSAPP_RPC_SKIP_BINARY_DOWNLOAD=1
|
|
8
|
+
* - CI=true (CI builds from source)
|
|
9
|
+
* - Binary already exists
|
|
10
|
+
* - Go is installed and WHATSAPP_RPC_PREFER_SOURCE=1
|
|
11
|
+
*/
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import { createWriteStream, existsSync, mkdirSync, chmodSync, readFileSync, unlinkSync } from 'fs';
|
|
14
|
+
import { resolve, dirname } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import https from 'https';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const ROOT = resolve(__dirname, '..');
|
|
20
|
+
const BIN_DIR = resolve(ROOT, 'bin');
|
|
21
|
+
|
|
22
|
+
// Read version from package.json
|
|
23
|
+
const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8'));
|
|
24
|
+
const VERSION = pkg.version;
|
|
25
|
+
|
|
26
|
+
// GitHub release URL
|
|
27
|
+
const GITHUB_REPO = 'trohitg/whatsapp-rpc';
|
|
28
|
+
const BASE_URL = `https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}`;
|
|
29
|
+
|
|
30
|
+
// Platform detection
|
|
31
|
+
function getPlatformInfo() {
|
|
32
|
+
const osMap = { 'win32': 'windows', 'darwin': 'darwin', 'linux': 'linux' };
|
|
33
|
+
const archMap = { 'x64': 'amd64', 'arm64': 'arm64' };
|
|
34
|
+
|
|
35
|
+
const os = osMap[process.platform];
|
|
36
|
+
const goarch = archMap[process.arch];
|
|
37
|
+
|
|
38
|
+
if (!os || !goarch) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
43
|
+
return { os, goarch, ext };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if Go is installed
|
|
47
|
+
function hasGo() {
|
|
48
|
+
try {
|
|
49
|
+
execSync('go version', { stdio: 'ignore' });
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Download file with redirect handling
|
|
57
|
+
function downloadFile(url, dest) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
// Remove partial download if exists
|
|
60
|
+
if (existsSync(dest)) {
|
|
61
|
+
try { unlinkSync(dest); } catch { /* ignore */ }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const request = (currentUrl, redirectCount = 0) => {
|
|
65
|
+
if (redirectCount > 5) {
|
|
66
|
+
reject(new Error('Too many redirects'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const protocol = currentUrl.startsWith('https') ? https : require('http');
|
|
71
|
+
|
|
72
|
+
protocol.get(currentUrl, (response) => {
|
|
73
|
+
// Handle redirects (GitHub releases redirect to CDN)
|
|
74
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
75
|
+
request(response.headers.location, redirectCount + 1);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (response.statusCode === 404) {
|
|
80
|
+
reject(new Error(`Binary not found: v${VERSION} may not have pre-built binaries yet`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (response.statusCode !== 200) {
|
|
85
|
+
reject(new Error(`HTTP ${response.statusCode}`));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const file = createWriteStream(dest);
|
|
90
|
+
const totalBytes = parseInt(response.headers['content-length'], 10);
|
|
91
|
+
let downloadedBytes = 0;
|
|
92
|
+
|
|
93
|
+
response.on('data', (chunk) => {
|
|
94
|
+
downloadedBytes += chunk.length;
|
|
95
|
+
if (totalBytes) {
|
|
96
|
+
const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
|
|
97
|
+
process.stdout.write(`\r Downloading: ${percent}%`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
response.pipe(file);
|
|
102
|
+
file.on('finish', () => {
|
|
103
|
+
file.close();
|
|
104
|
+
console.log(' Done');
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
file.on('error', (err) => {
|
|
108
|
+
file.close();
|
|
109
|
+
try { unlinkSync(dest); } catch { /* ignore */ }
|
|
110
|
+
reject(err);
|
|
111
|
+
});
|
|
112
|
+
}).on('error', (err) => {
|
|
113
|
+
try { unlinkSync(dest); } catch { /* ignore */ }
|
|
114
|
+
reject(err);
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
request(url);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Main
|
|
123
|
+
async function main() {
|
|
124
|
+
// Skip conditions
|
|
125
|
+
if (process.env.WHATSAPP_RPC_SKIP_BINARY_DOWNLOAD === '1') {
|
|
126
|
+
console.log('[whatsapp-rpc] Skipping binary download (WHATSAPP_RPC_SKIP_BINARY_DOWNLOAD=1)');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true') {
|
|
131
|
+
console.log('[whatsapp-rpc] Skipping binary download (CI environment - will build from source)');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const platformInfo = getPlatformInfo();
|
|
136
|
+
if (!platformInfo) {
|
|
137
|
+
console.log(`[whatsapp-rpc] Unsupported platform: ${process.platform}/${process.arch}`);
|
|
138
|
+
console.log('[whatsapp-rpc] Please build from source: npm run build');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { os, goarch, ext } = platformInfo;
|
|
143
|
+
const binaryName = `whatsapp-rpc-server-${os}-${goarch}${ext}`;
|
|
144
|
+
const downloadUrl = `${BASE_URL}/${binaryName}`;
|
|
145
|
+
const destPath = resolve(BIN_DIR, `whatsapp-rpc-server${ext}`);
|
|
146
|
+
|
|
147
|
+
// Check if binary already exists
|
|
148
|
+
if (existsSync(destPath)) {
|
|
149
|
+
console.log(`[whatsapp-rpc] Binary already exists: ${destPath}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if Go is installed and user prefers source
|
|
154
|
+
if (hasGo() && process.env.WHATSAPP_RPC_PREFER_SOURCE === '1') {
|
|
155
|
+
console.log('[whatsapp-rpc] Go is installed and WHATSAPP_RPC_PREFER_SOURCE=1, skipping download');
|
|
156
|
+
console.log('[whatsapp-rpc] Build from source with: npm run build');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`[whatsapp-rpc] Downloading pre-built binary...`);
|
|
161
|
+
console.log(` Version: v${VERSION}`);
|
|
162
|
+
console.log(` Platform: ${os}/${goarch}`);
|
|
163
|
+
|
|
164
|
+
// Create bin directory
|
|
165
|
+
if (!existsSync(BIN_DIR)) {
|
|
166
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Download binary
|
|
170
|
+
try {
|
|
171
|
+
await downloadFile(downloadUrl, destPath);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(`\n[whatsapp-rpc] Failed to download binary: ${error.message}`);
|
|
174
|
+
|
|
175
|
+
if (hasGo()) {
|
|
176
|
+
console.log('[whatsapp-rpc] Go is installed - you can build from source: npm run build');
|
|
177
|
+
} else {
|
|
178
|
+
console.log('[whatsapp-rpc] Install Go to build from source: https://go.dev/dl/');
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Set executable permission (Unix only)
|
|
184
|
+
if (process.platform !== 'win32') {
|
|
185
|
+
try {
|
|
186
|
+
chmodSync(destPath, 0o755);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.warn(`[whatsapp-rpc] Warning: Could not set executable permission: ${err.message}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(`[whatsapp-rpc] Binary installed: ${destPath}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
main().catch((err) => {
|
|
196
|
+
// Don't fail npm install - just log and continue
|
|
197
|
+
console.error('[whatsapp-rpc] Binary download error:', err.message);
|
|
198
|
+
console.log('[whatsapp-rpc] WhatsApp features will not work until binary is built.');
|
|
199
|
+
console.log('[whatsapp-rpc] Build from source with: npm run build');
|
|
200
|
+
});
|