This commit is contained in:
Tour
2025-12-06 19:24:10 +01:00
parent b418912a1e
commit 48965d4c50
5 changed files with 518 additions and 24 deletions

View File

@@ -4,23 +4,42 @@ Complete WhatsApp automation server with REST API, WebSocket, and SQLite storage
## Features
**REST API** - Send messages, retrieve contacts/chats/messages
**WebSocket** - Real-time event streaming
**SQLite** - Persistent storage for all data
**Media Storage** - Automatic media file handling
**Authentication** - API key protection
**REST API** - Send messages, retrieve contacts/chats/messages
**WebSocket** - Real-time event streaming
**SQLite** - Persistent storage for all data
**Media Storage** - Automatic media file handling
**Authentication** - API key protection
**QR Code Web UI** - Easy WhatsApp authentication via browser
**All Original Commands** - Full command handler support
## Installation
## Quick Start
### Local Development
```bash
npm install
npm start
```
Visit `http://localhost:3000/qr` to scan the WhatsApp QR code.
### Docker Deployment
```bash
#FIRST WORKING VERSION!! Or manually sync later
curl -X POST http://localhost:3000/api/sync \
-H "X-API-Key: VERY_STRONG_API_KEY_12345" \
-H "Content-Type: application/json" \
-d '{"contacts":true,"chats":true}'
```
docker-compose up -d --build
```
Visit `http://your-server:3000/qr` to authenticate WhatsApp.
## QR Code Authentication
The server provides a web interface for easy WhatsApp authentication:
- **URL**: `http://localhost:3000/qr`
- **Features**:
- Real-time QR code display
- Live connection status updates
- Auto-refresh on new QR codes
- Mobile-friendly design
The QR code is also printed in the console logs for terminal access.

View File

@@ -21,9 +21,14 @@ services:
- MEDIA_PATH=/app/media
# WhatsApp configuration
# Set HEADLESS=false to see the browser window (useful for debugging)
- HEADLESS=true
- REJECT_CALLS=false
# WebSocket URL for QR page (set this to your public URL when using Traefik)
# Example: - WS_URL=https://whatjs.yourdomain.com
- WS_URL=
# CORS configuration
- CORS_ORIGIN=*

230
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"dependencies": {
"dotenv": "^16.3.1",
"express": "^4.18.2",
"qrcode": "^1.5.4",
"qrcode-terminal": "^0.12.0",
"sequelize": "^6.32.1",
"socket.io": "^4.7.2",
@@ -211,7 +212,6 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
}
@@ -220,7 +220,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -685,6 +684,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
@@ -795,11 +803,21 @@
"node": ">=6"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -812,7 +830,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/color-support": {
@@ -998,6 +1015,15 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -1081,6 +1107,12 @@
"integrity": "sha512-D+PTmWulkuQW4D1NTiCRCFxF7pQPn0hgp4YyX4wAQ6xYXKOadSWPR3ENGDQ47MW/Ewc9v2rpC/UEEGahgBYpSQ==",
"license": "BSD-3-Clause"
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -1173,8 +1205,7 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
@@ -1497,6 +1528,19 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/find-yarn-workspace-root": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
@@ -1671,6 +1715,15 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -2062,7 +2115,6 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
}
@@ -2236,6 +2288,18 @@
"license": "ISC",
"optional": true
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -2800,6 +2864,33 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-map": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
@@ -2816,6 +2907,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -2855,6 +2955,15 @@
"npm": ">5"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -2905,6 +3014,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postinstall-postinstall": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz",
@@ -3073,6 +3191,23 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode-terminal": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz",
@@ -3195,6 +3330,21 @@
"node": ">=8.10.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@@ -3516,8 +3666,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC",
"optional": true
"license": "ISC"
},
"node_modules/set-function-length": {
"version": "1.2.2",
@@ -3934,7 +4083,6 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"optional": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -3949,7 +4097,6 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -4365,6 +4512,12 @@
"node": ">= 8"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
@@ -4384,6 +4537,20 @@
"@types/node": "*"
}
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -4411,6 +4578,12 @@
}
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -4433,6 +4606,41 @@
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": {
"dotenv": "^16.3.1",
"express": "^4.18.2",
"qrcode": "^1.5.4",
"qrcode-terminal": "^0.12.0",
"sequelize": "^6.32.1",
"socket.io": "^4.7.2",

263
server.js
View File

@@ -8,6 +8,7 @@ const path = require('path')
const fs = require('fs').promises
const crypto = require('crypto')
const qrcode = require('qrcode-terminal')
const QRCode = require('qrcode')
// --- CONFIGURATION ---
const config = {
@@ -201,6 +202,255 @@ app.get('/api/status', [validateApiKey], async (req, res) => {
app.get('/health', (req, res) => res.json({ status: 'OK', timestamp: Date.now() }))
// QR Code storage
let currentQR = null
let qrTimestamp = null
// QR Code HTML page
app.get('/qr', (req, res) => {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WhatsApp QR Code</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 100%;
text-align: center;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
#qr-container {
background: #f5f5f5;
border-radius: 15px;
padding: 20px;
margin: 20px 0;
min-height: 300px;
display: flex;
justify-content: center;
align-items: center;
}
#qr-code {
max-width: 100%;
height: auto;
border-radius: 10px;
}
.status {
padding: 15px;
border-radius: 10px;
margin: 20px 0;
font-weight: 500;
}
.status.waiting {
background: #fff3cd;
color: #856404;
}
.status.ready {
background: #d4edda;
color: #155724;
}
.status.error {
background: #f8d7da;
color: #721c24;
}
.info {
color: #666;
font-size: 13px;
line-items: 1.6;
}
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.timestamp {
font-size: 11px;
color: #999;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔗 WhatsApp Connection</h1>
<p class="subtitle">Scan the QR code with your WhatsApp mobile app</p>
<div id="status" class="status waiting">
Waiting for QR code...
</div>
<div id="qr-container">
<div class="loader"></div>
</div>
<div class="info">
<p><strong>How to scan:</strong></p>
<p>1. Open WhatsApp on your phone</p>
<p>2. Tap Menu (⋮) > Linked Devices</p>
<p>3. Tap "Link a Device"</p>
<p>4. Point your phone at this screen</p>
</div>
<div class="timestamp" id="timestamp"></div>
</div>
<script>
const statusEl = document.getElementById('status');
const qrContainer = document.getElementById('qr-container');
const timestampEl = document.getElementById('timestamp');
let socket;
function connectWebSocket() {
socket = io('${process.env.WS_URL || ''}');
socket.on('connect', () => {
console.log('WebSocket connected');
});
socket.on('qr', (data) => {
console.log('QR code received');
statusEl.textContent = '📱 Scan this QR code with WhatsApp';
statusEl.className = 'status waiting';
qrContainer.innerHTML = '<img id="qr-code" src="/qr/image" alt="QR Code">';
timestampEl.textContent = 'Generated: ' + new Date(data.timestamp).toLocaleString();
});
socket.on('ready', (data) => {
console.log('WhatsApp ready');
statusEl.textContent = '✅ Connected successfully!';
statusEl.className = 'status ready';
qrContainer.innerHTML = '<div style="font-size: 48px;">✅</div><p style="margin-top: 10px; color: #155724;">WhatsApp is connected and ready</p>';
timestampEl.textContent = '';
});
socket.on('status', (data) => {
if (data.connected) {
statusEl.textContent = '✅ Already connected';
statusEl.className = 'status ready';
qrContainer.innerHTML = '<div style="font-size: 48px;">✅</div><p style="margin-top: 10px; color: #155724;">WhatsApp is already connected</p>';
}
});
socket.on('disconnect', () => {
console.log('WebSocket disconnected');
statusEl.textContent = '⚠️ Connection lost - Reconnecting...';
statusEl.className = 'status error';
});
socket.on('error', (error) => {
console.error('Socket error:', error);
statusEl.textContent = '❌ Error: ' + (error.message || 'Unknown error');
statusEl.className = 'status error';
});
}
// Initial check
fetch('/qr/check')
.then(r => r.json())
.then(data => {
if (data.hasQR) {
statusEl.textContent = '📱 Scan this QR code with WhatsApp';
statusEl.className = 'status waiting';
qrContainer.innerHTML = '<img id="qr-code" src="/qr/image" alt="QR Code">';
timestampEl.textContent = 'Generated: ' + new Date(data.timestamp).toLocaleString();
} else if (data.connected) {
statusEl.textContent = '✅ Already connected';
statusEl.className = 'status ready';
qrContainer.innerHTML = '<div style="font-size: 48px;">✅</div><p style="margin-top: 10px; color: #155724;">WhatsApp is already connected</p>';
timestampEl.textContent = '';
}
})
.catch(err => {
console.error('Check failed:', err);
});
// Connect WebSocket for live updates
connectWebSocket();
// Auto-refresh QR image every 30 seconds if waiting
setInterval(() => {
const img = document.getElementById('qr-code');
if (img && statusEl.classList.contains('waiting')) {
img.src = '/qr/image?t=' + Date.now();
}
}, 30000);
</script>
<script src="/socket.io/socket.io.js"></script>
</body>
</html>
`
res.send(html)
})
// QR Code check endpoint
app.get('/qr/check', (req, res) => {
res.json({
hasQR: !!currentQR,
connected: !!client.info,
timestamp: qrTimestamp
})
})
// QR Code image endpoint
app.get('/qr/image', async (req, res) => {
try {
if (!currentQR) {
// Generate a placeholder
res.setHeader('Content-Type', 'image/png')
const placeholder = await QRCode.toBuffer('Waiting for QR code...', {
width: 300,
color: { dark: '#ccc', light: '#fff' }
})
return res.send(placeholder)
}
res.setHeader('Content-Type', 'image/png')
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
const qrImage = await QRCode.toBuffer(currentQR, {
width: 300,
margin: 2,
color: { dark: '#000', light: '#fff' }
})
res.send(qrImage)
} catch (error) {
logger.error('Error generating QR image', error)
res.status(500).send('Error generating QR code')
}
})
// --- WEBSOCKET ---
io.on('connection', (socket) => {
logger.info(`WebSocket client connected: ${ socket.id }`)
@@ -222,12 +472,23 @@ const client = new Client({
client.on('qr', (qr) => {
logger.info('QR received - scan to authenticate')
logger.info('QR Code available at: http://localhost:' + config.port + '/qr')
qrcode.generate(qr, { small: true })
io.emit('qr', { qr, timestamp: Date.now() })
// Store QR for web display
currentQR = qr
qrTimestamp = Date.now()
io.emit('qr', { qr, timestamp: qrTimestamp })
})
client.on('ready', async () => {
logger.info('WhatsApp client ready');
// Clear QR code once connected
currentQR = null
qrTimestamp = null
const wwebVersion = await client.getWWebVersion();
// Sync contacts with proper type detection