whatsapp-rpc 0.0.13 → 0.0.15
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 +83 -0
- package/package.json +4 -1
- package/scripts/cli.js +60 -0
- package/scripts/serve-client.js +81 -0
- package/web/client/contacts.html +241 -0
- package/web/client/groups.html +311 -0
- package/web/client/index.html +310 -0
- package/web/client/js/app.js +210 -0
- package/web/client/js/rpc-client.js +210 -0
- package/web/client/messages.html +444 -0
- package/web/client/messaging.html +665 -0
- package/web/client/send.html +261 -0
- package/web/client/settings.html +453 -0
package/README.md
CHANGED
|
@@ -78,11 +78,32 @@ npx whatsapp-rpc restart # Restart API server
|
|
|
78
78
|
npx whatsapp-rpc status # Check if running
|
|
79
79
|
npx whatsapp-rpc api --foreground # Run in foreground (for Docker)
|
|
80
80
|
npx whatsapp-rpc build # Build from source (requires Go)
|
|
81
|
+
npx whatsapp-rpc web # Start standalone web dashboard (no Python needed)
|
|
82
|
+
npx whatsapp-rpc dev # Start API + web dashboard together
|
|
81
83
|
|
|
82
84
|
# Custom port
|
|
83
85
|
npx whatsapp-rpc start --port 8080
|
|
84
86
|
```
|
|
85
87
|
|
|
88
|
+
## Web Dashboard
|
|
89
|
+
|
|
90
|
+
A standalone static web client that connects directly to the Go WebSocket backend -- no Python/Flask required:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Start API server first
|
|
94
|
+
npx whatsapp-rpc start
|
|
95
|
+
|
|
96
|
+
# Then start the web dashboard
|
|
97
|
+
npx whatsapp-rpc web --port 3001 --ws-port 9400
|
|
98
|
+
|
|
99
|
+
# Or start both together
|
|
100
|
+
npx whatsapp-rpc dev
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Open `http://localhost:3001` for the full dashboard with pages for messaging, groups, contacts, and settings.
|
|
104
|
+
|
|
105
|
+
The web client is in `web/client/` and can also be served by any static file server. Set `window.WS_PORT` to point to the Go backend port.
|
|
106
|
+
|
|
86
107
|
## Docker
|
|
87
108
|
|
|
88
109
|
```dockerfile
|
|
@@ -461,6 +482,67 @@ asyncio.run(main())
|
|
|
461
482
|
|
|
462
483
|
See [src/python/README.md](src/python/README.md) for full Python client documentation.
|
|
463
484
|
|
|
485
|
+
## Android Integration
|
|
486
|
+
|
|
487
|
+
Cross-compile the server for Android and embed it in your app:
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
# Build for all platforms including Android
|
|
491
|
+
npm run build-cross
|
|
492
|
+
|
|
493
|
+
# Or manually:
|
|
494
|
+
# arm64 real device
|
|
495
|
+
CGO_ENABLED=0 GOOS=android GOARCH=arm64 go build -ldflags="-s -w" -o libwhatsapp-rpc.so ./src/go/cmd/server
|
|
496
|
+
|
|
497
|
+
# x86_64 emulator (use GOOS=linux, not android)
|
|
498
|
+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o libwhatsapp-rpc-x86_64.so ./src/go/cmd/server
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Place binaries in your Android project:
|
|
502
|
+
```
|
|
503
|
+
app/android/app/src/main/jniLibs/
|
|
504
|
+
arm64-v8a/libwhatsapp-rpc.so
|
|
505
|
+
x86_64/libwhatsapp-rpc-x86_64.so
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Required Gradle config (`build.gradle.kts`):
|
|
509
|
+
```kotlin
|
|
510
|
+
android {
|
|
511
|
+
packaging { jniLibs { useLegacyPackaging = true } }
|
|
512
|
+
defaultConfig { ndk { abiFilters += listOf("x86_64", "arm64-v8a") } }
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Launch from Kotlin:
|
|
517
|
+
```kotlin
|
|
518
|
+
val binary = File(applicationInfo.nativeLibraryDir, "libwhatsapp-rpc.so")
|
|
519
|
+
val pb = ProcessBuilder(binary.absolutePath)
|
|
520
|
+
pb.environment()["SSL_CERT_DIR"] = "/system/etc/security/cacerts"
|
|
521
|
+
pb.environment()["WHATSAPP_RPC_ANDROID"] = "1" // enables Android DNS resolver
|
|
522
|
+
pb.start()
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Then connect via WebSocket at `ws://127.0.0.1:9400/ws/rpc`.
|
|
526
|
+
|
|
527
|
+
### CrossMeow (Flutter Example App)
|
|
528
|
+
|
|
529
|
+
A complete Flutter Android app is in `examples/android/`. It bundles the Go binary and the static web client into a single APK:
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
cd examples/android
|
|
533
|
+
|
|
534
|
+
# Build Go binary for emulator
|
|
535
|
+
cd ../../ && npm run build-cross # or manually cross-compile
|
|
536
|
+
|
|
537
|
+
# Copy binary to jniLibs
|
|
538
|
+
cp bin/libwhatsapp-rpc-android-arm64.so examples/android/android/app/src/main/jniLibs/arm64-v8a/libwhatsapp-rpc.so
|
|
539
|
+
|
|
540
|
+
# Build APK
|
|
541
|
+
cd examples/android && flutter build apk --debug
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
The app uses a Dart HTTP server to serve the web client from Flutter assets, with a WebView loading the dashboard. See [examples/android/](examples/android/) for the full source.
|
|
545
|
+
|
|
464
546
|
## Requirements
|
|
465
547
|
|
|
466
548
|
- Node.js 18+
|
|
@@ -468,6 +550,7 @@ See [src/python/README.md](src/python/README.md) for full Python client document
|
|
|
468
550
|
- Linux (amd64, arm64)
|
|
469
551
|
- macOS (amd64, arm64)
|
|
470
552
|
- Windows (amd64)
|
|
553
|
+
- Android (arm64, x86_64)
|
|
471
554
|
|
|
472
555
|
## License
|
|
473
556
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whatsapp-rpc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WhatsApp WebSocket RPC Server with JSON-RPC 2.0 protocol",
|
|
6
6
|
"author": "Rohit G <trohitg@gmail.com>",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"bin/",
|
|
29
29
|
"scripts/",
|
|
30
30
|
"configs/",
|
|
31
|
+
"web/client/",
|
|
31
32
|
"README.md"
|
|
32
33
|
],
|
|
33
34
|
"publishConfig": {
|
|
@@ -44,8 +45,10 @@
|
|
|
44
45
|
"status": "node scripts/cli.js status",
|
|
45
46
|
"api": "node scripts/cli.js api --foreground",
|
|
46
47
|
"web": "node scripts/cli.js web",
|
|
48
|
+
"dev": "node scripts/cli.js dev",
|
|
47
49
|
"prebuild": "node -e \"require('fs').existsSync('node_modules') || process.exit(1)\" || npm install",
|
|
48
50
|
"build": "node scripts/cli.js build",
|
|
51
|
+
"build-cross": "node scripts/cli.js build-cross",
|
|
49
52
|
"preclean": "node -e \"require('fs').existsSync('node_modules') || process.exit(1)\" || npm install",
|
|
50
53
|
"clean": "node scripts/cli.js clean"
|
|
51
54
|
},
|
package/scripts/cli.js
CHANGED
|
@@ -162,6 +162,63 @@ async function clean(opts = {}) {
|
|
|
162
162
|
log('Clean complete', 'green');
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
const CROSS_TARGETS = [
|
|
166
|
+
{ goos: 'linux', goarch: 'amd64', output: 'whatsapp-rpc-server-linux-amd64' },
|
|
167
|
+
{ goos: 'linux', goarch: 'arm64', output: 'whatsapp-rpc-server-linux-arm64' },
|
|
168
|
+
{ goos: 'darwin', goarch: 'amd64', output: 'whatsapp-rpc-server-darwin-amd64' },
|
|
169
|
+
{ goos: 'darwin', goarch: 'arm64', output: 'whatsapp-rpc-server-darwin-arm64' },
|
|
170
|
+
{ goos: 'windows', goarch: 'amd64', output: 'whatsapp-rpc-server-windows-amd64.exe' },
|
|
171
|
+
{ goos: 'android', goarch: 'arm64', output: 'libwhatsapp-rpc-android-arm64.so' },
|
|
172
|
+
{ goos: 'linux', goarch: 'amd64', output: 'libwhatsapp-rpc-android-x86_64.so', label: 'android-x86_64' },
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
async function buildCross() {
|
|
176
|
+
if (!hasGo()) {
|
|
177
|
+
log('Go is not installed. Install from: https://go.dev/dl/', 'red');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
if (!existsSync(BIN_DIR)) {
|
|
181
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const target of CROSS_TARGETS) {
|
|
185
|
+
const label = target.label || `${target.goos}/${target.goarch}`;
|
|
186
|
+
const out = join(BIN_DIR, target.output);
|
|
187
|
+
log(`Building ${label} -> ${target.output}`, 'blue');
|
|
188
|
+
await execa('go', ['build', '-ldflags=-s -w', '-o', out, './src/go/cmd/server'], {
|
|
189
|
+
cwd: ROOT,
|
|
190
|
+
stdio: 'inherit',
|
|
191
|
+
env: { ...process.env, GOOS: target.goos, GOARCH: target.goarch, CGO_ENABLED: '0' },
|
|
192
|
+
});
|
|
193
|
+
log(` ${target.output} (${(statSync(out).size / 1024 / 1024).toFixed(1)}MB)`, 'green');
|
|
194
|
+
}
|
|
195
|
+
log(`All ${CROSS_TARGETS.length} binaries built`, 'green');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function web(opts = {}) {
|
|
199
|
+
const webPort = opts.port ? parseInt(opts.port) : 3001;
|
|
200
|
+
const wsPort = opts.wsPort ? parseInt(opts.wsPort) : getPort(opts);
|
|
201
|
+
const script = join(__dirname, 'serve-client.js');
|
|
202
|
+
|
|
203
|
+
log(`Starting web UI on port ${webPort} (backend: ${wsPort})...`, 'blue');
|
|
204
|
+
spawn('node', [script], {
|
|
205
|
+
cwd: ROOT,
|
|
206
|
+
stdio: 'inherit',
|
|
207
|
+
env: { ...process.env, WEB_PORT: String(webPort), WS_PORT: String(wsPort) },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function dev(opts = {}) {
|
|
212
|
+
const port = getPort(opts);
|
|
213
|
+
const webPort = opts.webPort ? parseInt(opts.webPort) : 3001;
|
|
214
|
+
|
|
215
|
+
// Start API server in background
|
|
216
|
+
await api({ ...opts, foreground: false });
|
|
217
|
+
|
|
218
|
+
// Start web UI in foreground
|
|
219
|
+
await web({ port: String(webPort), wsPort: String(port) });
|
|
220
|
+
}
|
|
221
|
+
|
|
165
222
|
// Global port option for all commands
|
|
166
223
|
const portOption = ['-p, --port <port>', 'API port (default: 9400, or PORT/WHATSAPP_RPC_PORT env var)'];
|
|
167
224
|
|
|
@@ -171,6 +228,9 @@ program.command('stop').description('Stop API server').option(...portOption).act
|
|
|
171
228
|
program.command('restart').description('Restart API server').option(...portOption).action(async (opts) => { await stop(opts); await sleep(1000); await start(opts); });
|
|
172
229
|
program.command('status').description('Show server status').option(...portOption).action(status);
|
|
173
230
|
program.command('api').description('Start API server').option('-f, --foreground', 'Run in foreground').option(...portOption).action(api);
|
|
231
|
+
program.command('web').description('Start web dashboard (static client)').option('-p, --port <port>', 'Web UI port (default: 3001)').option('--ws-port <port>', 'Go backend WebSocket port (default: 9400)').action(web);
|
|
232
|
+
program.command('dev').description('Start API server + web dashboard').option(...portOption).option('--web-port <port>', 'Web UI port (default: 3001)').action(dev);
|
|
174
233
|
program.command('build').description('Build binary from source (requires Go)').action(build);
|
|
234
|
+
program.command('build-cross').description('Cross-compile binaries for all platforms (desktop + Android)').action(buildCross);
|
|
175
235
|
program.command('clean').description('Full cleanup (stop server, remove bin/, data/, node_modules/)').action(clean);
|
|
176
236
|
program.parse();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Static file server for the whatsapp-rpc web client.
|
|
4
|
+
* Serves web/client/ on localhost, with dynamic /config.js for WebSocket port injection.
|
|
5
|
+
*
|
|
6
|
+
* Based on MachinaOs/scripts/serve-client.js pattern.
|
|
7
|
+
*
|
|
8
|
+
* Environment variables:
|
|
9
|
+
* WEB_PORT - Port for the web UI (default: 3001)
|
|
10
|
+
* WS_PORT - Go backend WebSocket port (default: 9400)
|
|
11
|
+
*/
|
|
12
|
+
import { createServer } from 'http';
|
|
13
|
+
import { readFile } from 'fs';
|
|
14
|
+
import { extname, resolve } from 'path';
|
|
15
|
+
import { dirname } from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const CLIENT_DIR = resolve(__dirname, '..', 'web', 'client');
|
|
20
|
+
const PORT = parseInt(process.env.WEB_PORT) || 3001;
|
|
21
|
+
const WS_PORT = parseInt(process.env.WS_PORT || process.env.WHATSAPP_RPC_PORT || process.env.PORT) || 9400;
|
|
22
|
+
|
|
23
|
+
const MIME_TYPES = {
|
|
24
|
+
'.html': 'text/html',
|
|
25
|
+
'.js': 'text/javascript',
|
|
26
|
+
'.css': 'text/css',
|
|
27
|
+
'.json': 'application/json',
|
|
28
|
+
'.png': 'image/png',
|
|
29
|
+
'.jpg': 'image/jpeg',
|
|
30
|
+
'.gif': 'image/gif',
|
|
31
|
+
'.svg': 'image/svg+xml',
|
|
32
|
+
'.ico': 'image/x-icon',
|
|
33
|
+
'.woff': 'font/woff',
|
|
34
|
+
'.woff2': 'font/woff2',
|
|
35
|
+
'.ttf': 'font/ttf',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const server = createServer((req, res) => {
|
|
39
|
+
let url = req.url === '/' ? '/index.html' : req.url;
|
|
40
|
+
url = url.split('?')[0];
|
|
41
|
+
|
|
42
|
+
// Dynamic config — injects the Go backend WebSocket port
|
|
43
|
+
if (url === '/config.js') {
|
|
44
|
+
res.writeHead(200, { 'Content-Type': 'text/javascript' });
|
|
45
|
+
res.end(`window.WS_PORT = ${WS_PORT};`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const filePath = resolve(CLIENT_DIR, '.' + url);
|
|
50
|
+
|
|
51
|
+
// Prevent path traversal
|
|
52
|
+
if (!filePath.startsWith(CLIENT_DIR)) {
|
|
53
|
+
res.writeHead(403);
|
|
54
|
+
res.end('Forbidden');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
readFile(filePath, (err, content) => {
|
|
59
|
+
if (err) {
|
|
60
|
+
if (err.code === 'ENOENT' && !extname(url)) {
|
|
61
|
+
// SPA fallback
|
|
62
|
+
readFile(resolve(CLIENT_DIR, 'index.html'), (err2, indexContent) => {
|
|
63
|
+
if (err2) { res.writeHead(404); res.end('Not found'); }
|
|
64
|
+
else { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(indexContent); }
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
res.writeHead(404);
|
|
68
|
+
res.end('Not found');
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
const ext = extname(filePath);
|
|
72
|
+
res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
|
|
73
|
+
res.end(content);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
server.listen(PORT, '0.0.0.0', () => {
|
|
79
|
+
console.log(`Web UI: http://localhost:${PORT}`);
|
|
80
|
+
console.log(`Backend: ws://localhost:${WS_PORT}/ws/rpc`);
|
|
81
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Contacts - WhatsApp Controller</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<style>
|
|
9
|
+
.status-indicator { @apply inline-block w-3 h-3 rounded-full mr-2; }
|
|
10
|
+
.status-connected { @apply bg-green-500; }
|
|
11
|
+
.status-disconnected { @apply bg-red-500; }
|
|
12
|
+
.status-connecting { @apply bg-yellow-500 animate-pulse; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body class="bg-gray-100 min-h-screen">
|
|
16
|
+
<nav id="main-nav"></nav>
|
|
17
|
+
<main class="max-w-7xl mx-auto px-4 py-8">
|
|
18
|
+
<div class="space-y-8">
|
|
19
|
+
<!-- Page Header -->
|
|
20
|
+
<div>
|
|
21
|
+
<h1 class="text-3xl font-bold text-gray-900">Contacts</h1>
|
|
22
|
+
<p class="text-gray-600 mt-1">Check if phone numbers are on WhatsApp and view profile pictures</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<!-- Check Numbers Section -->
|
|
26
|
+
<div class="bg-white rounded-lg shadow-md p-6">
|
|
27
|
+
<h2 class="text-xl font-semibold mb-4">Check Phone Numbers</h2>
|
|
28
|
+
<p class="text-gray-600 mb-4">Enter phone numbers (one per line) to check if they are registered on WhatsApp</p>
|
|
29
|
+
|
|
30
|
+
<div class="space-y-4">
|
|
31
|
+
<div>
|
|
32
|
+
<label class="block text-sm font-medium text-gray-700 mb-2">Phone Numbers</label>
|
|
33
|
+
<textarea id="phone-numbers" rows="5"
|
|
34
|
+
placeholder="Enter phone numbers, one per line (without + prefix) e.g. 919876543210 14155551234"
|
|
35
|
+
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"></textarea>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<button onclick="checkContacts()" class="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center">
|
|
39
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
40
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
41
|
+
</svg>
|
|
42
|
+
Check Numbers
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- Results -->
|
|
47
|
+
<div id="check-results" class="mt-6 hidden">
|
|
48
|
+
<h3 class="font-semibold text-gray-800 mb-3">Results</h3>
|
|
49
|
+
<div id="results-container" class="space-y-2"></div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Profile Picture Section -->
|
|
54
|
+
<div class="bg-white rounded-lg shadow-md p-6">
|
|
55
|
+
<h2 class="text-xl font-semibold mb-4">Profile Picture</h2>
|
|
56
|
+
<p class="text-gray-600 mb-4">View profile picture for a user or group</p>
|
|
57
|
+
|
|
58
|
+
<div class="flex flex-wrap gap-4 items-end">
|
|
59
|
+
<div class="flex-1 min-w-[250px]">
|
|
60
|
+
<label class="block text-sm font-medium text-gray-700 mb-2">JID (Phone or Group)</label>
|
|
61
|
+
<input type="text" id="profile-jid"
|
|
62
|
+
placeholder="919876543210@s.whatsapp.net or group@g.us"
|
|
63
|
+
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500">
|
|
64
|
+
</div>
|
|
65
|
+
<button onclick="getProfilePicture()" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center">
|
|
66
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
67
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
68
|
+
</svg>
|
|
69
|
+
Get Picture
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- Profile Picture Result -->
|
|
74
|
+
<div id="profile-result" class="mt-6 hidden">
|
|
75
|
+
<div id="profile-container" class="flex items-center space-x-4">
|
|
76
|
+
<img id="profile-image" src="" alt="Profile Picture" class="w-24 h-24 rounded-full object-cover border-4 border-gray-200">
|
|
77
|
+
<div>
|
|
78
|
+
<p id="profile-jid-display" class="font-semibold text-gray-800"></p>
|
|
79
|
+
<p id="profile-status" class="text-sm text-gray-600"></p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div id="profile-no-picture" class="hidden text-gray-500">
|
|
83
|
+
<svg class="w-24 h-24 text-gray-300 mx-auto" fill="currentColor" viewBox="0 0 24 24">
|
|
84
|
+
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
|
85
|
+
</svg>
|
|
86
|
+
<p class="text-center mt-2">No profile picture available</p>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Presence Section -->
|
|
92
|
+
<div class="bg-white rounded-lg shadow-md p-6">
|
|
93
|
+
<h2 class="text-xl font-semibold mb-4">Presence Status</h2>
|
|
94
|
+
<p class="text-gray-600 mb-4">Set your online/offline presence status</p>
|
|
95
|
+
|
|
96
|
+
<div class="flex flex-wrap gap-4">
|
|
97
|
+
<button onclick="setPresence('available')" class="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center">
|
|
98
|
+
<span class="w-3 h-3 bg-green-300 rounded-full mr-2"></span>
|
|
99
|
+
Set Online
|
|
100
|
+
</button>
|
|
101
|
+
<button onclick="setPresence('unavailable')" class="px-6 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 flex items-center">
|
|
102
|
+
<span class="w-3 h-3 bg-gray-400 rounded-full mr-2"></span>
|
|
103
|
+
Set Offline
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
<p id="presence-status" class="mt-3 text-sm text-gray-600"></p>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</main>
|
|
110
|
+
<footer class="bg-white border-t mt-12">
|
|
111
|
+
<div class="max-w-7xl mx-auto px-4 py-6">
|
|
112
|
+
<p class="text-center text-gray-600">WhatsApp Controller - Static Web Client</p>
|
|
113
|
+
</div>
|
|
114
|
+
</footer>
|
|
115
|
+
<script src="/js/rpc-client.js"></script>
|
|
116
|
+
<script src="/js/app.js"></script>
|
|
117
|
+
<script>
|
|
118
|
+
async function checkContacts() {
|
|
119
|
+
const textarea = document.getElementById('phone-numbers');
|
|
120
|
+
const lines = textarea.value.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
|
121
|
+
|
|
122
|
+
if (lines.length === 0) {
|
|
123
|
+
showNotification('Please enter at least one phone number', 'warning');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const resultsSection = document.getElementById('check-results');
|
|
128
|
+
const container = document.getElementById('results-container');
|
|
129
|
+
resultsSection.classList.remove('hidden');
|
|
130
|
+
container.innerHTML = '<p class="text-gray-500">Checking...</p>';
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const results = await rpc.call('contact_check', { phones: lines });
|
|
134
|
+
|
|
135
|
+
container.innerHTML = results.map(r => {
|
|
136
|
+
const statusClass = r.is_registered ? 'bg-green-100 border-green-300' : 'bg-red-100 border-red-300';
|
|
137
|
+
const statusIcon = r.is_registered
|
|
138
|
+
? '<svg class="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>'
|
|
139
|
+
: '<svg class="w-5 h-5 text-red-600" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>';
|
|
140
|
+
|
|
141
|
+
let businessBadge = '';
|
|
142
|
+
if (r.is_business) {
|
|
143
|
+
businessBadge = `<span class="ml-2 px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded">Business${r.business_name ? ': ' + escapeHtml(r.business_name) : ''}</span>`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `
|
|
147
|
+
<div class="flex items-center justify-between p-3 border rounded-lg ${statusClass}">
|
|
148
|
+
<div class="flex items-center space-x-3">
|
|
149
|
+
${statusIcon}
|
|
150
|
+
<div>
|
|
151
|
+
<p class="font-medium">${escapeHtml(r.query)}</p>
|
|
152
|
+
${r.is_registered ? `<p class="text-xs text-gray-600">${escapeHtml(r.jid || '')}</p>` : ''}
|
|
153
|
+
</div>
|
|
154
|
+
${businessBadge}
|
|
155
|
+
</div>
|
|
156
|
+
<span class="text-sm font-medium ${r.is_registered ? 'text-green-700' : 'text-red-700'}">
|
|
157
|
+
${r.is_registered ? 'On WhatsApp' : 'Not on WhatsApp'}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
`;
|
|
161
|
+
}).join('');
|
|
162
|
+
} catch (error) {
|
|
163
|
+
container.innerHTML = `<div class="text-red-600">Error: ${escapeHtml(error.message)}</div>`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function getProfilePicture() {
|
|
168
|
+
const jidInput = document.getElementById('profile-jid').value.trim();
|
|
169
|
+
|
|
170
|
+
if (!jidInput) {
|
|
171
|
+
showNotification('Please enter a JID', 'warning');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Auto-append @s.whatsapp.net if no @ present
|
|
176
|
+
let fullJid = jidInput;
|
|
177
|
+
if (!jidInput.includes('@')) {
|
|
178
|
+
fullJid = jidInput + '@s.whatsapp.net';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const resultSection = document.getElementById('profile-result');
|
|
182
|
+
const container = document.getElementById('profile-container');
|
|
183
|
+
const noPicture = document.getElementById('profile-no-picture');
|
|
184
|
+
const image = document.getElementById('profile-image');
|
|
185
|
+
const jidDisplay = document.getElementById('profile-jid-display');
|
|
186
|
+
const statusDisplay = document.getElementById('profile-status');
|
|
187
|
+
|
|
188
|
+
resultSection.classList.remove('hidden');
|
|
189
|
+
container.classList.add('hidden');
|
|
190
|
+
noPicture.classList.remove('hidden');
|
|
191
|
+
noPicture.innerHTML = '<p class="text-gray-500">Loading...</p>';
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const data = await rpc.call('contact_profile_pic', { jid: fullJid, preview: false });
|
|
195
|
+
|
|
196
|
+
jidDisplay.textContent = fullJid;
|
|
197
|
+
|
|
198
|
+
if (data.exists) {
|
|
199
|
+
container.classList.remove('hidden');
|
|
200
|
+
noPicture.classList.add('hidden');
|
|
201
|
+
|
|
202
|
+
if (data.data) {
|
|
203
|
+
image.src = 'data:image/jpeg;base64,' + data.data;
|
|
204
|
+
} else if (data.url) {
|
|
205
|
+
image.src = data.url;
|
|
206
|
+
}
|
|
207
|
+
statusDisplay.textContent = 'Profile picture found';
|
|
208
|
+
} else {
|
|
209
|
+
container.classList.add('hidden');
|
|
210
|
+
noPicture.classList.remove('hidden');
|
|
211
|
+
noPicture.innerHTML = `
|
|
212
|
+
<svg class="w-24 h-24 text-gray-300 mx-auto" fill="currentColor" viewBox="0 0 24 24">
|
|
213
|
+
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
|
214
|
+
</svg>
|
|
215
|
+
<p class="text-center mt-2">No profile picture available</p>
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
container.classList.add('hidden');
|
|
220
|
+
noPicture.classList.remove('hidden');
|
|
221
|
+
noPicture.innerHTML = `<p class="text-red-600">Error: ${escapeHtml(error.message)}</p>`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function setPresence(status) {
|
|
226
|
+
const statusEl = document.getElementById('presence-status');
|
|
227
|
+
statusEl.textContent = 'Setting presence...';
|
|
228
|
+
statusEl.className = 'mt-3 text-sm text-blue-600';
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
await rpc.call('presence', { status: status });
|
|
232
|
+
statusEl.textContent = 'Presence updated successfully';
|
|
233
|
+
statusEl.className = 'mt-3 text-sm text-green-600';
|
|
234
|
+
} catch (error) {
|
|
235
|
+
statusEl.textContent = 'Failed: ' + error.message;
|
|
236
|
+
statusEl.className = 'mt-3 text-sm text-red-600';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
</script>
|
|
240
|
+
</body>
|
|
241
|
+
</html>
|