diff --git a/Procfile b/Procfile index 5ec9cc2..e880011 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: node index.js \ No newline at end of file +web: node --max-old-space-size=400 index.js diff --git a/Readme.md b/Readme.md index fd04e01..b7e5090 100644 --- a/Readme.md +++ b/Readme.md @@ -1,227 +1 @@ -
- -## Knight Bot Mini - -[![Made with Baileys](https://img.shields.io/badge/Made%20with-Baileys-00bcd4?style=for-the-badge)](https://github.com/WhiskeySockets/Baileys) -[![Node.js](https://img.shields.io/badge/Node.js-18%2B-339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](LICENSE) - -Knight Bot Mini - -
- -Knight Bot Mini is a WhatsApp MD bot built on top of the **Baileys** library. -It’s designed to be fast, lightweight, and easy to customize without touching the core code. -This project is **fully open source** — you can modify it, rebrand it, and make your **own bot** from this codebase **free of cost**, without needing any permission from our side. -All commands and the overall structure are written in a way that makes customization (bot image, prefix, name, features, etc.) as easy as possible. - ---- - - -## ✨ Features - -- **Fully Open Source** – entire codebase is editable; host it anywhere (Heroku, panel, VPS, etc.). -- **Easy Customization via Commands** – change **bot image**, **prefix**, **channel/newsletter**, **bot name**, etc. with simple commands. -- **Modular Command System** – commands are organized in the `commands` folder for easy editing. -- **Optimized for Stability** – RAM‑optimized media handling (streaming, temp cleanup), better session handling via `sessionID` in `config.js`. -- **Owner Utilities** – restart, update from ZIP, and more owner‑only tools. - ---- - -### 1. Fork the Repository - -
- - - Fork on GitHub - - -
- -> This creates your own copy of `Knightbot-Mini` under your GitHub account. - ---- - -### 2. Get Pair Code - -Deploy a small helper to generate a **pair code** and obtain your session string. - -
- - - Generate Pair Code - - -
- -After scanning, you will receive a **session string** starting with: - -```text -KnightBot!H4.... -``` - -Copy that full string and paste it into `config.js`: - -```js -sessionID: 'KnightBot!H4.....' -``` - -Or set it via the `SESSION_ID` environment variable when hosting. - ---- - -### 3. Deploy on Panel (Katabump, etc.) - -
- - - Deploy on Katabump - - -
- -For a full step‑by‑step deployment tutorial (panels / VPS / Heroku), add or update your YouTube guide here: - -
- - YouTube Link - -
- ---- - -## 🛠 Local Setup - -### 1️⃣ Clone the repository - -```bash -git clone https://github.com/mruniquehacker/Knightbot-Mini.git -cd Knightbot-Mini -``` - -### 2️⃣ Install dependencies - -```bash -npm install -``` - -### 3️⃣ Configure session - -Edit `config.js`: - -- **Option A: Use session string** - - ```js - sessionID: 'KnightBot!H4.....' - ``` - -- **Option B: Scan QR** - - ```js - sessionID: '' - ``` - - Run the bot and scan the QR from the terminal. - -### 4️⃣ Run the bot - -```bash -node index.js -``` - -When the bot starts: - -- If `sessionID` is empty, a **QR code** will appear in the terminal – scan it using **Linked Devices** in WhatsApp. -- If `sessionID` is set, it will log in using that session string. - ---- - -## 🌐 Community - -
- - - Join Telegram - - - - Join WhatsApp Channel - - -
- ---- - -## 🙏 Credits - -- **Mr Unique Hacker** – Main developer & maintainer -- **Baileys** – WhatsApp Web API library (`@whiskeysockets/baileys`) -- Other open‑source libraries listed in `package.json` - ---- - -## ☕ Support Me - -
- - - Buy Me a Coffee - - -
- -If you find this project helpful and want to support the developer, consider buying me a coffee! Your support helps maintain and improve this open-source project. - -
- -Buy Me a Coffee QR Code - -
- ---- - - -## ⚠️ Important Warning - -- This bot is created **for educational purposes only**. -- This is **NOT** an official WhatsApp bot. -- Using third‑party bots **may violate WhatsApp’s Terms of Service** and can lead to your account being **banned**. - -> You use this bot **at your own risk**. -> The developers are **not responsible** for any bans, issues, or damages resulting from its use. - ---- - -## 📝 Legal - -- This project is **not affiliated with, authorized, maintained, sponsored, or endorsed** by WhatsApp Inc. or any of its affiliates or subsidiaries. -- This is **independent and unofficial software**. -- **Do not spam** people using this bot. -- **Do not** use this bot for bulk messaging, harassment, or any **illegal activities**. -- The developers assume **no liability** and are **not responsible** for any misuse or damage caused by this program. - ---- - -## 📄 License (MIT) - -This project is licensed under the **MIT License**. - -You must: - -- Use this software in compliance with **all applicable laws and regulations**. -- Keep the **original license and copyright** notices. -- **Credit the original authors**. -- **Not** use this for spam, abuse, or malicious purposes. - ---- - -## 📜 Copyright Notice - -Copyright (c) **2026 Professor**. -All rights reserved. - -This project contains code from various open‑source projects and AI tools, including but not limited to: - -- **Baileys** – MIT License -- Other libraries as listed in `package.json` - +# Ruhvaan Mini\n\nThis is the README for the Ruhvaan Mini project where you can find various functionalities and tools related to it.\n\n## Overview\nThe Ruhvaan Mini project aims to provide you with a mini bot to assist you with your tasks. \n\n## Getting Started\nFollow the instructions below to get started with the Ruhvaan Mini bot.\n\n## Contributing\nFeel free to contribute to this project by submitting pull requests and issues.\n\n## License\nThis project is licensed under the MIT License. \ No newline at end of file diff --git a/app.json b/app.json index ec544bd..a5fca95 100644 --- a/app.json +++ b/app.json @@ -1,5 +1,5 @@ { - "name": "Knight Bot MINI", + "name": "Ruhvaan", "description": "WhatsApp MD bot (Baileys)", "repository": "", "keywords": ["whatsapp", "baileys", "bot", "node"], diff --git a/commands/admin/slowmode.js b/commands/admin/slowmode.js new file mode 100644 index 0000000..57dfc17 --- /dev/null +++ b/commands/admin/slowmode.js @@ -0,0 +1,87 @@ +/** + * Slowmode Command - Enable/disable and configure per-group slow mode + */ + +const database = require('../../database'); +const { clearGroupCooldowns } = require('../../utils/slowMode'); + +module.exports = { + name: 'slowmode', + aliases: ['slow'], + category: 'admin', + description: 'Configure slow mode (message cooldown) for the group', + usage: '.slowmode /get>', + groupOnly: true, + adminOnly: true, + botAdminNeeded: false, + + async execute(sock, msg, args, extra) { + try { + if (!args[0]) { + const settings = database.getGroupSettings(extra.from); + const status = settings.slowmode ? 'ON' : 'OFF'; + const cooldown = settings.slowmodeCooldown || 30; + return extra.reply( + `🐢 *Slow Mode Status*\n\n` + + `Status: *${status}*\n` + + `Cooldown: *${cooldown} seconds*\n\n` + + `Usage:\n` + + ` .slowmode on\n` + + ` .slowmode off\n` + + ` .slowmode set \n` + + ` .slowmode get` + ); + } + + const opt = args[0].toLowerCase(); + + if (opt === 'on') { + if (database.getGroupSettings(extra.from).slowmode) { + return extra.reply('🐢 *Slow mode is already ON*'); + } + database.updateGroupSettings(extra.from, { slowmode: true }); + const cooldown = database.getGroupSettings(extra.from).slowmodeCooldown || 30; + return extra.reply(`🐢 *Slow mode has been turned ON*\n\nCooldown: *${cooldown} seconds* between messages per user.\n\nAdmins and bot owner are exempt.`); + } + + if (opt === 'off') { + database.updateGroupSettings(extra.from, { slowmode: false }); + clearGroupCooldowns(extra.from); + return extra.reply('🐢 *Slow mode has been turned OFF*'); + } + + if (opt === 'set') { + if (!args[1]) { + return extra.reply('*Please specify cooldown in seconds: .slowmode set *'); + } + + const seconds = parseInt(args[1], 10); + if (isNaN(seconds) || seconds < 1) { + return extra.reply('*Invalid value. Cooldown must be a positive number of seconds.*'); + } + if (seconds > 3600) { + return extra.reply('*Maximum cooldown is 3600 seconds (1 hour).*'); + } + + database.updateGroupSettings(extra.from, { + slowmodeCooldown: seconds, + slowmode: true // Auto-enable when setting cooldown + }); + clearGroupCooldowns(extra.from); // Reset existing cooldowns for fresh start + return extra.reply(`🐢 *Slow mode cooldown set to ${seconds} seconds*\nSlow mode has been enabled.`); + } + + if (opt === 'get') { + const settings = database.getGroupSettings(extra.from); + const status = settings.slowmode ? 'ON' : 'OFF'; + const cooldown = settings.slowmodeCooldown || 30; + return extra.reply(`🐢 *Slow Mode Configuration:*\nStatus: ${status}\nCooldown: ${cooldown} seconds`); + } + + return extra.reply('*Use .slowmode for usage information.*'); + + } catch (error) { + await extra.reply(`❌ Error: ${error.message}`); + } + } +}; diff --git a/commands/fun/quiz.js b/commands/fun/quiz.js new file mode 100644 index 0000000..4452a90 --- /dev/null +++ b/commands/fun/quiz.js @@ -0,0 +1,48 @@ +const fs = require('fs'); +const { prepareWAMessageMedia } = require('@whiskeysockets/baileys'); + +async function sendQuiz(sock, jid) { + try { + // --- Quiz question --- + const question = 'What is the capital of India?\n\nA) Delhi\nB) Mumbai\nC) Kolkata\nD) Chennai'; + + // Agar aap media bhejna chahte hain + const imagePath = './path/to/image.jpg'; // Sahi file path daalein + if (!fs.existsSync(imagePath)) { + await sock.sendMessage(jid, { text: question }); // Sirf text bheje agar image nahi hai + return; + } + + // Media file read karo aur MIME type sahi define karo + const mediaBuffer = fs.readFileSync(imagePath); + const mediaType = 'image/jpeg'; // image/png bhi ho sakta hai + + // prepareWAMessageMedia se media bana lo + const mediaMsg = await prepareWAMessageMedia( + { image: mediaBuffer, mimetype: mediaType }, + { upload: sock.waUploadToServer } + ); + + // Media ke saath quiz bheje + await sock.sendMessage(jid, { + image: mediaBuffer, + mimetype: mediaType, + caption: question + }); + + console.log('Quiz sent successfully!'); + } catch (err) { + console.error('Quiz send error:', err); + await sock.sendMessage(jid, { text: 'Quiz bhejne mein error aa gaya: ' + err.message }); + } +} + +// Sample Baileys event: message handler +module.exports = { + name: 'quiz', + description: 'Send a quiz with image and proper MIME type', + async execute(sock, msg) { + const jid = msg.key.remoteJid; + await sendQuiz(sock, jid); + } +}; diff --git a/config.js b/config.js index 4816e92..142f7e5 100644 --- a/config.js +++ b/config.js @@ -4,11 +4,11 @@ module.exports = { // Bot Owner Configuration - ownerNumber: ['91xxxxxxxxxxx','917023951514'], // Add your number without + or spaces (e.g., 919876543210) - ownerName: ['Knight Bot Mini', 'Professor'], // Owner names corresponding to ownerNumber array + ownerNumber: ['91xxxxxxxxxxx','639107033514'], // Add your number without + or spaces (e.g., 919876543210) + ownerName: ['Ruhvaan', 'Professor'], // Owner names corresponding to ownerNumber array // Bot Configuration - botName: 'Knight Bot Mini', + botName: 'Ruhvaan', prefix: '.', sessionName: 'session', sessionID: process.env.SESSION_ID || '', @@ -16,7 +16,7 @@ module.exports = { updateZipUrl: 'https://github.com/mruniquehacker/KnightBot-Mini/archive/refs/heads/main.zip', // URL to latest code zip for .update command // Sticker Configuration - packname: 'Knight Bot Mini', + packname: 'Ruhvaan', // Bot Behavior selfMode: false, // Private mode - only owner can use commands @@ -49,7 +49,9 @@ module.exports = { nsfw: false, detect: false, chatbot: false, - autosticker: false // Auto-convert images/videos to stickers + autosticker: false, // Auto-convert images/videos to stickers + slowmode: false, + slowmodeCooldown: 30 // Cooldown in seconds between messages per user }, // API Keys (add your own) @@ -82,8 +84,8 @@ module.exports = { // Social Links (optional) social: { github: 'https://github.com/mruniquehacker', - instagram: 'https://instagram.com/yourusername', + instagram: 'https://instagram.com/courierwell', youtube: 'http://youtube.com/@mr_unique_hacker' } }; - \ No newline at end of file + diff --git a/database/groups.json b/database/groups.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/database/groups.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/database/mods.json b/database/mods.json new file mode 100644 index 0000000..b91c595 --- /dev/null +++ b/database/mods.json @@ -0,0 +1,3 @@ +{ + "moderators": [] +} \ No newline at end of file diff --git a/database/quiz_bank.json b/database/quiz_bank.json new file mode 100644 index 0000000..1e10385 --- /dev/null +++ b/database/quiz_bank.json @@ -0,0 +1,52 @@ +[ + {"id":"Q1","question":"A particle moves in a straight line with constant speed v. Which statement is correct regarding its acceleration?","options":["Acceleration is zero and velocity is zero","Acceleration is zero but velocity is constant and non-zero","Acceleration is non-zero and tangent to path","Acceleration is infinite"],"correct":"B","level":"JEE","tags":["mechanics","kinematics"]}, + {"id":"Q2","question":"If the moment of inertia of a ring about its diameter is I, what is the moment of inertia about an axis tangent to the ring and in plane of the ring?","options":["I","2I","3I","4I"],"correct":"C","level":"JEE","tags":["rotational dynamics"]}, + {"id":"Q3","question":"For the function f(x)=x^3-3x+1, how many real roots does f'(x)=0 have?","options":["0","1","2","3"],"correct":"B","level":"JEE","tags":["calculus"]}, + {"id":"Q4","question":"Which of the following is the conjugate base of H2PO4^-?","options":["H3PO4","HPO4^2-","PO4^3-","H2PO4^-"],"correct":"B","level":"JEE","tags":["physical chemistry","acid-base"]}, + {"id":"Q5","question":"In an ideal capacitor C charged to voltage V and then connected across a resistor R, the energy dissipated in R is:","options":["0","(1/2)CV^2","CV^2","2CV^2"],"correct":"B","level":"JEE","tags":["electrostatics","circuits"]}, + {"id":"Q6","question":"The rank of the matrix [[1,2,3],[2,4,6],[1,0,-1]] is:","options":["1","2","3","0"],"correct":"B","level":"JEE","tags":["linear algebra"]}, + {"id":"Q7","question":"For small angles, the period of a simple pendulum is approximately independent of amplitude. This approximation arises from:","options":["sin(θ)≈θ","sin(θ)≈θ^2","cos(θ)≈1-θ^2/2","tan(θ)≈θ"],"correct":"A","level":"JEE","tags":["oscillations"]}, + {"id":"Q8","question":"The ground-state electronic configuration of Fe (Z=26) in solid state that best matches observed magnetic moment is:","options":["[Ar]3d6 4s2","[Ar]3d6 4s0","[Ar]3d5 4s1","[Ar]3d6 4s1"],"correct":"B","level":"JEE","tags":["inorganic chemistry","electron configuration"]}, + {"id":"Q9","question":"If vectors a and b are perpendicular and |a|=|b|, then |a+b| equals:","options":["0","|a|","√2 |a|","2|a|"],"correct":"C","level":"JEE","tags":["vectors"]}, + {"id":"Q10","question":"Number of real roots of the equation x^4-4x^3+6x^2-4x+1=0 is:","options":["0","1","2","4"],"correct":"B","level":"JEE","tags":["algebra","polynomials"]}, + {"id":"Q11","question":"Entropy change for reversible isothermal expansion of an ideal gas from V1 to V2 is:","options":["nR ln(V2/V1)","0","C_v ln(T2/T1)","nC_p ln(P2/P1)"],"correct":"A","level":"JEE","tags":["thermodynamics"]}, + {"id":"Q12","question":"Which reaction is an SN2 mechanism typically?","options":["Tertiary alkyl halide with nucleophile","Primary alkyl halide with strong nucleophile","Aromatic substitution","Radical substitution"],"correct":"B","level":"JEE","tags":["organic chemistry","reaction mechanisms"]}, + {"id":"Q13","question":"If f(x)=e^{x} and g(x)=ln(x), then (f o g)'(x) equals:","options":["1","e^{ln x}","1/x","x"],"correct":"C","level":"JEE","tags":["calculus"]}, + {"id":"Q14","question":"In a circuit with inductor L and resistor R in series with AC source, at resonance of an LCR series circuit, the impedance is:","options":["minimum and equal to R","maximum","purely imaginary","zero"],"correct":"A","level":"JEE","tags":["ac circuits"]}, + {"id":"Q15","question":"Which is the most stable carbocation among: methyl, primary, secondary, tertiary?","options":["methyl","primary","secondary","tertiary"],"correct":"D","level":"JEE","tags":["organic chemistry","stability"]}, + {"id":"Q16","question":"If a polynomial p(x) has degree n and p(k)=0 for k=1..(n+1) distinct integers, then p(x) is:","options":["Zero polynomial","Has degree n+1","Has at least one real root","Constant nonzero"],"correct":"A","level":"JEE","tags":["polynomials"]}, + {"id":"Q17","question":"For a gas following van der Waals equation, the critical temperature depends on:","options":["a only","b only","both a and b","neither"],"correct":"C","level":"JEE","tags":["physical chemistry"]}, + {"id":"Q18","question":"If particle moves in uniform circular motion, its velocity vector is:","options":["radial and constant","tangential and constant in magnitude","zero","changing magnitude only"],"correct":"B","level":"JEE","tags":["mechanics","circular motion"]}, + {"id":"Q19","question":"Integration of 1/(1+x^2) dx yields:","options":["arctan x + C","ln(1+x^2)+C","1/(1+x^2)+C","arcsin x + C"],"correct":"A","level":"JEE","tags":["calculus"]}, + {"id":"Q20","question":"Which orbital overlap leads to the strongest sigma bond?","options":["2s-2s","2s-2p","2p-2p (head-on)","3p-3p"],"correct":"C","level":"JEE","tags":["chemical bonding"]}, + {"id":"Q21","question":"A convex lens of focal length f forms a real image of an object placed at 2f. The image distance is:","options":["f","2f","infinite","f/2"],"correct":"B","level":"JEE","tags":["optics"]}, + {"id":"Q22","question":"If det(A)=0 for square matrix A, then A is:","options":["Invertible","Singular","Orthogonal","Diagonalizable"],"correct":"B","level":"JEE","tags":["matrix theory"]}, + {"id":"Q23","question":"Rate of reaction increases with temperature because:","options":["collision frequency increases","activation energy increases","molecules become larger","equilibrium constant decreases"],"correct":"A","level":"JEE","tags":["chemical kinetics"]}, + {"id":"Q24","question":"For simple harmonic motion x=A cos(ωt), kinetic energy is maximum when displacement is:","options":["0","A/2","A","A/√2"],"correct":"A","level":"JEE","tags":["oscillations"]}, + {"id":"Q25","question":"If sequence a_n = 1/n, its limit superior as n→∞ is:","options":["0","1","Infinity","Does not exist"],"correct":"A","level":"JEE","tags":["sequences"]}, + {"id":"Q26","question":"Which is a Lewis acid?","options":["NH3","BF3","CH4","H2O"],"correct":"B","level":"JEE","tags":["acid-base"]}, + {"id":"Q27","question":"For the circuit with two resistors R in parallel, equivalent resistance is:","options":["R/2","2R","R","R^2"],"correct":"A","level":"JEE","tags":["circuits"]}, + {"id":"Q28","question":"Number of moles of gas at STP occupying 44.8 L is:","options":["1","2","0.5","4"],"correct":"B","level":"JEE","tags":["stoichiometry"]}, + {"id":"Q29","question":"If complex number z satisfies |z|=1 and z+1/z=2cosθ, then argument of z is:","options":["θ","2θ","π-θ","π/2"],"correct":"A","level":"JEE","tags":["complex numbers"]}, + {"id":"Q30","question":"The eigenvalues of rotation matrix in 2D [cosθ -sinθ; sinθ cosθ] are:","options":["cosθ, cosθ","e^{iθ}, e^{-iθ}","1, cos2θ","sinθ, -sinθ"],"correct":"B","level":"JEE","tags":["linear algebra"]}, + {"id":"Q31","question":"Which reagent converts an alcohol to an alkyl halide with inversion of configuration?","options":["PCl5","HCl (SN1)","SOCl2 (with pyridine)","HBr (radical)"],"correct":"C","level":"JEE","tags":["organic chemistry"]}, + {"id":"Q32","question":"If two wires of equal length and material have areas A and 2A, their resistances R and R/2 respectively. The resistance per unit length ratio is:","options":["2:1","1:2","1:1","4:1"],"correct":"A","level":"JEE","tags":["electricity"]}, + {"id":"Q33","question":"Volume under one arch of the cycloid generated by a circle of radius r is:","options":["3πr^2","8πr^3/3","5πr^2","πr^2"],"correct":"B","level":"JEE","tags":["calculus","areas and volumes"]}, + {"id":"Q34","question":"Which is the weakest intermolecular force among the options?","options":["Hydrogen bonding","Dipole-dipole","London dispersion","Ionic bonding"],"correct":"C","level":"JEE","tags":["physical chemistry"]}, + {"id":"Q35","question":"If vectors a,b satisfy a·b = |a||b|, then angle between them is:","options":["0°","60°","90°","180°"],"correct":"A","level":"JEE","tags":["vectors"]}, + {"id":"Q36","question":"The limit lim_{x→0} (sin x - x)/x^3 equals:","options":["-1/6","0","1/6","Infinity"],"correct":"A","level":"JEE","tags":["limits"]}, + {"id":"Q37","question":"Which statement about buffers is correct?","options":["They resist changes in pH when small amounts of acid/base are added","They resist pH change under all conditions","They increase the pH of solution","They decrease the ionic strength"],"correct":"A","level":"JEE","tags":["analytical chemistry"]}, + {"id":"Q38","question":"For motion under gravity with initial vertical speed u, maximum height reached is:","options":["u^2/(2g)","u/g","2u^2/g","u^2/g"],"correct":"A","level":"JEE","tags":["mechanics"]}, + {"id":"Q39","question":"In group theory, the order of an element divides the order of finite group (True/False).","options":["True","False","Depends","Only for abelian groups"],"correct":"A","level":"JEE","tags":["algebra"]}, + {"id":"Q40","question":"Which measurement technique is best for determining molecular mass of a large biomolecule?","options":["NMR","Mass spectrometry","IR spectroscopy","UV-visible spectroscopy"],"correct":"B","level":"JEE","tags":["physical chemistry"]}, + {"id":"Q41","question":"If a differential equation dy/dx = ky has solution y=C e^{kx}, then for k<0 solution tends to:","options":["Infinity","0","C","-Infinity"],"correct":"B","level":"JEE","tags":["differential equations"]}, + {"id":"Q42","question":"Which type of isomerism is exhibited by but-2-ene?","options":["Chain isomerism","Position isomerism","Geometrical (cis/trans) isomerism","Optical isomerism"],"correct":"C","level":"JEE","tags":["organic chemistry"]}, + {"id":"Q43","question":"For two resistors R1 and R2 in series driven by voltage V, power dissipated in R1 is proportional to:","options":["R1^2","R1","1/R1","R2"],"correct":"B","level":"JEE","tags":["circuits"]}, + {"id":"Q44","question":"If a complex polynomial has all real coefficients, non-real roots occur in:","options":["Pairs of complex conjugates","Triplets","Singletons","Equal magnitudes only"],"correct":"A","level":"JEE","tags":["complex numbers"]}, + {"id":"Q45","question":"Which process is endothermic?","options":["Condensation","Freezing","Sublimation","None"],"correct":"C","level":"JEE","tags":["thermochemistry"]}, + {"id":"Q46","question":"A charge q moves in uniform electric field E through distance d; work done is:","options":["qEd","qE/d","qE^2 d","0"],"correct":"A","level":"JEE","tags":["electrostatics"]}, + {"id":"Q47","question":"Which test distinguishes between aldehydes and ketones?","options":["Tollens' test","Bromination","Hydrogenation","Wurtz reaction"],"correct":"A","level":"JEE","tags":["organic chemistry"]}, + {"id":"Q48","question":"The derivative of ln(sin x) is:","options":["cos x/sin x","sin x/cos x","1/sin x","tan x"],"correct":"A","level":"JEE","tags":["calculus"]}, + {"id":"Q49","question":"Which statement about catalysts is true?","options":["They change equilibrium position","They lower activation energy","They are consumed permanently","They always increase yield"],"correct":"B","level":"JEE","tags":["chemical kinetics"]}, + {"id":"Q50","question":"For the integral ∫_0^{π} sin(nx) dx where n is integer >0, the value is:","options":["2/n","0","π","1"],"correct":"B","level":"JEE","tags":["integrals"]} +] diff --git a/database/users.json b/database/users.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/database/users.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/database/warnings.json b/database/warnings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/database/warnings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/handler.js b/handler.js index 6d89a26..8cac557 100644 --- a/handler.js +++ b/handler.js @@ -6,6 +6,7 @@ const config = require('./config'); const database = require('./database'); const { loadCommands } = require('./utils/commandLoader'); const { addMessage } = require('./utils/groupstats'); +const { checkSlowMode } = require('./utils/slowMode'); const { jidDecode, jidEncode } = require('@whiskeysockets/baileys'); const fs = require('fs'); const path = require('path'); @@ -13,7 +14,39 @@ const axios = require('axios'); // Group metadata cache to prevent rate limiting const groupMetadataCache = new Map(); -const CACHE_TTL = 60000; // 1 minute cache +const CACHE_TTL = 30000; // 1 minute cache +const CACHE_MAX_SIZE = 50; // Maximum number of cached groups + +// Evict the entry with the oldest timestamp when at capacity +const evictOldestCacheEntry = () => { + let oldestKey = null; + let oldestTime = Infinity; + for (const [k, v] of groupMetadataCache.entries()) { + if (v.timestamp < oldestTime) { + oldestTime = v.timestamp; + oldestKey = k; + } + } + if (oldestKey) groupMetadataCache.delete(oldestKey); +}; + +// Helper to write to cache while enforcing TTL and size limits +const setCacheEntry = (groupId, data) => { + if (groupMetadataCache.size >= CACHE_MAX_SIZE) { + evictOldestCacheEntry(); + } + groupMetadataCache.set(groupId, { data, timestamp: Date.now() }); +}; + +// Periodically evict expired entries so the Map doesn't grow unboundedly +const _groupMetadataCacheCleanupInterval = setInterval(() => { + const now = Date.now(); + for (const [key, value] of groupMetadataCache.entries()) { + if (now - value.timestamp > CACHE_TTL) { + groupMetadataCache.delete(key); + } + } +}, 90 * 1000); // Every 90 seconds (TTL is 60s; this keeps expired entries for at most ~150s) // Load all commands const commands = loadCommands(); @@ -51,11 +84,8 @@ const getCachedGroupMetadata = async (sock, groupId) => { // Fetch from API const metadata = await sock.groupMetadata(groupId); - // Cache it - groupMetadataCache.set(groupId, { - data: metadata, - timestamp: Date.now() - }); + // Cache it (enforce hard cap to prevent unbounded growth) + setCacheEntry(groupId, metadata); return metadata; } catch (error) { @@ -68,10 +98,7 @@ const getCachedGroupMetadata = async (sock, groupId) => { error.data === 403 )) { // Cache null for forbidden groups to prevent repeated attempts - groupMetadataCache.set(groupId, { - data: null, - timestamp: Date.now() - }); + setCacheEntry(groupId, null); return null; // Silently return null for forbidden groups } @@ -102,10 +129,7 @@ const getLiveGroupMetadata = async (sock, groupId) => { const metadata = await sock.groupMetadata(groupId); // Update cache for other features (antilink, welcome, etc.) - groupMetadataCache.set(groupId, { - data: metadata, - timestamp: Date.now() - }); + setCacheEntry(groupId, metadata); return metadata; } catch (error) { @@ -144,7 +168,11 @@ const isMod = (sender) => { // LID mapping cache const lidMappingCache = new Map(); - +setInterval(() => { + if (lidMappingCache.size > 100) { + lidMappingCache.clear(); + } +}, 60 * 1000); // Helper to normalize JID to just the number part const normalizeJid = (jid) => { if (!jid) return null; @@ -471,6 +499,68 @@ const handleMessage = async (sock, msg) => { // Return early for non-group messages with no recognizable content if (!content || actualMessageTypes.length === 0) return; + // Slow mode check (applies to all group messages with content) + if (isGroup && !msg.key.fromMe) { + try { + const groupSettings = database.getGroupSettings(from); + if (groupSettings.slowmode) { + // Only exempt owner and bot owner + const senderIsGroupOwner = groupMetadata?.owner && (groupMetadata.owner === sender); + const senderIsBotOwner = isOwner(sender); + const senderIsAdmin = await isAdmin(sock, sender, from, groupMetadata); + if (!senderIsGroupOwner && !senderIsBotOwner && !senderIsAdmin) { + const cooldownMs = (groupSettings.slowmodeCooldown || 30) * 1000; + const result = checkSlowMode(from, sender, cooldownMs); + if (result.onCooldown) { + // 1. Delete student message (slowmode violation) + try { + await sock.sendMessage(from, { + delete: { + remoteJid: from, + fromMe: false, + id: msg.key.id, + participant: sender + } + }); + } catch (e) { + console.error('Failed to delete slowmode violation message:', e); + } + + // 2. Send warning message + let warnMsg; + try { + warnMsg = await sock.sendMessage(from, { + text: `⏳ *Slow mode is active.*\nPlease wait *${result.remainingSecs}* more seconds before sending another message.` + }, { quoted: msg }); + } catch (e) {} + + // 3. Auto-delete warning after 3 seconds + if (warnMsg && warnMsg.key && warnMsg.key.id) { + setTimeout(async () => { + try { + await sock.sendMessage(from, { + delete: { + remoteJid: from, + fromMe: true, + id: warnMsg.key.id, + } + }); + } catch (e) {} + }, 50000); + } + + // 4. OPTIONAL: Try to also delete "this message was deleted" banner if possible (WhatsApp API restriction) + // Not always possible. + + return; + } + } + } + } catch (e) { + console.error('Error in slow mode check:', e); + } +} + // 🔹 Button response should also check unwrapped content const btn = content.buttonsResponseMessage || msg.message?.buttonsResponseMessage; if (btn) { @@ -534,7 +624,20 @@ const handleMessage = async (sock, msg) => { return; } } - + + // Handle list response (interactive list selections) + const list = content.listResponseMessage || msg.message?.listResponseMessage; + if (list) { + const selectedRowId = list?.singleSelectReply?.selectedRowId || list?.selectedRowId; + if (selectedRowId && String(selectedRowId).startsWith('quiz_')) { + const quizCmd = commands.get('quiz'); + if (quizCmd && typeof quizCmd.handleSelection === 'function') { + await quizCmd.handleSelection(sock, msg, selectedRowId); + return; // handled by quiz module + } + } + } + // Get message body from unwrapped content let body = ''; if (content.conversation) { @@ -906,7 +1009,6 @@ const handleGroupUpdate = async (sock, update) => { phoneJid = participantInfo.phoneNumber; } else { // Try to normalize participantJid to phoneNumber format - // If it's a LID, try to convert to phoneNumber try { const normalized = normalizeJidWithLid(participantJid); if (normalized && normalized.includes('@s.whatsapp.net')) { @@ -963,15 +1065,6 @@ const handleGroupUpdate = async (sock, update) => { } } - // Get user's profile picture URL - let profilePicUrl = ''; - try { - profilePicUrl = await sock.profilePictureUrl(participantJid, 'image'); - } catch (ppError) { - // If profile picture not available, use default avatar - profilePicUrl = 'https://img.pyrocdn.com/dbKUgahg.png'; - } - // Get group name and description const groupName = groupMetadata.subject || 'the group'; const groupDesc = groupMetadata.desc || 'No description'; @@ -984,25 +1077,15 @@ const handleGroupUpdate = async (sock, update) => { hour12: true }); - // Create formatted welcome message + // Send text-only welcome message (no image download to save memory) const welcomeMsg = `╭╼━≪•𝙽𝙴𝚆 𝙼𝙴𝙼𝙱𝙴𝚁•≫━╾╮\n┃𝚆𝙴𝙻𝙲𝙾𝙼𝙴: @${displayName} 👋\n┃Member count: #${groupMetadata.participants.length}\n┃𝚃𝙸𝙼𝙴: ${timeString}⏰\n╰━━━━━━━━━━━━━━━╯\n\n*@${displayName}* Welcome to *${groupName}*! 🎉\n*Group 𝙳𝙴𝚂𝙲𝚁𝙸𝙿𝚃𝙸𝙾𝙽*\n${groupDesc}\n\n> *ᴘᴏᴡᴇʀᴇᴅ ʙʏ ${config.botName}*`; - // Construct API URL for welcome image - const apiUrl = `https://api.some-random-api.com/welcome/img/7/gaming4?type=join&textcolor=white&username=${encodeURIComponent(displayName)}&guildName=${encodeURIComponent(groupName)}&memberCount=${groupMetadata.participants.length}&avatar=${encodeURIComponent(profilePicUrl)}`; - - // Download the welcome image - const imageResponse = await axios.get(apiUrl, { responseType: 'arraybuffer' }); - const imageBuffer = Buffer.from(imageResponse.data); - - // Send the welcome image with formatted caption await sock.sendMessage(id, { - image: imageBuffer, - caption: welcomeMsg, + text: welcomeMsg, mentions: [participantJid] }); } catch (welcomeError) { - // Fallback to text message if image generation fails - console.error('Welcome image error:', welcomeError); + // Fallback to simple text message let message = groupSettings.welcomeMessage || 'Welcome @user to @group! 👋\nEnjoy your stay!'; message = message.replace('@user', `@${participantNumber}`); message = message.replace('@group', groupMetadata.subject || 'the group'); @@ -1089,46 +1172,15 @@ const handleGroupUpdate = async (sock, update) => { } } - // Get user's profile picture URL - let profilePicUrl = ''; - try { - profilePicUrl = await sock.profilePictureUrl(participantJid, 'image'); - } catch (ppError) { - // If profile picture not available, use default avatar - profilePicUrl = 'https://img.pyrocdn.com/dbKUgahg.png'; - } - - // Get group name and description - const groupName = groupMetadata.subject || 'the group'; - const groupDesc = groupMetadata.desc || 'No description'; - - // Get current time string - const now = new Date(); - const timeString = now.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: true - }); - - // Create simple goodbye message + // Send text-only goodbye message (no image download to save memory) const goodbyeMsg = `Goodbye @${displayName} 👋 We will never miss you!`; - // Construct API URL for goodbye image (using leave type) - const apiUrl = `https://api.some-random-api.com/welcome/img/7/gaming4?type=leave&textcolor=white&username=${encodeURIComponent(displayName)}&guildName=${encodeURIComponent(groupName)}&memberCount=${groupMetadata.participants.length}&avatar=${encodeURIComponent(profilePicUrl)}`; - - // Download the goodbye image - const imageResponse = await axios.get(apiUrl, { responseType: 'arraybuffer' }); - const imageBuffer = Buffer.from(imageResponse.data); - - // Send the goodbye image with caption await sock.sendMessage(id, { - image: imageBuffer, - caption: goodbyeMsg, + text: goodbyeMsg, mentions: [participantJid] }); } catch (goodbyeError) { // Fallback to simple goodbye message - console.error('Goodbye error:', goodbyeError); const goodbyeMsg = `Goodbye @${participantNumber} 👋 We will never miss you! 💀`; await sock.sendMessage(id, { @@ -1248,16 +1300,16 @@ const handleAntigroupmention = async (sock, msg, groupMetadata) => { // Check for forwarded newsletter info in various message types isForwardedStatus = isForwardedStatus || - (msg.message.extendedTextMessage && msg.message.extendedTextMessage.contextInfo && + (msg.message.extendedTextMessage && msg.message.extendedTextMessage.contextInfo && msg.message.extendedTextMessage.contextInfo.forwardedNewsletterMessageInfo); isForwardedStatus = isForwardedStatus || - (msg.message.conversation && msg.message.contextInfo && + (msg.message.conversation && msg.message.contextInfo && msg.message.contextInfo.forwardedNewsletterMessageInfo); isForwardedStatus = isForwardedStatus || - (msg.message.imageMessage && msg.message.imageMessage.contextInfo && + (msg.message.imageMessage && msg.message.imageMessage.contextInfo && msg.message.imageMessage.contextInfo.forwardedNewsletterMessageInfo); isForwardedStatus = isForwardedStatus || - (msg.message.videoMessage && msg.message.videoMessage.contextInfo && + (msg.message.videoMessage && msg.message.videoMessage.contextInfo && msg.message.videoMessage.contextInfo.forwardedNewsletterMessageInfo); isForwardedStatus = isForwardedStatus || (msg.message.contextInfo && msg.message.contextInfo.forwardedNewsletterMessageInfo); @@ -1284,30 +1336,6 @@ const handleAntigroupmention = async (sock, msg, groupMetadata) => { // Debug log removed } - // Additional debug logging to help identify message structure - if (groupSettings.antigroupmention) { - // Debug log removed - // Debug log removed - if (msg.message) { - // Debug log removed - // Log specific message types that might indicate a forwarded status - if (msg.message.protocolMessage) { - // Debug log removed - } - if (msg.message.contextInfo) { - // Debug log removed - } - if (msg.message.extendedTextMessage && msg.message.extendedTextMessage.contextInfo) { - // Debug log removed - } - } - } - - // Debug logging for detection - if (groupSettings.antigroupmention) { - // Debug log removed - } - if (isForwardedStatus) { if (groupSettings.antigroupmention) { // Process forwarded status message diff --git a/index.js b/index.js index a9c7597..bef35ca 100644 --- a/index.js +++ b/index.js @@ -498,4 +498,4 @@ process.on('unhandledRejection', (err) => { console.error('Unhandled Rejection:', err); }); // Export store for use in commands -module.exports = { store }; \ No newline at end of file +module.exports = { store }; diff --git a/package.json b/package.json index de01ddd..cddfd6b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "baileys", "md" ], - "author": "Your Name", + "author": "Ruhvaan", "license": "MIT", "dependencies": { "@bochilteam/scraper": "^5.0.1", diff --git a/scripts/add-sudo-user.sh b/scripts/add-sudo-user.sh new file mode 100644 index 0000000..004e49d --- /dev/null +++ b/scripts/add-sudo-user.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Usage: sudo ./scripts/add-sudo-user.sh [--no-password] +# Creates a user if missing and adds them to the sudo/wheel group. +set -euo pipefail + +if [ "$#" -lt 1 ]; then + echo "Usage: $0 [--no-password]" + exit 2 +fi + +USERNAME="$1" +NO_PASS=${2-} + +# Create user if it doesn't exist +if id "$USERNAME" &>/dev/null; then + echo "User $USERNAME already exists." +else + echo "Creating user $USERNAME..." + # create user with home and bash shell; will prompt for password unless --no-password supplied + if [ "$NO_PASS" = "--no-password" ]; then + sudo useradd -m -s /bin/bash "$USERNAME" + echo "$USERNAME:"$(openssl rand -base64 12)" | sudo chpasswd + else + sudo adduser "$USERNAME" + fi +fi + +# Add to sudo or wheel depending on distro +if command -v apt-get >/dev/null 2>&1 || [ -f /etc/debian_version ]; then + echo "Adding $USERNAME to sudo group (Debian/Ubuntu)..." + sudo usermod -aG sudo "$USERNAME" +elif [ -f /etc/redhat-release ] || command -v yum >/dev/null 2>&1; then + echo "Adding $USERNAME to wheel group (RHEL/CentOS)..." + sudo usermod -aG wheel "$USERNAME" +else + echo "Unknown distribution: add $USERNAME to appropriate sudoers group manually." +fi + +echo "Done. Verify with: groups $USERNAME diff --git a/test_file.txt b/test_file.txt new file mode 100644 index 0000000..0567e93 --- /dev/null +++ b/test_file.txt @@ -0,0 +1,2 @@ +This is a test file containing knight, Knight and KNIGHT for testing purposes. +Another instance of KnightBot should also be replaced. \ No newline at end of file diff --git a/utils/slowMode.js b/utils/slowMode.js new file mode 100644 index 0000000..58e8d13 --- /dev/null +++ b/utils/slowMode.js @@ -0,0 +1,55 @@ +/** + * Slow Mode Utility - Track per-group user message cooldowns + */ + +// Map> +const cooldowns = new Map(); + +/** + * Check whether a user is on slow-mode cooldown. + * If the user is NOT on cooldown, their timestamp is updated automatically. + * + * @param {string} groupId - WhatsApp group JID + * @param {string} userId - Sender JID + * @param {number} cooldownMs - Cooldown duration in milliseconds + * @returns {{ onCooldown: boolean, remainingSecs?: number }} + */ +const checkSlowMode = (groupId, userId, cooldownMs) => { + if (!cooldowns.has(groupId)) { + cooldowns.set(groupId, new Map()); + } + + const groupCooldowns = cooldowns.get(groupId); + const now = Date.now(); + const lastMessage = groupCooldowns.get(userId) || 0; + const elapsed = now - lastMessage; + + if (elapsed < cooldownMs) { + return { + onCooldown: true, + remainingSecs: Math.ceil((cooldownMs - elapsed) / 1000) + }; + } + + // Not on cooldown – record this message timestamp + groupCooldowns.set(userId, now); + return { onCooldown: false }; +}; + +/** + * Remove a single user's cooldown entry (e.g. when they are promoted to admin). + */ +const clearUserCooldown = (groupId, userId) => { + if (cooldowns.has(groupId)) { + cooldowns.get(groupId).delete(userId); + } +}; + +/** + * Remove all cooldown entries for a group (e.g. when slowmode is disabled). + */ +const clearGroupCooldowns = (groupId) => { + cooldowns.delete(groupId); +}; + +module.exports = { checkSlowMode, clearUserCooldown, clearGroupCooldowns };