index.js (16205B)
1 const { 2 default: makeWASocket, 3 DisconnectReason, 4 fetchLatestBaileysVersion, 5 BufferJSON, 6 initAuthCreds, 7 proto 8 } = require('@whiskeysockets/baileys'); 9 const express = require('express'); 10 const QRCode = require('qrcode'); 11 const pino = require('pino'); 12 const admin = require('firebase-admin'); 13 const crypto = require('crypto'); 14 15 // --- CONFIGURATION --- 16 const PORT = process.env.PORT || 3000; 17 const AUTH_USER = process.env.AUTH_USER; 18 const AUTH_PASS = process.env.AUTH_PASS; 19 20 // Initialize Express 21 const app = express(); 22 app.use(express.urlencoded({ extended: true })); 23 app.use(express.json()); 24 25 // --- FIREBASE SETUP --- 26 let serviceAccount; 27 try { 28 if (process.env.FIREBASE_SERVICE_ACCOUNT) { 29 serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT); 30 } else { 31 serviceAccount = require('./serviceAccountKey.json'); 32 } 33 34 admin.initializeApp({ 35 credential: admin.credential.cert(serviceAccount) 36 }); 37 console.log("System: Firebase Admin initialized successfully."); 38 } catch (error) { 39 console.error("System Error: Failed to initialize Firebase. Make sure FIREBASE_SERVICE_ACCOUNT env var is set."); 40 process.exit(1); 41 } 42 43 const db = admin.firestore(); 44 45 // --- FIRESTORE AUTH ADAPTER FOR BAILEYS --- 46 async function useFirestoreAuthState(db, collectionName = 'whatsapp_auth') { 47 const collection = db.collection(collectionName); 48 49 const writeData = async (data, id) => { 50 try { 51 // BufferJSON converts buffers & uint8 arrays into storable base64 strings 52 const str = JSON.stringify(data, BufferJSON.replacer); 53 await collection.doc(id).set({ data: str }); 54 } catch (err) { 55 console.error("System: Error writing auth state:", err.message); 56 } 57 }; 58 59 const readData = async (id) => { 60 try { 61 const doc = await collection.doc(id).get(); 62 if (doc.exists) { 63 return JSON.parse(doc.data().data, BufferJSON.reviver); 64 } 65 } catch (err) { 66 console.error("System: Error reading auth state:", err.message); 67 } 68 return null; 69 }; 70 71 const removeData = async (id) => { 72 try { 73 await collection.doc(id).delete(); 74 } catch (err) { 75 console.error("System: Error removing auth state:", err.message); 76 } 77 }; 78 79 // Load credentials from Firestore or generate new ones (for initial QR scan) 80 const creds = (await readData('creds')) || initAuthCreds(); 81 82 return { 83 state: { 84 creds, 85 keys: { 86 get: async (type, ids) => { 87 const data = {}; 88 await Promise.all(ids.map(async id => { 89 let value = await readData(`${type}-${id}`); 90 if (type === 'app-state-sync-key' && value) { 91 value = proto.Message.AppStateSyncKeyData.fromObject(value); 92 } 93 data[id] = value; 94 })); 95 return data; 96 }, 97 set: async (data) => { 98 const tasks = []; 99 for (const category in data) { 100 for (const id in data[category]) { 101 const value = data[category][id]; 102 const docId = `${category}-${id}`; 103 if (value) { 104 tasks.push(writeData(value, docId)); 105 } else { 106 tasks.push(removeData(docId)); 107 } 108 } 109 } 110 await Promise.all(tasks); 111 } 112 } 113 }, 114 saveCreds: () => { 115 return writeData(creds, 'creds'); 116 }, 117 clearState: async () => { 118 // We only need to delete the primary creds to force a new QR scan 119 await removeData('creds'); 120 } 121 }; 122 } 123 124 // --- BAILEYS SETUP --- 125 let qrCodeData = null; 126 let sock = null; 127 let isConnected = false; 128 129 async function startWhatsApp() { 130 const logger = pino({ level: 'silent' }); 131 132 // Use our custom Firestore Auth adapter instead of useMultiFileAuthState 133 const { state, saveCreds, clearState } = await useFirestoreAuthState(db, 'whatsapp_auth'); 134 const { version } = await fetchLatestBaileysVersion(); 135 136 console.log("System: Connecting to WhatsApp servers..."); 137 138 sock = makeWASocket({ 139 version, 140 logger, 141 printQRInTerminal: true, 142 auth: state, 143 browser: ["WhatsApp Logger Backend", "Chrome", "1.0.0"], 144 syncFullHistory: true 145 }); 146 147 sock.ev.on('connection.update', async (update) => { 148 const { connection, lastDisconnect, qr } = update; 149 150 if (qr) { 151 console.log("System: No valid credentials. New QR Code generated."); 152 qrCodeData = qr; 153 isConnected = false; 154 } 155 156 if (connection === 'close') { 157 isConnected = false; 158 const statusCode = lastDisconnect?.error?.output?.statusCode; 159 const shouldReconnect = statusCode !== DisconnectReason.loggedOut; 160 161 console.log(`System: Connection closed (Status: ${statusCode})`); 162 163 if (shouldReconnect) { 164 console.log("System: Reconnecting in 5 seconds..."); 165 setTimeout(startWhatsApp, 5000); 166 } else { 167 console.log("System: Device Logged Out. Wiping session from Firestore."); 168 await clearState(); 169 qrCodeData = null; 170 startWhatsApp(); // Restart to grab a fresh QR code 171 } 172 } else if (connection === 'open') { 173 console.log("System: Connection Open and Authenticated. Firebase Auth Sync Active."); 174 qrCodeData = null; 175 isConnected = true; 176 } 177 }); 178 179 // Write updated credentials back to Firestore whenever keys change 180 sock.ev.on('creds.update', saveCreds); 181 182 // --- FEATURE: REAL NUMBER SYNC --- 183 sock.ev.on('contacts.upsert', async (contacts) => { 184 for (const contact of contacts) { 185 let updateData = {}; 186 const displayName = contact.name || contact.notify; 187 188 if (displayName) updateData.displayName = displayName; 189 190 // Extract standard phone number from standard JID 191 if (contact.id && contact.id.endsWith('@s.whatsapp.net')) { 192 updateData.phoneNumber = contact.id.split('@')[0]; 193 } 194 195 // Sync using LID or ID 196 const primaryId = contact.lid || contact.id; 197 198 if (primaryId && Object.keys(updateData).length > 0) { 199 try { 200 await db.collection('Chats').doc(primaryId).set(updateData, { merge: true }); 201 202 // Keep the fallback JID document synced as well if we routed via LID 203 if (contact.lid && contact.id !== contact.lid) { 204 await db.collection('Chats').doc(contact.id).set(updateData, { merge: true }); 205 } 206 } catch (err) { 207 // Silent fail to keep logs clean 208 } 209 } 210 } 211 }); 212 213 sock.ev.on('messages.upsert', async ({ messages, type }) => { 214 if (type !== 'notify' && type !== 'append') return; 215 216 for (const msg of messages) { 217 try { 218 if (!msg.message) continue; 219 220 const remoteJid = msg.key.remoteJid; 221 if (remoteJid === 'status@broadcast') continue; 222 223 const textContent = 224 msg.message.conversation || 225 msg.message.extendedTextMessage?.text || 226 msg.message.imageMessage?.caption || 227 msg.message.videoMessage?.caption || 228 ""; 229 230 if (!textContent) continue; 231 232 const timestamp = msg.messageTimestamp 233 ? (typeof msg.messageTimestamp === 'number' ? msg.messageTimestamp : msg.messageTimestamp.low) 234 : Math.floor(Date.now() / 1000); 235 236 const isFromMe = msg.key.fromMe || false; 237 const senderName = isFromMe ? "Me" : (msg.pushName || "Unknown"); 238 239 // 1. Ensure Chat Document Exists 240 await db.collection('Chats').doc(remoteJid).set({ 241 lastActive: timestamp, 242 id: remoteJid 243 }, { merge: true }); 244 245 // 2. Save Message 246 await db.collection('Chats') 247 .doc(remoteJid) 248 .collection('Messages') 249 .doc(msg.key.id) 250 .set({ 251 text: textContent, 252 senderId: remoteJid, 253 senderName: senderName, 254 timestamp: timestamp, 255 fromMe: isFromMe, 256 id: msg.key.id 257 }, { merge: true }); 258 259 } catch (err) { 260 // Silent error handling 261 } 262 } 263 }); 264 } 265 266 // --- AUTH UTILS --- 267 const SESSION_SECRET = crypto.createHash('sha256').update(AUTH_PASS || 'default').digest('hex'); 268 269 function parseCookies(request) { 270 const list = {}; 271 const rc = request.headers.cookie; 272 if (rc) { 273 rc.split(';').forEach((cookie) => { 274 const parts = cookie.split('='); 275 list[parts.shift().trim()] = decodeURI(parts.join('=')); 276 }); 277 } 278 return list; 279 } 280 281 // --- EXPRESS ROUTES --- 282 283 // 1. Ping (UptimeRobot) 284 app.get('/ping', (req, res) => { 285 res.status(200).send('Pong'); 286 }); 287 288 // 2. API: Verify Credentials 289 app.post('/api/verify', (req, res) => { 290 res.header("Access-Control-Allow-Origin", "*"); 291 res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 292 293 const { username, password } = req.body; 294 295 if (username === AUTH_USER && password === AUTH_PASS) { 296 return res.json({ success: true }); 297 } else { 298 return res.status(401).json({ success: false }); 299 } 300 }); 301 302 // CORS Pre-flight 303 app.options('/api/verify', (req, res) => { 304 res.header("Access-Control-Allow-Origin", "*"); 305 res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 306 res.sendStatus(200); 307 }); 308 309 // 3. Login Page 310 app.get('/login', (req, res) => { 311 res.send(` 312 <html> 313 <body style="font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5;"> 314 <form action="/login" method="POST" style="background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 300px;"> 315 <h2 style="margin-top: 0; text-align: center;">WhatsApp Logger</h2> 316 <div style="margin-bottom: 1rem;"> 317 <label style="display: block; margin-bottom: 0.5rem;">Username</label> 318 <input type="text" name="username" required style="width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"> 319 </div> 320 <div style="margin-bottom: 1rem;"> 321 <label style="display: block; margin-bottom: 0.5rem;">Password</label> 322 <input type="password" name="password" required style="width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"> 323 </div> 324 <div style="margin-bottom: 1rem;"> 325 <label style="display: flex; align-items: center; font-size: 0.9rem;"> 326 <input type="checkbox" name="remember" value="yes" style="margin-right: 0.5rem;"> 327 Keep me logged in for 5 mins 328 </label> 329 </div> 330 <button type="submit" style="width: 100%; padding: 0.75rem; background: #25D366; color: white; border: none; border-radius: 4px; font-weight: bold; cursor: pointer;">Login</button> 331 </form> 332 </body> 333 </html> 334 `); 335 }); 336 337 // 4. Login Action 338 app.post('/login', (req, res) => { 339 const { username, password, remember } = req.body; 340 341 if (username === AUTH_USER && password === AUTH_PASS) { 342 let cookieSettings = 'HttpOnly; Path=/;'; 343 if (remember === 'yes') cookieSettings += ' Max-Age=300;'; 344 345 res.setHeader('Set-Cookie', `auth_session=${SESSION_SECRET}; ${cookieSettings}`); 346 return res.redirect('/'); 347 } 348 res.status(401).send('Invalid credentials. <a href="/login">Try again</a>'); 349 }); 350 351 // 5. Logout 352 app.get('/logout', (req, res) => { 353 res.setHeader('Set-Cookie', 'auth_session=; Max-Age=0; Path=/;'); 354 res.redirect('/login'); 355 }); 356 357 // --- MIDDLEWARE --- 358 const checkAuth = (req, res, next) => { 359 if (!AUTH_USER || !AUTH_PASS) return next(); 360 const cookies = parseCookies(req); 361 if (cookies.auth_session === SESSION_SECRET) return next(); 362 363 if (req.path.startsWith('/api')) res.status(401).send('Unauthorized'); 364 else res.redirect('/login'); 365 }; 366 367 app.use(checkAuth); 368 369 // 6. Main Route 370 app.get('/', async (req, res) => { 371 const logoutBtn = `<a href="/logout" style="position: absolute; top: 10px; right: 10px; padding: 8px 16px; background: #ff4444; color: white; text-decoration: none; border-radius: 4px; font-size: 14px;">Logout</a>`; 372 373 if (isConnected) { 374 return res.send(` 375 <html> 376 <body style="font-family: sans-serif; text-align: center; padding-top: 50px; background-color: #f0f2f5;"> 377 ${logoutBtn} 378 <div style="background: white; padding: 40px; border-radius: 10px; display: inline-block; box-shadow: 0 4px 12px rgba(0,0,0,0.1);"> 379 <h2 style="color: green;">System Operational</h2> 380 <p style="color: #555;">Connected to WhatsApp. State synced to Firestore.</p> 381 <p style="color: #999; font-size: 12px;">Back-end Service</p> 382 </div> 383 </body> 384 </html> 385 `); 386 } 387 388 if (qrCodeData) { 389 try { 390 const qrImage = await QRCode.toDataURL(qrCodeData); 391 return res.send(` 392 <html> 393 <head><meta http-equiv="refresh" content="5"></head> 394 <body style="font-family: sans-serif; text-align: center; padding-top: 50px; background-color: #f0f2f5;"> 395 ${logoutBtn} 396 <div style="background: white; padding: 40px; border-radius: 10px; display: inline-block; box-shadow: 0 4px 12px rgba(0,0,0,0.1);"> 397 <h2>Scan to Link</h2> 398 <img src="${qrImage}" alt="QR Code" /> 399 <p style="color: #666;">Refreshes every 5 seconds...</p> 400 </div> 401 </body> 402 </html> 403 `); 404 } catch (e) { 405 return res.send("Error generating QR."); 406 } 407 } 408 409 return res.send(` 410 <html> 411 <head><meta http-equiv="refresh" content="2"></head> 412 <body style="font-family: sans-serif; text-align: center; padding-top: 50px;"> 413 <p>Initializing connection or restoring auth state... please wait.</p> 414 ${logoutBtn} 415 </body> 416 </html> 417 `); 418 }); 419 420 // --- START SERVER --- 421 app.listen(PORT, () => { 422 startWhatsApp(); 423 console.log(`Server running on port ${PORT}`); 424 });