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 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+
@@ -0,0 +1,7 @@
1
+ environment: "development"
2
+ log_level: 4
3
+ server:
4
+ port: 9400
5
+ host: "0.0.0.0"
6
+ database:
7
+ path: "data/whatsapp.db"
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
+ });