WhatsApp-Logger-Self-Hosted-

A privacy-focused, self-hosted WhatsApp archiving tool. It captures messages (including deleted ones) via a linked device connection and sto...

git clone https://github.com/notamitgamer/WhatsApp-Logger-Self-Hosted-.git
· rebuilt hourlyOpen on GitHub
commit a468b13f9a5208f847f6f56266325e90f8faeff2
Author: AMIT DUTTA <amitdutta4255@gmail.com>
Date:   Mon, 16 Feb 2026 21:47:26 +0530

Add files via upload
Diffstat:
ACODE_OF_CONDUCT.md | 19+++++++++++++++++++
ACONTRIBUTING.md | 19+++++++++++++++++++
ADockerfile | 30++++++++++++++++++++++++++++++
ALICENSE | 21+++++++++++++++++++++
AREADME.md | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ASECURITY.md | 25+++++++++++++++++++++++++
Aindex.html | 812+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aindex.js | 320+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackage.json | 22++++++++++++++++++++++
9 files changed, 1424 insertions(+), 0 deletions(-)

diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md @@ -0,0 +1,19 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainer at mail@amit.is-a.dev. All complaints will be reviewed and investigated promptly and fairly. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing to WhatsApp Logger + +Thank you for your interest in contributing. + +## How to Contribute + +1. Fork the repository. +2. Create a new branch for your feature or fix. +3. Commit your changes. +4. Push to your fork and submit a Pull Request. + +## Reporting Bugs + +If you encounter an issue that is not a security vulnerability, please open an Issue on GitHub or email mail@amit.is-a.dev. + +## Guidelines + +- **No Logging of Data:** Do not submit code that adds `console.log` or print statements revealing message content, phone numbers, or user data. Pull Requests containing such logs will be rejected to protect user privacy. +- **Clean Code:** Ensure your code is readable and commented where necessary. diff --git a/Dockerfile b/Dockerfile @@ -0,0 +1,30 @@ +FROM node:20-slim + +# Install system dependencies +# We add git because some npm packages require it to fetch dependencies. +# We add python3, make, and g++ because some crypto libraries used by Baileys +# might need to compile native code. +RUN apt-get update && apt-get install -y \ + git \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Create app directory +WORKDIR /usr/src/app + +# Install dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +COPY package*.json ./ + +RUN npm install + +# Bundle app source +COPY . . + +# Expose the port the app runs on +EXPOSE 3000 + +# Start command +CMD [ "node", "index.js" ] diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 AMIT DUTTA + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md @@ -0,0 +1,156 @@ +# WhatsApp Logger (Self-Hosted) + +A privacy-focused, self-hosted WhatsApp archiving tool. It captures messages (including deleted ones) via a linked device connection and stores them in your own Firebase Firestore database. + +## 🚀 Features + +* **Anti-Delete**: Logs messages instantly, preserving them even if the sender deletes them. +* **Privacy First**: You host the backend and database. No third-party servers access your data. +* **Secure Access**: Frontend is protected by a password validated against your backend. +* **Media Support**: Captures text messages (Images/Media support depends on Baileys implementation, primarily text-focused). +* **Search & Filter**: Search by content or filter by date. +* **Export**: Export chat logs to `.txt` files. + +--- + +## 🛠️ Prerequisities + +1. A **GitHub** Account. +2. A **Render** Account (Free tier works). +3. A **Firebase** Account (Free Spark plan works). +4. A **WhatsApp** account on your phone. +5. An **UptimeRobot** Account (Free). + +--- + +## step 1: Firebase Setup (The Database) + +1. Go to the [Firebase Console](https://console.firebase.google.com/) and create a new project. +2. **Create Database**: + * Navigate to **Firestore Database** in the sidebar. + * Click **Create Database**. + * Select a location (e.g., `nam5` or `eur3`). + * Start in **Production Mode**. +3. **Set Security Rules**: + * Go to the **Rules** tab in Firestore. + * Replace the rules with the following (allows anyone to read, but only backend with Admin SDK can write): + ```javascript + rules_version = '2'; + service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read: if true; // Frontend needs read access + allow write: if false; // Only Backend (Admin SDK) can write + } + } + } + ``` +4. **Get Backend Credentials (Service Account)**: + * Go to **Project Settings** (Gear icon) -> **Service accounts**. + * Click **Generate new private key**. + * This will download a `.json` file. **Keep this safe.** You will need its content for Render. +5. **Get Frontend Configuration**: + * Go to **Project Settings** -> **General**. + * Scroll down to "Your apps" and click the **Web (</>)** icon. + * Register app (nickname: "Logger Frontend"). + * Copy the `firebaseConfig` object (API Key, Project ID, etc.). You will need this for `index.html`. + +--- + +## Step 2: Deploy Backend (The Listener) + +1. **Fork this Repository** to your own GitHub account. +2. Log in to [Render](https://render.com/). +3. Click **New +** -> **Web Service**. +4. Connect your forked repository. +5. **Runtime**: Select **Docker**. +6. **Environment Variables** (Critical Step): + Add the following variables under "Advanced": + * `FIREBASE_SERVICE_ACCOUNT`: Paste the **entire content** of the JSON file you downloaded in Step 1. + * `AUTH_USER`: Set a username (e.g., `admin`). + * `AUTH_PASS`: Set a strong password. This creates the lock for your logger. +7. Click **Create Web Service**. +8. Wait for the deployment to finish. Render will give you a URL like `https://your-app.onrender.com`. + +--- + +## Step 3: Connect WhatsApp + +1. Open your Render URL (`https://your-app.onrender.com`) in a browser. +2. You will be prompted for a login. Use the `AUTH_USER` and `AUTH_PASS` you set in Render. +3. You will see a **QR Code**. +4. Open **WhatsApp** on your phone: + * iOS: Settings -> Linked Devices + * Android: Three dots -> Linked Devices +5. Tap **Link a Device** and scan the QR code. +6. The page should refresh and say **"System Operational"**. Your backend is now listening! + +--- + +## Step 4: Setup Frontend (The Viewer) + +1. Download the `chat.html` file from this repository and rename it to `index.html`. +2. Open `index.html` in a text editor (Notepad, VS Code, etc.). +3. Locate the Configuration section (around line 370). +4. **Fill in the details**: + * `RENDER_BACKEND_URL`: Your Render URL (e.g., `https://your-app.onrender.com` - **No trailing slash**). + * `firebaseConfig`: The keys you copied in Step 1.5. + + **It should look like this before you edit it:** + ```javascript + const RENDER_BACKEND_URL = ""; + + // Firebase Config + const firebaseConfig = { + apiKey: "", + authDomain: "", + projectId: "", + storageBucket: "", + messagingSenderId: "", + appId: "" + }; + ``` + +5. **Deploy the Frontend**: + * You can host this single file anywhere: + * **Firebase Hosting** (Recommended): `firebase init` -> Hosting -> Select `public` directory -> Put `index.html` there -> `firebase deploy`. + * **GitHub Pages**: Enable Pages in your repo settings. + * **Netlify/Vercel**: Drag and drop the folder containing `index.html`. + +--- + +## Step 5: Usage + +1. Navigate to your hosted frontend URL (e.g., `https://amit.is-a.dev/wp-chat`). +2. You will see a Login Screen. +3. Enter the same `AUTH_USER` and `AUTH_PASS` you configured on Render. +4. Once unlocked, your chats will load from Firebase. + * **Sidebar**: Shows chat list sorted by newest activity. + * **Search**: Filter contacts by name or phone number. + * **Export**: Download chat history as a `.txt` file. + +--- + +## Step 6: Keep it Alive (UptimeRobot) + +Render's free tier spins down after inactivity. To keep your logger running 24/7: + +1. Create a free account on [UptimeRobot](https://uptimerobot.com/). +2. Click **Add New Monitor**. +3. **Monitor Type**: HTTP(s). +4. **Friendly Name**: WhatsApp Logger. +5. **URL (or IP)**: `https://your-app.onrender.com/ping` (Make sure to add `/ping` at the end). +6. **Monitoring Interval**: 5 minutes. +7. Click **Create Monitor**. + +--- + +## Troubleshooting + +* **"No chats found"**: Send a message to the linked WhatsApp account to trigger the first log. +* **"Incorrect Credentials"**: Ensure your Render backend is running and you are using the exact Username/Password defined in Render Environment Variables. +* **Proof/Phone Numbers**: If a chat shows a long ID (e.g., `1155...@lid`), wait a few minutes. The backend automatically syncs contacts and updates the record with the real phone number. + +## Disclaimer + +This tool is for personal archiving purposes. Using it to log conversations without consent may violate privacy laws in your jurisdiction. The author is not responsible for misuse. diff --git a/SECURITY.md b/SECURITY.md @@ -0,0 +1,25 @@ +# Security Policy + +## Supported Versions + +Please use the latest version of this project to ensure you have the latest security patches. + +## Reporting a Vulnerability + +We take the security of this project seriously. If you discover a security vulnerability, please do **NOT** open a public issue. + +Instead, please send an email to: +**amitddutta4255@gmail.com** + +Please include: +- A description of the vulnerability. +- Steps to reproduce the issue. +- Any relevant code snippets or logs (redacted). + +We will acknowledge your email within 48 hours and will keep you updated on the progress of the fix. + +## Privacy & Logs + +This application handles private communication data. +- **Strict No-Logging Policy:** The application is designed to never output message content to system logs (stdout/stderr). +- If you modify the source code, you are responsible for maintaining this policy to prevent leaking private messages into server logs. diff --git a/index.html b/index.html @@ -0,0 +1,811 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> + <title>WhatsApp Logger M3</title> + + <!-- SVG Favicon --> + <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='30' fill='%23006C4C'/%3E%3Cpath d='M30 35C30 32.2386 32.2386 30 35 30H65C67.7614 30 70 32.2386 70 35V55C70 57.7614 67.7614 60 65 60H45L30 70V35Z' fill='white'/%3E%3Ccircle cx='42' cy='45' r='3' fill='%23006C4C'/%3E%3Ccircle cx='50' cy='45' r='3' fill='%23006C4C'/%3E%3Ccircle cx='58' cy='45' r='3' fill='%23006C4C'/%3E%3C/svg%3E"> + + <!-- Fonts & Icons --> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Google+Sans:wght@400;500;700&display=swap" rel="stylesheet"> + <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" /> + + <!-- Firebase SDKs --> + <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js"></script> + <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-firestore-compat.js"></script> + + <style> + :root { + /* M3 Light Theme Tokens - Nature/Mint Palette */ + --md-sys-color-primary: #006C4C; + --md-sys-color-on-primary: #FFFFFF; + --md-sys-color-primary-container: #89F8C7; + --md-sys-color-on-primary-container: #002114; + + --md-sys-color-secondary: #4D6357; + --md-sys-color-on-secondary: #FFFFFF; + --md-sys-color-secondary-container: #CFE9D9; + --md-sys-color-on-secondary-container: #092016; + + --md-sys-color-surface: #FBFDF9; + --md-sys-color-surface-dim: #D9DBD9; + --md-sys-color-surface-container: #ECEEEB; + --md-sys-color-surface-container-high: #E6E8E5; + + --md-sys-color-on-surface: #191C1A; + --md-sys-color-on-surface-variant: #404944; + --md-sys-color-outline: #707974; + --md-sys-color-outline-variant: #BFC9C2; + + --md-sys-color-error: #BA1A1A; + + /* WhatsApp Specifics */ + --chat-bubble-me: #E2FFEC; + --chat-bubble-them: #FFFFFF; + + /* Elevations & Shapes */ + --elevation-1: 0px 1px 3px 1px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3); + --elevation-2: 0px 2px 6px 2px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3); + --shape-corner-extra-large: 28px; + --shape-corner-large: 16px; + --shape-corner-medium: 12px; + + /* Motion */ + --motion-easing-emphasized: cubic-bezier(0.2, 0.0, 0.0, 1.0); + --motion-duration-medium: 400ms; + } + + * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; } + + body { + font-family: 'Roboto', sans-serif; + background-color: var(--md-sys-color-surface); + color: var(--md-sys-color-on-surface); + height: 100vh; + overflow: hidden; + display: flex; + justify-content: center; + } + + /* --- ANIMATIONS --- */ + .hidden { display: none !important; } + .fade-in { animation: fadeIn 0.4s var(--motion-easing-emphasized); } + @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } + + .stagger-enter { opacity: 0; transform: translateY(15px); animation: staggerSlideUp 0.5s var(--motion-easing-emphasized) forwards; } + @keyframes staggerSlideUp { to { opacity: 1; transform: translateY(0); } } + + .delay-1 { animation-delay: 0.1s; } + .delay-2 { animation-delay: 0.2s; } + .delay-3 { animation-delay: 0.3s; } + .delay-4 { animation-delay: 0.4s; } + + /* --- RIPPLE EFFECT --- */ + .ripple-surface { + position: relative; + overflow: hidden; + transform: translate3d(0, 0, 0); + transition: transform 0.1s; + } + .ripple-surface:active { transform: scale(0.98); } + .ripple { + position: absolute; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.1); + transform: scale(0); + animation: ripple-anim 0.6s linear; + pointer-events: none; + } + @keyframes ripple-anim { to { transform: scale(4); opacity: 0; } } + + /* --- AUTH SCREEN --- */ + #auth-screen { + position: fixed; inset: 0; z-index: 999; + background: radial-gradient(circle at 50% 10%, #E6F7EF 0%, var(--md-sys-color-surface) 60%); + display: flex; flex-direction: column; align-items: center; justify-content: center; + padding: 24px; + } + + .auth-card { + width: 100%; max-width: 400px; + text-align: center; + } + + .brand-icon { + width: 80px; height: 80px; + display: inline-flex; align-items: center; justify-content: center; + margin-bottom: 24px; + animation: float 6s ease-in-out infinite; + } + .brand-icon svg { width: 100%; height: 100%; filter: drop-shadow(0 4px 12px rgba(0, 108, 76, 0.2)); } + @keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } } + + .m3-field { + position: relative; margin-bottom: 16px; + background: var(--md-sys-color-surface-container-high); + border-radius: 4px 4px 0 0; + border-bottom: 1px solid var(--md-sys-color-outline); + transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + .m3-field:focus-within { + border-bottom: 2px solid var(--md-sys-color-primary); + background: var(--md-sys-color-surface-container); + } + + .m3-input { + width: 100%; padding: 24px 48px 8px 16px; + border: none; background: transparent; outline: none; + font-size: 16px; color: var(--md-sys-color-on-surface); + font-family: inherit; + } + .m3-label { + position: absolute; left: 16px; top: 18px; + color: var(--md-sys-color-on-surface-variant); + font-size: 16px; pointer-events: none; transition: 0.2s; + } + .m3-input:focus ~ .m3-label, .m3-input:not(:placeholder-shown) ~ .m3-label { + top: 6px; font-size: 12px; color: var(--md-sys-color-primary); + } + + .eye-toggle { + position: absolute; right: 12px; top: 50%; transform: translateY(-50%); + border: none; background: none; cursor: pointer; + color: var(--md-sys-color-on-surface-variant); + padding: 8px; border-radius: 50%; + display: flex; align-items: center; justify-content: center; + transition: background 0.2s; + } + .eye-toggle:hover { background: rgba(0,0,0,0.05); color: var(--md-sys-color-on-surface); } + + .m3-btn { + width: 100%; height: 56px; border-radius: 28px; + border: none; outline: none; cursor: pointer; + font-family: 'Google Sans', sans-serif; font-weight: 500; font-size: 16px; + display: flex; align-items: center; justify-content: center; gap: 8px; + transition: 0.2s; + } + .m3-btn.filled { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + box-shadow: var(--elevation-1); + } + .m3-btn.filled:hover { box-shadow: var(--elevation-2); background: #005A3F; transform: translateY(-1px); } + .m3-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; } + + /* --- APP LAYOUT --- */ + #app-layout { + display: flex; width: 100%; height: 100%; max-width: 1600px; + background: var(--md-sys-color-surface); + opacity: 0; transition: opacity 0.6s ease-in-out; + } + + /* --- SIDEBAR --- */ + .sidebar { + width: 400px; display: flex; flex-direction: column; + border-right: 1px solid var(--md-sys-color-outline-variant); + background: var(--md-sys-color-surface); + z-index: 10; + } + + .sidebar-header { + padding: 16px 20px; + display: flex; justify-content: space-between; align-items: center; + } + + .search-container { padding: 0 16px 12px; } + .search-bar { + background: var(--md-sys-color-surface-container-high); + border-radius: 28px; height: 48px; + display: flex; align-items: center; padding: 0 16px; + transition: 0.3s; + } + .search-bar:focus-within { background: var(--md-sys-color-surface-container); box-shadow: 0 1px 4px rgba(0,0,0,0.1); } + .search-bar input { + border: none; background: transparent; outline: none; + flex: 1; font-size: 16px; margin-left: 12px; + color: var(--md-sys-color-on-surface); + } + + .contact-list { + flex: 1; overflow-y: auto; overflow-x: hidden; + padding: 8px; + } + + .contact-item { + display: flex; align-items: center; padding: 12px 16px; + border-radius: 16px; cursor: pointer; + transition: background 0.2s, transform 0.1s; margin-bottom: 2px; + } + .contact-item:hover { background: rgba(0,0,0,0.03); } + .contact-item.active { background: var(--md-sys-color-secondary-container); } + .contact-item.active .contact-name { color: var(--md-sys-color-on-secondary-container); font-weight: 700; } + + .avatar { + width: 48px; height: 48px; border-radius: 50%; + background: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + display: flex; align-items: center; justify-content: center; + font-size: 18px; font-weight: 500; flex-shrink: 0; + margin-right: 16px; + transition: transform 0.3s; + } + .contact-item:hover .avatar { transform: scale(1.05); } + + .contact-info { flex: 1; min-width: 0; } + .contact-top { display: flex; justify-content: space-between; margin-bottom: 4px; } + .contact-name { font-size: 16px; font-weight: 500; color: var(--md-sys-color-on-surface); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .contact-time { font-size: 12px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; margin-left: 8px;} + .contact-sub { font-size: 14px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + + /* --- CHAT AREA --- */ + .chat-area { + flex: 1; display: flex; flex-direction: column; + position: relative; + background-color: #EFE7DE; + background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236e7874' fill-opacity='0.08' fill-rule='evenodd'/%3E%3C/svg%3E"); + } + + .chat-header { + height: 64px; + background: rgba(251, 253, 249, 0.95); + backdrop-filter: blur(10px); + display: flex; align-items: center; padding: 0 16px; + border-bottom: 1px solid rgba(0,0,0,0.05); + z-index: 5; + } + + .chat-content { + flex: 1; overflow-y: auto; padding: 16px; + display: flex; flex-direction: column; gap: 4px; + } + + /* Messages */ + .date-divider { + align-self: center; + background: var(--md-sys-color-surface-container-high); + color: var(--md-sys-color-on-surface-variant); + padding: 6px 12px; + border-radius: 8px; + font-size: 12px; font-weight: 500; + margin: 16px 0; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); + } + + .msg-bubble { + max-width: 80%; + padding: 8px 12px; + position: relative; + font-size: 15px; line-height: 1.4; + box-shadow: 0 1px 2px rgba(0,0,0,0.1); + animation: popIn 0.3s var(--motion-easing-emphasized); + display: flex; flex-direction: column; + } + + .msg-in { + align-self: flex-start; + background: var(--chat-bubble-them); + border-radius: 0 16px 16px 16px; + color: var(--md-sys-color-on-surface); + } + .msg-out { + align-self: flex-end; + background: var(--chat-bubble-me); + border-radius: 16px 0 16px 16px; + color: var(--md-sys-color-on-surface); + } + + .msg-sender { font-size: 12px; font-weight: 700; color: var(--md-sys-color-primary); margin-bottom: 2px; } + .msg-meta { font-size: 10px; opacity: 0.6; align-self: flex-end; margin-top: 2px; margin-left: 8px; display: inline-flex; align-items: center; gap: 2px; } + + @keyframes popIn { from { opacity: 0; transform: translateY(10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } + + /* Icon Buttons */ + .icon-btn { + width: 40px; height: 40px; border-radius: 50%; + border: none; background: transparent; cursor: pointer; + color: var(--md-sys-color-on-surface-variant); + display: flex; align-items: center; justify-content: center; + transition: 0.2s; + } + .icon-btn:hover { background: rgba(0,0,0,0.05); color: var(--md-sys-color-on-surface); } + .back-btn { display: none; margin-right: 8px; } + + /* Empty State */ + .empty-state { + display: flex; flex-direction: column; align-items: center; justify-content: center; + height: 100%; color: var(--md-sys-color-outline); + text-align: center; + animation: fadeIn 0.5s; + } + + /* --- SKELETON LOADER --- */ + .skeleton-wrapper { width: 100%; } + .skeleton { + background: linear-gradient(90deg, #f0f2f0 25%, #f7f9f7 50%, #f0f2f0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite linear; + border-radius: 4px; + } + @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } + + /* Specific Skeleton Items */ + .skel-contact { display: flex; align-items: center; padding: 12px 16px; gap: 16px; } + .skel-avatar { width: 48px; height: 48px; border-radius: 50%; } + .skel-text-col { flex: 1; display: flex; flex-direction: column; gap: 8px; } + + .skel-chat { display: flex; flex-direction: column; gap: 16px; padding: 20px; } + .skel-bubble { height: 40px; border-radius: 12px; width: 40%; } + .skel-left { align-self: flex-start; border-bottom-left-radius: 0; } + .skel-right { align-self: flex-end; border-bottom-right-radius: 0; width: 50%; } + + /* --- MOBILE ADAPTATION --- */ + @media (max-width: 768px) { + .sidebar { width: 100%; flex: 1; border-right: none; } + .chat-area { + position: fixed; inset: 0; z-index: 20; + transform: translateX(100%); transition: transform 0.35s cubic-bezier(0.2, 0, 0, 1); + } + .chat-area.active { transform: translateX(0); } + .back-btn { display: flex; } + } + </style> +</head> +<body> + + <!-- AUTHENTICATION OVERLAY --> + <div id="auth-screen"> + <div class="auth-card"> + <div class="brand-icon stagger-enter"> + <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> + <rect width="100" height="100" rx="30" fill="#006C4C"/> + <path d="M30 35C30 32.2386 32.2386 30 35 30H65C67.7614 30 70 32.2386 70 35V55C70 57.7614 67.7614 60 65 60H45L30 70V35Z" fill="white"/> + <circle cx="42" cy="45" r="3" fill="#006C4C"/> + <circle cx="50" cy="45" r="3" fill="#006C4C"/> + <circle cx="58" cy="45" r="3" fill="#006C4C"/> + </svg> + </div> + <h1 class="stagger-enter delay-1" style="font-family: 'Google Sans'; margin-bottom: 8px; color: var(--md-sys-color-on-surface);">Welcome Back</h1> + <p class="stagger-enter delay-1" style="color: var(--md-sys-color-on-surface-variant); margin-bottom: 32px;">Enter your credentials to access logs</p> + + <div class="m3-field stagger-enter delay-2"> + <input type="text" id="username" class="m3-input" placeholder=" " autocomplete="off"> + <label class="m3-label">Username</label> + </div> + + <div class="m3-field stagger-enter delay-3"> + <input type="password" id="password" class="m3-input" placeholder=" "> + <label class="m3-label">Password</label> + <button class="eye-toggle" onclick="togglePasswordVisibility()"> + <span class="material-symbols-rounded" id="passIcon">visibility</span> + </button> + </div> + + <div class="stagger-enter delay-3" style="display:flex; align-items:center; gap:12px; margin: 16px 0 24px;"> + <input type="checkbox" id="rememberMe" style="width:18px; height:18px; accent-color: var(--md-sys-color-primary); cursor: pointer;"> + <label for="rememberMe" style="font-size:14px; color:var(--md-sys-color-on-surface); cursor: pointer;">Keep me logged in</label> + </div> + + <button class="m3-btn filled ripple-surface stagger-enter delay-4" onclick="attemptLogin()" id="loginBtn"> + <span id="loginText">Access Dashboard</span> + <span class="material-symbols-rounded" id="loginIcon">arrow_forward</span> + </button> + <p id="authError" style="color: var(--md-sys-color-error); margin-top: 16px; font-size: 14px; opacity: 0; transition: 0.3s;">Invalid Credentials</p> + </div> + </div> + + <!-- MAIN APPLICATION --> + <div id="app-layout"> + <!-- Sidebar: Contact List --> + <aside class="sidebar"> + <div class="sidebar-header"> + <h2 style="font-family:'Google Sans'; font-size: 22px;">Chats</h2> + <div style="display:flex; gap: 8px;"> + <button class="icon-btn ripple-surface" onclick="promptAddChat()" title="Add Chat"> + <span class="material-symbols-rounded">add_circle</span> + </button> + <button class="icon-btn ripple-surface" onclick="handleLogout()" title="Logout"> + <span class="material-symbols-rounded">logout</span> + </button> + </div> + </div> + + <div class="search-container"> + <div class="search-bar"> + <span class="material-symbols-rounded" style="color: var(--md-sys-color-on-surface-variant);">search</span> + <input type="text" id="searchInput" placeholder="Search..."> + </div> + </div> + + <div class="contact-list" id="contactList"> + <!-- Content injected via JS --> + </div> + </aside> + + <!-- Main Chat Area --> + <main class="chat-area" id="chatArea"> + <header class="chat-header"> + <button class="icon-btn back-btn ripple-surface" onclick="closeChatMobile()"> + <span class="material-symbols-rounded">arrow_back</span> + </button> + <div id="chatHeaderContent" style="display:flex; align-items:center; gap:12px; flex:1; opacity:0; transition:0.3s;"> + <div class="avatar" id="headerAvatar" style="width:40px; height:40px; font-size:16px;">A</div> + <div> + <div id="headerName" style="font-weight: 500; font-size: 16px;">Alice</div> + <div id="headerId" style="font-size: 12px; opacity: 0.7;">Online</div> + </div> + </div> + </header> + + <div class="chat-content" id="messagesContainer"> + <div class="empty-state"> + <div style="width:120px; height:120px; margin-bottom:24px;"> + <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" style="width: 100%; height: 100%;"> + <rect width="100" height="100" rx="30" fill="#E6F7EF"/> + <path d="M30 35C30 32.2386 32.2386 30 35 30H65C67.7614 30 70 32.2386 70 35V55C70 57.7614 67.7614 60 65 60H45L30 70V35Z" fill="#006C4C"/> + <circle cx="42" cy="45" r="3" fill="white"/> + <circle cx="50" cy="45" r="3" fill="white"/> + <circle cx="58" cy="45" r="3" fill="white"/> + </svg> + </div> + <h3 style="margin-bottom:8px; color:var(--md-sys-color-on-surface);">WhatsApp Logger</h3> + <p>Select a conversation to view history</p> + </div> + </div> + </main> + </div> + + <script> + // CONFIGURATION + const RENDER_BACKEND_URL = ""; + const firebaseConfig = { + apiKey: "", + authDomain: "", + projectId: "", + storageBucket: "", + messagingSenderId: "", + appId: "" + }; + + // INIT FIREBASE + firebase.initializeApp(firebaseConfig); + const db = firebase.firestore(); + let messageUnsubscribe = null; + let activeChatId = null; + let isCurrentChatGroup = false; + + // --- RIPPLE EFFECT --- + document.addEventListener('mousedown', function(e) { + const target = e.target.closest('.ripple-surface'); + if (target) { + const rect = target.getBoundingClientRect(); + const ripple = document.createElement('span'); + ripple.className = 'ripple'; + const size = Math.max(rect.width, rect.height); + ripple.style.width = ripple.style.height = `${size}px`; + ripple.style.left = `${e.clientX - rect.left - size/2}px`; + ripple.style.top = `${e.clientY - rect.top - size/2}px`; + target.appendChild(ripple); + setTimeout(() => ripple.remove(), 600); + } + }); + + // --- AUTH & INIT --- + window.onload = () => { + if(localStorage.getItem('wp_auth_expiry') > Date.now()) { + revealApp(); + } + }; + + function togglePasswordVisibility() { + const input = document.getElementById('password'); + const icon = document.getElementById('passIcon'); + if (input.type === 'password') { + input.type = 'text'; + icon.innerText = 'visibility_off'; + } else { + input.type = 'password'; + icon.innerText = 'visibility'; + } + } + + async function attemptLogin() { + const btn = document.getElementById('loginBtn'); + const btnText = document.getElementById('loginText'); + const btnIcon = document.getElementById('loginIcon'); + const err = document.getElementById('authError'); + + btn.disabled = true; + btnText.innerText = 'Verifying...'; + btnIcon.innerText = 'refresh'; + btnIcon.style.animation = 'spin 1s linear infinite'; + + try { + const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + username: document.getElementById('username').value, + password: document.getElementById('password').value + }) + }); + + const data = await response.json(); + + if(data.success) { + if(document.getElementById('rememberMe').checked) { + localStorage.setItem('wp_auth_expiry', Date.now() + (3 * 24 * 60 * 60 * 1000)); + } + setTimeout(revealApp, 600); + } else { + throw new Error('Invalid'); + } + } catch (e) { + err.style.opacity = '1'; + err.innerText = e.message === 'Invalid' ? 'Incorrect username or password' : 'Server unavailable'; + + btn.disabled = false; + btnText.innerText = 'Access Dashboard'; + btnIcon.innerText = 'arrow_forward'; + btnIcon.style.animation = 'none'; + + btn.style.animation = 'shake 0.4s ease-in-out'; + setTimeout(() => btn.style.animation = '', 400); + } + } + + function revealApp() { + const auth = document.getElementById('auth-screen'); + const app = document.getElementById('app-layout'); + + auth.style.opacity = '0'; + auth.style.pointerEvents = 'none'; + setTimeout(() => auth.style.display = 'none', 600); + + app.style.opacity = '1'; + loadChats(); + } + + function handleLogout() { + localStorage.removeItem('wp_auth_expiry'); + location.reload(); + } + + // --- SKELETON GENERATORS --- + function renderContactSkeleton() { + const list = document.getElementById('contactList'); + let html = ''; + for(let i=0; i<6; i++) { + html += ` + <div class="skel-contact fade-in" style="animation-delay: ${i*0.05}s"> + <div class="skeleton skel-avatar"></div> + <div class="skel-text-col"> + <div class="skeleton" style="width:60%; height:16px;"></div> + <div class="skeleton" style="width:40%; height:12px;"></div> + </div> + </div>`; + } + list.innerHTML = html; + } + + function renderChatSkeleton() { + const container = document.getElementById('messagesContainer'); + container.innerHTML = ` + <div class="skel-chat"> + <div class="skeleton skel-bubble skel-left"></div> + <div class="skeleton skel-bubble skel-right" style="width:60%"></div> + <div class="skeleton skel-bubble skel-left" style="width:30%"></div> + <div class="skeleton skel-bubble skel-left" style="width:50%"></div> + <div class="skeleton skel-bubble skel-right"></div> + </div> + `; + } + + // --- CHAT LOGIC --- + async function loadChats() { + renderContactSkeleton(); + + try { + const list = document.getElementById('contactList'); + const snap = await db.collection('Chats').get(); + + let chats = []; + const excludedIds = []; + + snap.forEach(doc => { + const data = doc.data(); + if (data.lastActive && !excludedIds.includes(doc.id)) { + chats.push({ id: doc.id, ...data }); + } + }); + + chats.sort((a, b) => b.lastActive - a.lastActive); + + list.innerHTML = ''; + if(chats.length === 0) { + list.innerHTML = `<div style="text-align:center; padding:32px; color:var(--md-sys-color-outline);">No chats found</div>`; + return; + } + + chats.forEach((chat, index) => { + const isGroup = chat.id.includes('@g.us'); + let name, subText; + + if (isGroup) { + name = (chat.displayName && chat.displayName !== 'Unknown') ? chat.displayName : "Group Chat"; + subText = "Group Chat"; + } else { + const phone = chat.phoneNumber || chat.id.split('@')[0]; + name = (chat.displayName && chat.displayName !== 'Unknown') ? chat.displayName : phone; + subText = (name === phone) ? chat.id.split('@')[0] : phone; + } + + const time = formatTime(chat.lastActive); + const colorIndex = (chat.id.charCodeAt(0) + chat.id.charCodeAt(chat.id.length-1)) % 5; + const colors = ['#FFD8E4', '#D7F6E3', '#EADDFF', '#FDE2C4', '#DCECF9']; + const textColors = ['#31111D', '#04210F', '#1D192B', '#2B1700', '#001E30']; + + const div = document.createElement('div'); + div.className = 'contact-item ripple-surface fade-in'; + div.style.animationDelay = `${index * 0.05}s`; + div.dataset.id = chat.id; + div.innerHTML = ` + <div class="avatar" style="background:${colors[colorIndex]}; color:${textColors[colorIndex]}">${isGroup ? '<span class="material-symbols-rounded">group</span>' : name.slice(0,1).toUpperCase()}</div> + <div class="contact-info"> + <div class="contact-top"> + <span class="contact-name">${name}</span> + <span class="contact-time">${time}</span> + </div> + <div class="contact-sub">${subText}</div> + </div> + `; + div.onclick = () => openChat(chat.id, name, div, colors[colorIndex], textColors[colorIndex], isGroup); + list.appendChild(div); + }); + + } catch (err) { + console.error("Error loading chats:", err); + } + } + + function openChat(id, name, element, avatarBg, avatarColor, isGroup) { + document.querySelectorAll('.contact-item').forEach(el => el.classList.remove('active')); + if(element) element.classList.add('active'); + activeChatId = id; + isCurrentChatGroup = isGroup; + + const chatArea = document.getElementById('chatArea'); + chatArea.classList.add('active'); + + document.getElementById('headerAvatar').style.background = avatarBg || '#e0e0e0'; + document.getElementById('headerAvatar').style.color = avatarColor || '#000'; + document.getElementById('headerAvatar').innerHTML = isGroup ? '<span class="material-symbols-rounded">group</span>' : name.slice(0,1).toUpperCase(); + document.getElementById('headerName').innerText = name; + document.getElementById('headerId').innerText = isGroup ? "Group Info" : id.split('@')[0]; + document.getElementById('chatHeaderContent').style.opacity = '1'; + + renderChatSkeleton(); + + if(messageUnsubscribe) messageUnsubscribe(); + + messageUnsubscribe = db.collection('Chats').doc(id).collection('Messages') + .orderBy('timestamp', 'asc') + .onSnapshot(snap => { + renderMessages(snap); + }); + } + + function renderMessages(snap) { + const container = document.getElementById('messagesContainer'); + container.innerHTML = ''; + + if(snap.empty) { + container.innerHTML = '<div class="empty-state"><p>No messages recorded</p></div>'; + return; + } + + let lastDateStr = ''; + + snap.forEach(doc => { + const msg = doc.data(); + const date = new Date(msg.timestamp * 1000); + const dateStr = getDateString(date); + + if(dateStr !== lastDateStr) { + const div = document.createElement('div'); + div.className = 'date-divider'; + div.innerText = dateStr; + container.appendChild(div); + lastDateStr = dateStr; + } + + const bubble = document.createElement('div'); + bubble.className = `msg-bubble ${msg.fromMe ? 'msg-out' : 'msg-in'}`; + + const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }); + const showSenderName = !msg.fromMe && isCurrentChatGroup; + + bubble.innerHTML = ` + ${showSenderName ? `<div class="msg-sender">${msg.senderName || 'Sender'}</div>` : ''} + <div>${msg.text}</div> + <div class="msg-meta"> + ${timeStr} + ${msg.fromMe ? '<span class="material-symbols-rounded" style="font-size:14px; color:var(--md-sys-color-primary)">done_all</span>' : ''} + </div> + `; + + container.appendChild(bubble); + }); + + container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' }); + } + + function closeChatMobile() { + document.getElementById('chatArea').classList.remove('active'); + setTimeout(() => { + if(!document.querySelector('.chat-area.active')) { + activeChatId = null; + } + }, 300); + } + + function promptAddChat() { + const id = prompt("Enter WhatsApp ID (e.g. 123456789@s.whatsapp.net):"); + if(id && id.includes('@')) { + db.collection('Chats').doc(id).set({ + displayName: 'Manual Add', + id: id, + lastActive: Math.floor(Date.now()/1000) + }, {merge:true}); + setTimeout(loadChats, 1000); + } + } + + document.getElementById('searchInput').addEventListener('input', (e) => { + const val = e.target.value.toLowerCase(); + document.querySelectorAll('.contact-item').forEach(el => { + const name = el.innerText.toLowerCase(); + const id = el.dataset.id.toLowerCase(); + el.style.display = (name.includes(val) || id.includes(val)) ? 'flex' : 'none'; + }); + }); + + function formatTime(ts) { + if (!ts) return ''; + const date = new Date(ts * 1000); + const now = new Date(); + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + + if (date.toDateString() === now.toDateString()) { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }); + } else if (date.toDateString() === yesterday.toDateString()) { + return 'Yesterday'; + } else { + return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }); + } + } + + function getDateString(date) { + const now = new Date(); + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + + if (date.toDateString() === now.toDateString()) return 'Today'; + if (date.toDateString() === yesterday.toDateString()) return 'Yesterday'; + return date.toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'long' }); + } + + const style = document.createElement('style'); + style.innerHTML = ` + @keyframes spin { to { transform: rotate(360deg); } } + @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } } + `; + document.head.appendChild(style); + + </script> +</body> +</html> +\ No newline at end of file diff --git a/index.js b/index.js @@ -0,0 +1,320 @@ +const { + default: makeWASocket, + useMultiFileAuthState, + DisconnectReason, + fetchLatestBaileysVersion +} = require('@whiskeysockets/baileys'); +const express = require('express'); +const QRCode = require('qrcode'); +const pino = require('pino'); +const admin = require('firebase-admin'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +// --- CONFIGURATION --- +const PORT = process.env.PORT || 3000; +const AUTH_USER = process.env.AUTH_USER; +const AUTH_PASS = process.env.AUTH_PASS; + +// Initialize Express +const app = express(); +app.use(express.urlencoded({ extended: true })); // Parse form data +app.use(express.json()); // Parse JSON bodies (for API requests) + +// --- FIREBASE SETUP --- +let serviceAccount; +try { + if (process.env.FIREBASE_SERVICE_ACCOUNT) { + serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT); + } else { + serviceAccount = require('./serviceAccountKey.json'); + } + + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount) + }); + console.log("System: Firebase Admin initialized successfully."); +} catch (error) { + console.error("System Error: Failed to initialize Firebase. Make sure FIREBASE_SERVICE_ACCOUNT env var is set."); + process.exit(1); +} + +const db = admin.firestore(); + +// --- BAILEYS SETUP --- +let qrCodeData = null; +let sock = null; +let isConnected = false; + +async function startWhatsApp() { + const logger = pino({ level: 'silent' }); + + const { state, saveCreds } = await useMultiFileAuthState('auth_info_baileys'); + const { version } = await fetchLatestBaileysVersion(); + + sock = makeWASocket({ + version, + logger, + printQRInTerminal: false, + auth: state, + browser: ["WhatsApp Logger by notamitgamer", "Chrome", "1.0.0"], + syncFullHistory: false + }); + + sock.ev.on('connection.update', (update) => { + const { connection, lastDisconnect, qr } = update; + + if (qr) { + qrCodeData = qr; + isConnected = false; + } + + if (connection === 'close') { + isConnected = false; + const shouldReconnect = (lastDisconnect.error)?.output?.statusCode !== DisconnectReason.loggedOut; + if (shouldReconnect) { + startWhatsApp(); + } + } else if (connection === 'open') { + console.log("System: Connection Open and Authenticated"); + qrCodeData = null; + isConnected = true; + } + }); + + sock.ev.on('creds.update', saveCreds); + + sock.ev.on('messages.upsert', async ({ messages, type }) => { + if (type !== 'notify' && type !== 'append') return; + + for (const msg of messages) { + try { + if (!msg.message) continue; + + const remoteJid = msg.key.remoteJid; + if (remoteJid === 'status@broadcast') continue; + + const textContent = + msg.message.conversation || + msg.message.extendedTextMessage?.text || + msg.message.imageMessage?.caption || + msg.message.videoMessage?.caption || + ""; + + if (!textContent) continue; + + const timestamp = msg.messageTimestamp + ? (typeof msg.messageTimestamp === 'number' ? msg.messageTimestamp : msg.messageTimestamp.low) + : Math.floor(Date.now() / 1000); + + const isFromMe = msg.key.fromMe || false; + const senderName = isFromMe ? "Me" : (msg.pushName || "Unknown"); + const phoneNumber = remoteJid.split('@')[0]; // Extract number from JID + + // --- FIX: Create/Update Parent Chat Document --- + // This makes the chat visible in the list automatically + await db.collection('Chats').doc(remoteJid).set({ + lastActive: timestamp, + displayName: senderName, + phoneNumber: phoneNumber, // Added: Save phone number/ID to Chat + id: remoteJid + }, { merge: true }); + + // --- Save Message --- + await db.collection('Chats') + .doc(remoteJid) + .collection('Messages') + .doc(msg.key.id) + .set({ + text: textContent, + senderId: remoteJid, + senderName: senderName, + senderPhoneNumber: phoneNumber, // Added: Save phone number/ID to Message + timestamp: timestamp, + fromMe: isFromMe, + id: msg.key.id + }, { merge: true }); + + } catch (err) { + // Silent error handling + } + } + }); +} + +// --- AUTH UTILS --- +const SESSION_SECRET = crypto.createHash('sha256').update(AUTH_PASS || 'default').digest('hex'); + +function parseCookies(request) { + const list = {}; + const rc = request.headers.cookie; + if (rc) { + rc.split(';').forEach((cookie) => { + const parts = cookie.split('='); + list[parts.shift().trim()] = decodeURI(parts.join('=')); + }); + } + return list; +} + +// --- EXPRESS ROUTES --- + +// 1. PUBLIC ROUTE: Ping +app.get('/ping', (req, res) => { + res.status(200).send('Pong'); +}); + +// 2. PUBLIC ROUTE: Verify Credentials API +app.post('/api/verify', (req, res) => { + // CORS Headers + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); + + const { username, password } = req.body; + + if (username === AUTH_USER && password === AUTH_PASS) { + return res.json({ success: true }); + } else { + return res.status(401).json({ success: false }); + } +}); + +app.options('/api/verify', (req, res) => { + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); + res.sendStatus(200); +}); + +// 3. LOGIN PAGE (GET) +app.get('/login', (req, res) => { + res.send(` + <html> + <body style="font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5;"> + <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;"> + <h2 style="margin-top: 0; text-align: center;">WhatsApp Logger</h2> + <div style="margin-bottom: 1rem;"> + <label style="display: block; margin-bottom: 0.5rem;">Username</label> + <input type="text" name="username" required style="width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"> + </div> + <div style="margin-bottom: 1rem;"> + <label style="display: block; margin-bottom: 0.5rem;">Password</label> + <input type="password" name="password" required style="width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"> + </div> + <div style="margin-bottom: 1rem;"> + <label style="display: flex; align-items: center; font-size: 0.9rem;"> + <input type="checkbox" name="remember" value="yes" style="margin-right: 0.5rem;"> + Keep me logged in for 5 mins + </label> + </div> + <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> + </form> + </body> + </html> + `); +}); + +// 4. LOGIN ACTION (POST) +app.post('/login', (req, res) => { + const { username, password, remember } = req.body; + + if (username === AUTH_USER && password === AUTH_PASS) { + let cookieSettings = 'HttpOnly; Path=/;'; + + if (remember === 'yes') { + cookieSettings += ' Max-Age=300;'; + } + + res.setHeader('Set-Cookie', `auth_session=${SESSION_SECRET}; ${cookieSettings}`); + return res.redirect('/'); + } + + res.status(401).send('Invalid credentials. <a href="/login">Try again</a>'); +}); + +// 5. LOGOUT ACTION +app.get('/logout', (req, res) => { + res.setHeader('Set-Cookie', 'auth_session=; Max-Age=0; Path=/;'); + res.redirect('/login'); +}); + +// --- MIDDLEWARE: FORM AUTH --- +const checkAuth = (req, res, next) => { + if (!AUTH_USER || !AUTH_PASS) return next(); + + const cookies = parseCookies(req); + if (cookies.auth_session === SESSION_SECRET) { + return next(); + } + + if (req.path.startsWith('/api')) { + res.status(401).send('Unauthorized'); + } else { + res.redirect('/login'); + } +}; + +app.use(checkAuth); + +// 6. PROTECTED ROUTE: Main Page (QR Code) +app.get('/', async (req, res) => { + 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>`; + + if (isConnected) { + return res.send(` + <html> + <body style="font-family: sans-serif; text-align: center; padding-top: 50px; background-color: #f0f2f5;"> + ${logoutBtn} + <div style="background: white; padding: 40px; border-radius: 10px; display: inline-block; box-shadow: 0 4px 12px rgba(0,0,0,0.1);"> + <h2 style="color: green;">System Operational</h2> + <p style="color: #555;">Connected to WhatsApp.</p> + <p style="color: #999; font-size: 12px;">Back-end Service</p> + </div> + </body> + </html> + `); + } + + if (qrCodeData) { + try { + const qrImage = await QRCode.toDataURL(qrCodeData); + return res.send(` + <html> + <head><meta http-equiv="refresh" content="5"></head> + <body style="font-family: sans-serif; text-align: center; padding-top: 50px; background-color: #f0f2f5;"> + ${logoutBtn} + <div style="background: white; padding: 40px; border-radius: 10px; display: inline-block; box-shadow: 0 4px 12px rgba(0,0,0,0.1);"> + <h2>Scan to Link</h2> + <img src="${qrImage}" alt="QR Code" /> + <p style="color: #666;">Refreshes every 5 seconds...</p> + </div> + </body> + </html> + `); + } catch (e) { + return res.send("Error generating QR."); + } + } + + return res.send(` + <html> + <head><meta http-equiv="refresh" content="2"></head> + <body style="font-family: sans-serif; text-align: center; padding-top: 50px;"> + <p>Initializing... please refresh.</p> + ${logoutBtn} + </body> + </html> + `); +}); + +// --- START SERVER --- +app.listen(PORT, () => { + startWhatsApp(); + console.log(`Server running on port ${PORT}`); + + if (AUTH_USER && AUTH_PASS) { + console.log("Security: Form Authentication is ENABLED."); + } else { + console.log("Security: Form Authentication is DISABLED (Env vars missing)."); + } +}); diff --git a/package.json b/package.json @@ -0,0 +1,21 @@ +{ + "name": "whatsapp-logger-backend", + "version": "1.0.0", + "description": "Backend for WhatsApp Logger using Baileys and Firebase", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "@whiskeysockets/baileys": "^6.6.0", + "express": "^4.18.2", + "firebase-admin": "^11.11.1", + "pino": "^8.17.2", + "qrcode": "^1.5.3" + }, + "engines": { + "node": ">=16.0.0" + }, + "author": "Amit", + "license": "MIT" +} +\ No newline at end of file