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 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.13",
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)&#10;e.g.&#10;919876543210&#10;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>