WhatsApp-Logger-Self-Hosted-

A privacy-focused, self-hosted...
Log | Files | Refs | README | LICENSE

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 });
© notamitgamer • Site Built: 2026-07-21 13:58:23 UTC • git-mirror commit: 1037f62 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror