Initial commit with Flutter and Node.js code
This commit is contained in:
23
whatsapp_bridge/package.json
Normal file
23
whatsapp_bridge/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "whatsapp_bridge",
|
||||
"version": "1.0.0",
|
||||
"description": "WhatsApp Standalone WebSockets Bridge Server",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [
|
||||
"whatsapp",
|
||||
"bridge",
|
||||
"websockets"
|
||||
],
|
||||
"author": "Antigravity Dev Team",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"puppeteer": "^21.0.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"whatsapp-web.js": "^1.26.0",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
}
|
||||
394
whatsapp_bridge/server.js
Normal file
394
whatsapp_bridge/server.js
Normal file
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* WhatsApp WebSocket Bridge Server (Node.js)
|
||||
* Uses whatsapp-web.js to connect to WhatsApp via Puppeteer
|
||||
* Exposes a WebSocket API and HTTP health endpoint on Port 3025
|
||||
*/
|
||||
|
||||
const { Client, LocalAuth } = require('whatsapp-web.js');
|
||||
const qrcode = require('qrcode');
|
||||
const { WebSocketServer, WebSocket } = require('ws');
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
|
||||
// ─── Config ────────────────────────────────────────────────────────────────
|
||||
const PORT = 3025;
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
// ─── State ─────────────────────────────────────────────────────────────────
|
||||
let waClient = null;
|
||||
let clientReady = false;
|
||||
let qrCodeCache = null;
|
||||
const connectedClients = new Set();
|
||||
|
||||
// ─── Error Handling (Never Crash) ──────────────────────────────────────────
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[CRITICAL] Uncaught Exception:', err);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[CRITICAL] Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
// ─── WebSocket: Broadcast to all Flutter clients ───────────────────────────
|
||||
function broadcast(payload) {
|
||||
const data = JSON.stringify(payload);
|
||||
connectedClients.forEach((ws) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
ws.send(data);
|
||||
} catch (err) {
|
||||
console.error('[WS] Broadcast send error:', err.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendTo(ws, payload) {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
ws.send(JSON.stringify(payload));
|
||||
} catch (err) {
|
||||
console.error('[WS] Send error:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Format conversation object for Flutter ────────────────────────────────
|
||||
async function formatChat(chat) {
|
||||
let avatar = null;
|
||||
try {
|
||||
const contact = await chat.getContact();
|
||||
const pic = await contact.getProfilePicUrl();
|
||||
if (pic) avatar = pic;
|
||||
} catch (_) {}
|
||||
|
||||
// Last Message formatting
|
||||
let lastMessageFormatted = null;
|
||||
if (chat.lastMessage) {
|
||||
lastMessageFormatted = {
|
||||
body: chat.lastMessage.body || '',
|
||||
timestamp: chat.lastMessage.timestamp || Math.floor(Date.now() / 1000),
|
||||
fromMe: chat.lastMessage.fromMe || false,
|
||||
hasMedia: chat.lastMessage.hasMedia || false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: chat.id._serialized,
|
||||
name: chat.name || 'Unknown',
|
||||
isGroup: chat.isGroup || false,
|
||||
unreadCount: chat.unreadCount || 0,
|
||||
avatar: avatar,
|
||||
lastMessage: lastMessageFormatted,
|
||||
timestamp: chat.timestamp || Math.floor(Date.now() / 1000),
|
||||
pinned: chat.pinned || false,
|
||||
isMuted: chat.isMuted || false
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Format message object for Flutter ─────────────────────────────────────
|
||||
function formatMessage(msg) {
|
||||
// Map internal ack values if needed, otherwise fallback
|
||||
// ack: 0=error/none 1=pending 2=sent 3=delivered 4=read
|
||||
let ack = msg.ack || 0;
|
||||
if (ack < 0) ack = 0;
|
||||
|
||||
// Restrict types to allowed values: "chat"|"image"|"video"|"audio"|"document"|"sticker"
|
||||
let type = "chat";
|
||||
if (["chat", "image", "video", "audio", "document", "sticker"].includes(msg.type)) {
|
||||
type = msg.type;
|
||||
} else if (msg.type === "ptt") {
|
||||
type = "audio";
|
||||
}
|
||||
|
||||
return {
|
||||
id: msg.id._serialized,
|
||||
body: msg.body || '',
|
||||
fromMe: msg.fromMe || false,
|
||||
timestamp: msg.timestamp || Math.floor(Date.now() / 1000),
|
||||
type: type,
|
||||
hasMedia: msg.hasMedia || false,
|
||||
isForwarded: msg.isForwarded || false,
|
||||
author: msg.author || null,
|
||||
ack: ack
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Initialize WhatsApp Client ─────────────────────────────────────────────
|
||||
function initWhatsApp() {
|
||||
console.log('[WA] Initializing WhatsApp client using LocalAuth...');
|
||||
clientReady = false;
|
||||
|
||||
waClient = new Client({
|
||||
authStrategy: new LocalAuth({ clientId: 'whatsapp-bridge' }),
|
||||
puppeteer: {
|
||||
headless: 'new',
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--no-first-run',
|
||||
'--no-zygote',
|
||||
'--single-process',
|
||||
'--disable-gpu',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// QR Code received -> send to all WS clients
|
||||
waClient.on('qr', async (qr) => {
|
||||
console.log('[WA] QR Code received');
|
||||
try {
|
||||
const qrDataUrl = await qrcode.toDataURL(qr);
|
||||
qrCodeCache = qrDataUrl;
|
||||
broadcast({ type: 'qr', qr: qrDataUrl });
|
||||
} catch (err) {
|
||||
console.error('[WA] QR generation error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Authenticated
|
||||
waClient.on('authenticated', () => {
|
||||
console.log('[WA] Authenticated successfully');
|
||||
qrCodeCache = null;
|
||||
broadcast({ type: 'authenticated' });
|
||||
});
|
||||
|
||||
// Ready
|
||||
waClient.on('ready', () => {
|
||||
console.log('[WA] WhatsApp Client Ready');
|
||||
clientReady = true;
|
||||
qrCodeCache = null;
|
||||
broadcast({ type: 'ready' });
|
||||
broadcast({ type: 'status', ready: true });
|
||||
});
|
||||
|
||||
// New message received
|
||||
waClient.on('message', async (msg) => {
|
||||
console.log(`[WA] New message received from: ${msg.from}`);
|
||||
try {
|
||||
const formatted = formatMessage(msg);
|
||||
broadcast({ type: 'new_message', chatId: msg.from, data: formatted });
|
||||
} catch (err) {
|
||||
console.error('[WA] Error formatting new message event:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Message ACK update
|
||||
waClient.on('message_ack', (msg, ack) => {
|
||||
try {
|
||||
broadcast({
|
||||
type: 'message_ack',
|
||||
messageId: msg.id._serialized,
|
||||
chatId: msg.to || msg.from,
|
||||
ack: ack
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[WA] Error sending message_ack event:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Disconnected
|
||||
waClient.on('disconnected', (reason) => {
|
||||
console.warn('[WA] Disconnected! Reason:', reason);
|
||||
clientReady = false;
|
||||
broadcast({ type: 'disconnected', reason });
|
||||
broadcast({ type: 'status', ready: false });
|
||||
|
||||
// Clean up client resources
|
||||
try {
|
||||
waClient.destroy();
|
||||
} catch (_) {}
|
||||
|
||||
// Auto-reconnect after 5 seconds
|
||||
console.log('[WA] Reconnecting in 5 seconds...');
|
||||
setTimeout(initWhatsApp, 5000);
|
||||
});
|
||||
|
||||
waClient.initialize().catch((err) => {
|
||||
console.error('[WA] Client initialization failed:', err);
|
||||
console.log('[WA] Retrying initialization in 5 seconds...');
|
||||
setTimeout(initWhatsApp, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Handle WebSocket messages from Flutter ────────────────────────────────
|
||||
async function handleMessage(ws, raw) {
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
return sendTo(ws, { type: 'error', message: 'Invalid JSON payload' });
|
||||
}
|
||||
|
||||
const { type, requestId } = payload;
|
||||
if (!requestId) {
|
||||
return sendTo(ws, { type: 'error', message: 'Missing requestId' });
|
||||
}
|
||||
|
||||
const respond = (data) => sendTo(ws, { ...data, requestId });
|
||||
|
||||
// Handle type specific requests
|
||||
try {
|
||||
switch (type) {
|
||||
// ── Ping ───────────────────────────────────────────────────────────
|
||||
case 'ping':
|
||||
return respond({ type: 'pong', ready: clientReady });
|
||||
|
||||
// ── Conversations ──────────────────────────────────────────────────
|
||||
case 'get_conversations': {
|
||||
if (!clientReady) {
|
||||
return respond({ type: 'error', message: 'WhatsApp is not ready' });
|
||||
}
|
||||
const chats = await waClient.getChats();
|
||||
const limit = parseInt(payload.limit) || 50;
|
||||
const offset = parseInt(payload.offset) || 0;
|
||||
const slice = chats.slice(offset, offset + limit);
|
||||
const formatted = await Promise.all(slice.map(formatChat));
|
||||
|
||||
return respond({
|
||||
type: 'conversations',
|
||||
data: formatted,
|
||||
total: chats.length,
|
||||
requestId
|
||||
});
|
||||
}
|
||||
|
||||
// ── Messages ───────────────────────────────────────────────────────
|
||||
case 'get_messages': {
|
||||
if (!clientReady) {
|
||||
return respond({ type: 'error', message: 'WhatsApp is not ready' });
|
||||
}
|
||||
const { chatId, limit } = payload;
|
||||
if (!chatId) {
|
||||
return respond({ type: 'error', message: 'chatId is required' });
|
||||
}
|
||||
const chat = await waClient.getChatById(chatId);
|
||||
const fetchLimit = parseInt(limit) || 50;
|
||||
const messages = await chat.fetchMessages({ limit: fetchLimit });
|
||||
const formatted = messages.map(formatMessage);
|
||||
|
||||
return respond({
|
||||
type: 'messages',
|
||||
chatId: chatId,
|
||||
data: formatted,
|
||||
requestId
|
||||
});
|
||||
}
|
||||
|
||||
// ── Send Message ───────────────────────────────────────────────────
|
||||
case 'send_message': {
|
||||
if (!clientReady) {
|
||||
return respond({ type: 'error', message: 'WhatsApp is not ready' });
|
||||
}
|
||||
const { chatId, text } = payload;
|
||||
if (!chatId || !text) {
|
||||
return respond({ type: 'error', message: 'chatId and text are required' });
|
||||
}
|
||||
const sentMsg = await waClient.sendMessage(chatId, text);
|
||||
return respond({
|
||||
type: 'message_sent',
|
||||
chatId: chatId,
|
||||
data: formatMessage(sentMsg),
|
||||
requestId
|
||||
});
|
||||
}
|
||||
|
||||
// ── Mark as Read ───────────────────────────────────────────────────
|
||||
case 'mark_read': {
|
||||
if (!clientReady) {
|
||||
return respond({ type: 'error', message: 'WhatsApp is not ready' });
|
||||
}
|
||||
const { chatId } = payload;
|
||||
if (!chatId) {
|
||||
return respond({ type: 'error', message: 'chatId is required' });
|
||||
}
|
||||
const chat = await waClient.getChatById(chatId);
|
||||
await chat.sendSeen();
|
||||
return respond({
|
||||
type: 'marked_read',
|
||||
chatId: chatId,
|
||||
requestId
|
||||
});
|
||||
}
|
||||
|
||||
// ── Search Conversations ───────────────────────────────────────────
|
||||
case 'search_conversations': {
|
||||
if (!clientReady) {
|
||||
return respond({ type: 'error', message: 'WhatsApp is not ready' });
|
||||
}
|
||||
const query = (payload.query || '').toLowerCase();
|
||||
const chats = await waClient.getChats();
|
||||
const filtered = chats.filter((c) =>
|
||||
(c.name || '').toLowerCase().includes(query)
|
||||
);
|
||||
const formatted = await Promise.all(filtered.slice(0, 50).map(formatChat));
|
||||
|
||||
return respond({
|
||||
type: 'conversations',
|
||||
data: formatted,
|
||||
search: true,
|
||||
requestId
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return respond({ type: 'error', message: `Unknown request type: ${type}` });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[WS] Error processing request ${type}:`, err);
|
||||
return respond({ type: 'error', message: err.message || 'Server error' });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── WebSocket Connection Handler ──────────────────────────────────────────
|
||||
wss.on('connection', (ws, req) => {
|
||||
const ip = req.socket.remoteAddress;
|
||||
console.log(`[WS] New client connected from IP: ${ip}`);
|
||||
connectedClients.add(ws);
|
||||
|
||||
// Send status immediately on connection
|
||||
sendTo(ws, { type: 'status', ready: clientReady });
|
||||
|
||||
// If a QR code is active and client is not ready, push it immediately
|
||||
if (!clientReady && qrCodeCache) {
|
||||
sendTo(ws, { type: 'qr', qr: qrCodeCache });
|
||||
}
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
handleMessage(ws, data.toString());
|
||||
} catch (err) {
|
||||
console.error('[WS] Error handling incoming data:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log(`[WS] Client disconnected: ${ip}`);
|
||||
connectedClients.delete(ws);
|
||||
});
|
||||
|
||||
ws.on('error', (err) => {
|
||||
console.error(`[WS] Error on client connection ${ip}:`, err.message);
|
||||
connectedClients.delete(ws);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── HTTP Health Endpoint ──────────────────────────────────────────────────
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
status: 'ok',
|
||||
waReady: clientReady,
|
||||
clients: connectedClients.size,
|
||||
port: PORT
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Start HTTP + WebSocket Server ─────────────────────────────────────────
|
||||
server.listen(PORT, () => {
|
||||
console.log(`[SERVER] Standalone WhatsApp Bridge running on port ${PORT}`);
|
||||
initWhatsApp();
|
||||
});
|
||||
Reference in New Issue
Block a user