From 8f0588f4a5fcdf40c29bda982d1f905c417350bf Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Sun, 22 Mar 2026 22:18:13 +0530 Subject: [PATCH 01/27] Replace Knight with Ruhvaan across repo --- test_file.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 test_file.txt 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 From 1d2a78b757fa93d09e1044847315ebc3cd6b818c Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Sun, 22 Mar 2026 22:20:07 +0530 Subject: [PATCH 02/27] Replace Knight with Ruhvaan across repo --- Readme.md | 228 +----------------------------------------------------- 1 file changed, 1 insertion(+), 227 deletions(-) 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 From e455ecc726317310044ed82a17648c525ddca316 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 06:56:44 +0000 Subject: [PATCH 03/27] Initial plan From 125f522725bde11bf18d42e4324aee6f1e5fc5b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 07:01:01 +0000 Subject: [PATCH 04/27] Add slow mode/cooldown timer feature for WhatsApp groups Co-authored-by: elanaforrozeira-jpg <247721364+elanaforrozeira-jpg@users.noreply.github.com> Agent-Logs-Url: https://github.com/elanaforrozeira-jpg/KnightBot-Mini/sessions/6f1dd789-0a42-4cd6-a8a5-a62aeeb175c4 --- commands/admin/slowmode.js | 87 ++++++++++++++++++++++++++++++++++++++ config.js | 4 +- database/groups.json | 1 + database/mods.json | 3 ++ database/users.json | 1 + database/warnings.json | 1 + handler.js | 24 +++++++++++ utils/slowMode.js | 55 ++++++++++++++++++++++++ 8 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 commands/admin/slowmode.js create mode 100644 database/groups.json create mode 100644 database/mods.json create mode 100644 database/users.json create mode 100644 database/warnings.json create mode 100644 utils/slowMode.js 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/config.js b/config.js index 4816e92..4799e42 100644 --- a/config.js +++ b/config.js @@ -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) 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/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..edaf31d 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'); @@ -471,6 +472,29 @@ 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) { + const senderIsAdmin = await isAdmin(sock, sender, from, groupMetadata); + const senderIsOwner = isOwner(sender); + if (!senderIsAdmin && !senderIsOwner) { + const cooldownMs = (groupSettings.slowmodeCooldown || 30) * 1000; + const result = checkSlowMode(from, sender, cooldownMs); + if (result.onCooldown) { + await sock.sendMessage(from, { + text: `⏳ *Slow mode is active.*\nPlease wait *${result.remainingSecs}* more second${result.remainingSecs === 1 ? '' : 's'} before sending another message.` + }, { quoted: msg }); + 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) { 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 }; From 96baadccf06fec70f42afeccf242f8f538a5b6c1 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 13:13:45 +0530 Subject: [PATCH 05/27] Update config.js --- config.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config.js b/config.js index 4799e42..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 @@ -84,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 + From c9c523750d2c5df2faeca5c8168af458257574b4 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 13:16:18 +0530 Subject: [PATCH 06/27] Update app.json --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"], From f31e6ec45a0d825bad67ca6c6de9296228d0caa2 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 13:17:04 +0530 Subject: [PATCH 07/27] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From 56998a3a5936088c6d6c6c485d3e707db54cd3ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:04:32 +0000 Subject: [PATCH 08/27] Initial plan From 5311910f1627c940c475b7dcee515e540759e154 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:10:17 +0000 Subject: [PATCH 09/27] Fix memory leaks and Heroku OOM crashes Co-authored-by: elanaforrozeira-jpg <247721364+elanaforrozeira-jpg@users.noreply.github.com> Agent-Logs-Url: https://github.com/elanaforrozeira-jpg/KnightBot-Mini/sessions/cd63e565-a8c5-4b0b-98f2-a6e359579a03 --- handler.js | 109 ++++++++++++++++++++--------------------------------- index.js | 4 +- 2 files changed, 43 insertions(+), 70 deletions(-) diff --git a/handler.js b/handler.js index edaf31d..43473c0 100644 --- a/handler.js +++ b/handler.js @@ -15,6 +15,38 @@ const axios = require('axios'); // Group metadata cache to prevent rate limiting const groupMetadataCache = new Map(); const CACHE_TTL = 60000; // 1 minute cache +const CACHE_MAX_SIZE = 200; // 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(); @@ -52,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) { @@ -69,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 } @@ -103,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) { @@ -987,15 +1010,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'; @@ -1008,25 +1022,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'); @@ -1113,46 +1117,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, { diff --git a/index.js b/index.js index a9c7597..576007d 100644 --- a/index.js +++ b/index.js @@ -86,7 +86,7 @@ function cleanupPuppeteerCache() { // Optimized in-memory store with hard limits (Map-based for better memory management) const store = { messages: new Map(), // Use Map instead of plain object - maxPerChat: 20, // Limit to 20 messages per chat + maxPerChat: 10, // Limit to 10 messages per chat bind: (ev) => { ev.on('messages.upsert', ({ messages }) => { @@ -383,7 +383,7 @@ async function startBot() { const chatMsgs = store.messages.get(from); chatMsgs.set(msg.key.id, msg); - // Cleanup: Keep only last 20 per chat (reduced from 200) + // Cleanup: Keep only last 10 per chat (reduced from 20) if (chatMsgs.size > store.maxPerChat) { // Remove oldest messages const sortedIds = Array.from(chatMsgs.entries()) From 415e3045dd1feeefe19a9ecdf0a094e1a4ed1a92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:33:53 +0000 Subject: [PATCH 10/27] Initial plan From 17f6ce05fbff8da594e7335ca86052396263b325 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:36:30 +0000 Subject: [PATCH 11/27] Fix slowmode to only exempt bot owner and group owner, not all admins Co-authored-by: elanaforrozeira-jpg <247721364+elanaforrozeira-jpg@users.noreply.github.com> Agent-Logs-Url: https://github.com/elanaforrozeira-jpg/KnightBot-Mini/sessions/9434206f-f797-43e2-acac-ba2edbacef9d --- handler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handler.js b/handler.js index 43473c0..de09635 100644 --- a/handler.js +++ b/handler.js @@ -500,9 +500,9 @@ const handleMessage = async (sock, msg) => { try { const groupSettings = database.getGroupSettings(from); if (groupSettings.slowmode) { - const senderIsAdmin = await isAdmin(sock, sender, from, groupMetadata); const senderIsOwner = isOwner(sender); - if (!senderIsAdmin && !senderIsOwner) { + const senderIsGroupOwner = groupMetadata && findParticipant(groupMetadata.participants || [], sender)?.admin === 'superadmin'; + if (!senderIsOwner && !senderIsGroupOwner) { const cooldownMs = (groupSettings.slowmodeCooldown || 30) * 1000; const result = checkSlowMode(from, sender, cooldownMs); if (result.onCooldown) { From eb25978354e528b63bca7023d8335df7d3312075 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 14:49:47 +0530 Subject: [PATCH 12/27] Update handler.js --- handler.js | 72 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/handler.js b/handler.js index de09635..6a30e6e 100644 --- a/handler.js +++ b/handler.js @@ -496,27 +496,65 @@ const handleMessage = async (sock, msg) => { 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) { - const senderIsOwner = isOwner(sender); - const senderIsGroupOwner = groupMetadata && findParticipant(groupMetadata.participants || [], sender)?.admin === 'superadmin'; - if (!senderIsOwner && !senderIsGroupOwner) { - const cooldownMs = (groupSettings.slowmodeCooldown || 30) * 1000; - const result = checkSlowMode(from, sender, cooldownMs); - if (result.onCooldown) { - await sock.sendMessage(from, { - text: `⏳ *Slow mode is active.*\nPlease wait *${result.remainingSecs}* more second${result.remainingSecs === 1 ? '' : 's'} before sending another message.` - }, { quoted: msg }); - return; - } + 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); + if (!senderIsGroupOwner && !senderIsBotOwner) { + 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) {} + }, 3000); + } + + // 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); } } + } catch (e) { + console.error('Error in slow mode check:', e); + } +} // 🔹 Button response should also check unwrapped content const btn = content.buttonsResponseMessage || msg.message?.buttonsResponseMessage; From 030d1eb6e29ab12c159c0790bb6c18fb2bfc04b9 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 15:27:59 +0530 Subject: [PATCH 13/27] Update handler.js --- handler.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/handler.js b/handler.js index 6a30e6e..3cfada6 100644 --- a/handler.js +++ b/handler.js @@ -501,9 +501,10 @@ const handleMessage = async (sock, msg) => { 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); - if (!senderIsGroupOwner && !senderIsBotOwner) { + 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) { @@ -541,7 +542,7 @@ const handleMessage = async (sock, msg) => { } }); } catch (e) {} - }, 3000); + }, 5000); } // 4. OPTIONAL: Try to also delete "this message was deleted" banner if possible (WhatsApp API restriction) From ecffa9face96d5eba05404858c9905b60ed229ce Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 15:33:15 +0530 Subject: [PATCH 14/27] Update handler.js --- handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler.js b/handler.js index 3cfada6..9d56866 100644 --- a/handler.js +++ b/handler.js @@ -542,7 +542,7 @@ if (!senderIsGroupOwner && !senderIsBotOwner && !senderIsAdmin) { } }); } catch (e) {} - }, 5000); + }, 50000); } // 4. OPTIONAL: Try to also delete "this message was deleted" banner if possible (WhatsApp API restriction) From 57713201ee7cc6dbd8b1fb5aa8e792128653c4c8 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 15:49:05 +0530 Subject: [PATCH 15/27] Update handler.js --- handler.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/handler.js b/handler.js index 9d56866..ae3c904 100644 --- a/handler.js +++ b/handler.js @@ -14,8 +14,8 @@ const axios = require('axios'); // Group metadata cache to prevent rate limiting const groupMetadataCache = new Map(); -const CACHE_TTL = 60000; // 1 minute cache -const CACHE_MAX_SIZE = 200; // Maximum number of cached groups +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 = () => { @@ -168,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; From 13c90804ee6d9d967636dd0be5833b8c79cef5a8 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 15:49:33 +0530 Subject: [PATCH 16/27] Update Procfile --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 13e9ecf235adb8f89a579bdd4ed36d9e63601a5d Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 16:09:46 +0530 Subject: [PATCH 17/27] Update index.js --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 576007d..693d91b 100644 --- a/index.js +++ b/index.js @@ -220,7 +220,7 @@ async function startBot() { } const { state, saveCreds } = await useMultiFileAuthState(sessionFolder); - const { version } = await fetchLatestBaileysVersion(); + const version = [2, 3000, 1023456789]; // fixed version, network call nahi hogi // Use suppressed logger for socket const suppressedLogger = createSuppressedLogger('silent'); @@ -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 }; From f7d113220257fac1aad1fbf0f45eee22bc5cd892 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 16:17:39 +0530 Subject: [PATCH 18/27] Add new configuration options for socket connection --- index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 693d91b..e239020 100644 --- a/index.js +++ b/index.js @@ -238,7 +238,12 @@ async function startBot() { markOnlineOnConnect: false, getMessage: async () => undefined // Don't load messages from store }); - + generateHighQualityLinkPreview: false, + patchMessageBeforeSending: (msg) => msg, + transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, + connectTimeoutMs: 20000, + keepAliveIntervalMs: 30000, +}); // Bind store to socket store.bind(sock.ev); From 28ee2bf54cc359204d8f378eb740b5d10dbda951 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 16:26:14 +0530 Subject: [PATCH 19/27] Update index.js --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index e239020..027ac6d 100644 --- a/index.js +++ b/index.js @@ -58,7 +58,6 @@ const { useMultiFileAuthState, DisconnectReason, Browsers, - fetchLatestBaileysVersion } = require('@whiskeysockets/baileys'); const qrcode = require('qrcode-terminal'); const config = require('./config'); @@ -220,7 +219,8 @@ async function startBot() { } const { state, saveCreds } = await useMultiFileAuthState(sessionFolder); - const version = [2, 3000, 1023456789]; // fixed version, network call nahi hogi + const version = [2, 3000, 1015901307]; + // fixed version, network call nahi hogi // Use suppressed logger for socket const suppressedLogger = createSuppressedLogger('silent'); From 40408ebe9c103eef5f601c87868387f886058150 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 16:45:08 +0530 Subject: [PATCH 20/27] Update index.js --- index.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 027ac6d..90c0af5 100644 --- a/index.js +++ b/index.js @@ -226,24 +226,21 @@ async function startBot() { const suppressedLogger = createSuppressedLogger('silent'); const sock = makeWASocket({ - version, // explicit WA Web version negotiated with the server - logger: suppressedLogger, - printQRInTerminal: false, - // Use a common desktop browser signature - browser: ['Chrome', 'Windows', '10.0'], - auth: state, - // Memory optimization: prevent loading old messages into RAM - syncFullHistory: false, - downloadHistory: false, - markOnlineOnConnect: false, - getMessage: async () => undefined // Don't load messages from store - }); + version, // explicit WA Web version negotiated with the server + logger: suppressedLogger, + printQRInTerminal: false, + browser: ['Chrome', 'Windows', '10.0'], + auth: state, + syncFullHistory: false, + downloadHistory: false, + markOnlineOnConnect: false, + getMessage: async () => undefined, // Don't load messages from store generateHighQualityLinkPreview: false, patchMessageBeforeSending: (msg) => msg, transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, connectTimeoutMs: 20000, keepAliveIntervalMs: 30000, -}); + }); // Bind store to socket store.bind(sock.ev); From badfbcbea88ec8ff794683e9088baf55ad1cbe49 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Mon, 23 Mar 2026 19:16:23 +0530 Subject: [PATCH 21/27] Update index.js --- index.js | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index 90c0af5..bef35ca 100644 --- a/index.js +++ b/index.js @@ -58,6 +58,7 @@ const { useMultiFileAuthState, DisconnectReason, Browsers, + fetchLatestBaileysVersion } = require('@whiskeysockets/baileys'); const qrcode = require('qrcode-terminal'); const config = require('./config'); @@ -85,7 +86,7 @@ function cleanupPuppeteerCache() { // Optimized in-memory store with hard limits (Map-based for better memory management) const store = { messages: new Map(), // Use Map instead of plain object - maxPerChat: 10, // Limit to 10 messages per chat + maxPerChat: 20, // Limit to 20 messages per chat bind: (ev) => { ev.on('messages.upsert', ({ messages }) => { @@ -219,28 +220,25 @@ async function startBot() { } const { state, saveCreds } = await useMultiFileAuthState(sessionFolder); - const version = [2, 3000, 1015901307]; - // fixed version, network call nahi hogi + const { version } = await fetchLatestBaileysVersion(); // Use suppressed logger for socket const suppressedLogger = createSuppressedLogger('silent'); const sock = makeWASocket({ - version, // explicit WA Web version negotiated with the server - logger: suppressedLogger, - printQRInTerminal: false, - browser: ['Chrome', 'Windows', '10.0'], - auth: state, - syncFullHistory: false, - downloadHistory: false, - markOnlineOnConnect: false, - getMessage: async () => undefined, // Don't load messages from store - generateHighQualityLinkPreview: false, - patchMessageBeforeSending: (msg) => msg, - transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, - connectTimeoutMs: 20000, - keepAliveIntervalMs: 30000, - }); + version, // explicit WA Web version negotiated with the server + logger: suppressedLogger, + printQRInTerminal: false, + // Use a common desktop browser signature + browser: ['Chrome', 'Windows', '10.0'], + auth: state, + // Memory optimization: prevent loading old messages into RAM + syncFullHistory: false, + downloadHistory: false, + markOnlineOnConnect: false, + getMessage: async () => undefined // Don't load messages from store + }); + // Bind store to socket store.bind(sock.ev); @@ -385,7 +383,7 @@ async function startBot() { const chatMsgs = store.messages.get(from); chatMsgs.set(msg.key.id, msg); - // Cleanup: Keep only last 10 per chat (reduced from 20) + // Cleanup: Keep only last 20 per chat (reduced from 200) if (chatMsgs.size > store.maxPerChat) { // Remove oldest messages const sortedIds = Array.from(chatMsgs.entries()) From b7f69257af129d76312b8223075d187eb113dc8a Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Tue, 24 Mar 2026 00:05:08 +0530 Subject: [PATCH 22/27] Add scripts to create sudo user and wrapper + sudoers fragment --- scripts/add-sudo-user.sh | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 scripts/add-sudo-user.sh 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 From 026bf4d36f4f241ecd42ece72dda94d0e85c548d Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Tue, 24 Mar 2026 00:10:57 +0530 Subject: [PATCH 23/27] Add interactive quiz command and handler support --- commands/fun/quiz.js | 100 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 commands/fun/quiz.js diff --git a/commands/fun/quiz.js b/commands/fun/quiz.js new file mode 100644 index 0000000..a0f0488 --- /dev/null +++ b/commands/fun/quiz.js @@ -0,0 +1,100 @@ +// commands/fun/quiz.js +// Simple interactive quiz command using WhatsApp List messages. +// Usage: .quiz (sends a sample 4-option quiz to the chat) + +const activeQuizzes = new Map(); // key: `${chatJid}:${qid}` => { correct, question, options } + +module.exports = { + name: 'quiz', + description: 'Sends an interactive quiz (list) and handles answers', + ownerOnly: false, + groupOnly: false, + + // Called when user runs the command: .quiz + execute: async (sock, msg, args, ctx) => { + const from = ctx.from; + const qid = Date.now().toString(); + + // Example question (you can later modify to accept custom questions) + const question = 'In which case acceleration is zero but velocity is not zero?'; + const options = [ + { id: 'A', title: 'A) Free fall' }, + { id: 'B', title: 'B) Uniform motion' }, + { id: 'C', title: 'C) Circular motion' }, + { id: 'D', title: 'D) Projectile motion' } + ]; + + const correct = 'B'; // Uniform motion + + // Store active quiz state (keeps multiple quizzes per chat if needed) + activeQuizzes.set(`${from}:${qid}`, { correct, question, options, askedBy: ctx.sender, createdAt: Date.now() }); + + // Build list message sections/rows + const rows = options.map(opt => ({ id: `quiz_${qid}_${opt.id}`, title: opt.title })); + const sections = [{ title: 'Options', rows }]; + + const listMessage = { + interactive: { + type: 'list', + header: { type: 'text', text: 'Question (JEE Main 2023)' }, + body: { text: question }, + footer: { text: 'Anonymous Quiz' }, + action: { + button: 'Choose option', + sections + } + } + }; + + try { + await sock.sendMessage(from, listMessage); + } catch (err) { + console.error('[quiz.execute] Failed to send list message:', err); + await sock.sendMessage(from, { text: 'Failed to send quiz. Try again later.' }); + } + }, + + // Called by handler when a list selection arrives + handleSelection: async (sock, msg, selectedRowId) => { + try { + // selectedRowId format: quiz__ + const m = String(selectedRowId).match(/^quiz_(\d+)_(A|B|C|D)$/); + if (!m) return; // Not a quiz selection + + const qid = m[1]; + const choice = m[2]; + const chat = msg.key.remoteJid; + const key = `${chat}:${qid}`; + const state = activeQuizzes.get(key); + + if (!state) { + await sock.sendMessage(chat, { text: 'This quiz has expired or is not recognized.' }, { quoted: msg }); + return; + } + + const correct = state.correct; + const optionText = state.options.find(o => o.id === choice)?.title || choice; + const correctText = state.options.find(o => o.id === correct)?.title || correct; + + let reply; + if (choice === correct) { + reply = `✅ Correct!\nYou selected ${optionText}`; + } else { + reply = `❌ Wrong.\nYou selected ${optionText}\nCorrect answer: ${correctText}`; + } + + // Reply to the user who selected (quote their selection message) + await sock.sendMessage(chat, { text: reply }, { quoted: msg }); + + // Optionally keep quiz active to let others answer. If you want one-answer-per-user + // or disable after first answer, uncomment the following line to remove active quiz: + // activeQuizzes.delete(key); + + } catch (err) { + console.error('[quiz.handleSelection] Error handling selection:', err); + } + }, + + // expose map for debugging/testing if needed + __activeQuizzes: activeQuizzes +}; \ No newline at end of file From 4b59bafd3ba5e218e686c6244527a95d1e427b62 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Tue, 24 Mar 2026 00:29:47 +0530 Subject: [PATCH 24/27] Update quiz.js --- commands/fun/quiz.js | 242 ++++++++++++++++++++++++++++++------------- 1 file changed, 171 insertions(+), 71 deletions(-) diff --git a/commands/fun/quiz.js b/commands/fun/quiz.js index a0f0488..a6b9a2e 100644 --- a/commands/fun/quiz.js +++ b/commands/fun/quiz.js @@ -1,100 +1,200 @@ // commands/fun/quiz.js -// Simple interactive quiz command using WhatsApp List messages. -// Usage: .quiz (sends a sample 4-option quiz to the chat) +// Interactive JEE quiz with no-repeat until pool exhausted. +// Supports: inline question, random JEE bank, auto mode. +const fs = require('fs'); +const path = require('path'); + +const activeQuizzes = new Map(); // `${chat}:${qid}` => { correct, question, options, createdAt } +const autoQuizzes = new Map(); // `${chat}` => { intervalId, createdBy, intervalMs } +const usedQuestions = new Map(); // `${chat}` => Set of used indices to prevent repeats +const BANK_FILE = path.join(__dirname, '../../data/quiz_bank.json'); + +function ensureBankFile() { + const dir = path.dirname(BANK_FILE); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + if (!fs.existsSync(BANK_FILE)) fs.writeFileSync(BANK_FILE, '[]', 'utf8'); +} + +function loadBank() { + try { + ensureBankFile(); + const raw = fs.readFileSync(BANK_FILE, 'utf8').trim() || '[]'; + const arr = JSON.parse(raw); + return Array.isArray(arr) ? arr : []; + } catch (e) { + console.error('[quiz.loadBank]', e.message || e); + return []; + } +} + +function normalizeOptionsArray(options) { + return options.map((o, idx) => ({ id: String.fromCharCode(65 + idx), title: `${String.fromCharCode(65 + idx)}) ${o}` })); +} + +async function sendQuiz(sock, chat, questionObj, quotedMsg) { + try { + const qid = Date.now().toString(); + activeQuizzes.set(`${chat}:${qid}`, { correct: questionObj.correct, question: questionObj.question, options: questionObj.options, askedBy: null, createdAt: Date.now() }); + + const rows = questionObj.options.map(opt => ({ id: `quiz_${qid}_${opt.id}`, title: opt.title })); + const sections = [{ title: 'Options', rows }]; + + const listMessage = { + interactive: { + type: 'list', + header: { type: 'text', text: 'Quiz (JEE-level)' }, + body: { text: questionObj.question }, + footer: { text: 'Select the correct option' }, + action: { button: 'Choose option', sections } + } + }; -const activeQuizzes = new Map(); // key: `${chatJid}:${qid}` => { correct, question, options } + await sock.sendMessage(chat, listMessage, quotedMsg ? { quoted: quotedMsg } : {}); + return qid; + } catch (err) { + console.error('[quiz.sendQuiz] Error sending quiz:', err); + try { await sock.sendMessage(chat, { text: '⚠️ Failed to send quiz. Please try again later.' }); } catch (e) {} + return null; + } +} module.exports = { name: 'quiz', - description: 'Sends an interactive quiz (list) and handles answers', + description: 'Sends an interactive JEE quiz with no repeats until pool exhausted. Supports auto mode and inline Qs.', ownerOnly: false, groupOnly: false, - // Called when user runs the command: .quiz execute: async (sock, msg, args, ctx) => { - const from = ctx.from; - const qid = Date.now().toString(); - - // Example question (you can later modify to accept custom questions) - const question = 'In which case acceleration is zero but velocity is not zero?'; - const options = [ - { id: 'A', title: 'A) Free fall' }, - { id: 'B', title: 'B) Uniform motion' }, - { id: 'C', title: 'C) Circular motion' }, - { id: 'D', title: 'D) Projectile motion' } - ]; - - const correct = 'B'; // Uniform motion - - // Store active quiz state (keeps multiple quizzes per chat if needed) - activeQuizzes.set(`${from}:${qid}`, { correct, question, options, askedBy: ctx.sender, createdAt: Date.now() }); - - // Build list message sections/rows - const rows = options.map(opt => ({ id: `quiz_${qid}_${opt.id}`, title: opt.title })); - const sections = [{ title: 'Options', rows }]; + const from = ctx.from || msg.key.remoteJid; + const quoted = msg; + + // Auto mode: .quiz auto start | .quiz auto stop + if (args[0] && args[0].toLowerCase() === 'auto') { + const sub = (args[1] || '').toLowerCase(); + if (sub === 'start') { + const allowed = ctx.isOwner || ctx.isAdmin; + if (!allowed) return sock.sendMessage(from, { text: '🔒 Only owner or admins can start auto-quiz.' }, { quoted }); + const seconds = Math.max(30, parseInt(args[2], 10) || 3600); + const intervalMs = seconds * 1000; + if (autoQuizzes.has(from)) return sock.sendMessage(from, { text: 'Auto-quiz already running.' }, { quoted }); + + const intervalId = setInterval(async () => { + try { + const bank = loadBank(); + if (!bank.length) return; + // prefer JEE-level + const jeePool = bank.map((q, i) => ({ q, i })).filter(x => (x.q.level || '').toLowerCase() === 'jee' || (x.q.tags || []).map(t => t.toLowerCase()).includes('jee')); + const pool = jeePool.length ? jeePool : bank.map((q, i) => ({ q, i })); + + // Use usedQuestions to avoid repeats + if (!usedQuestions.has(from)) usedQuestions.set(from, new Set()); + const usedSet = usedQuestions.get(from); + + // build available list of indices excluding used + let available = pool.filter(x => !usedSet.has(x.i)); + if (!available.length) { + // reset used set if exhausted + usedSet.clear(); + available = pool; + } + + const pick = available[Math.floor(Math.random() * available.length)]; + if (!pick) return; + + const pickedQuestion = pick.q; + const idx = pick.i; + usedSet.add(idx); + + const opts = normalizeOptionsArray(pickedQuestion.options || []); + const questionObj = { question: pickedQuestion.question || 'Untitled', options: opts, correct: (pickedQuestion.correct || 'A').toUpperCase() }; + await sendQuiz(sock, from, questionObj, null); + } catch (e) { + console.error('[quiz.autoInterval] Error:', e); + } + }, intervalMs); + + autoQuizzes.set(from, { intervalId, createdBy: ctx.sender, intervalMs }); + return sock.sendMessage(from, { text: `✅ Auto-quiz started every ${seconds}s.` }, { quoted }); + } else if (sub === 'stop') { + const entry = autoQuizzes.get(from); + const allowed = ctx.isOwner || ctx.isAdmin || (entry && entry.createdBy === ctx.sender); + if (!allowed) return sock.sendMessage(from, { text: '🔒 Not allowed to stop auto-quiz.' }, { quoted }); + if (!entry) return sock.sendMessage(from, { text: 'Auto-quiz not running.' }, { quoted }); + clearInterval(entry.intervalId); + autoQuizzes.delete(from); + return sock.sendMessage(from, { text: '⛔ Auto-quiz stopped.' }, { quoted }); + } else { + return sock.sendMessage(from, { text: 'Usage: .quiz auto start | .quiz auto stop' }, { quoted }); + } + } - const listMessage = { - interactive: { - type: 'list', - header: { type: 'text', text: 'Question (JEE Main 2023)' }, - body: { text: question }, - footer: { text: 'Anonymous Quiz' }, - action: { - button: 'Choose option', - sections + // Inline question + const joined = args.join(' ').trim(); + let questionObj = null; + if (joined) { + const parts = joined.split('|').map(p => p.trim()).filter(p => p); + if (parts.length >= 3) { + const last = parts[parts.length - 1].toUpperCase(); + if (/^[A-Z]$/.test(last)) { + const correct = last; + const opts = parts.slice(1, parts.length - 1); + questionObj = { question: parts[0], options: normalizeOptionsArray(opts), correct }; + } else { + const opts = parts.slice(1); + questionObj = { question: parts[0], options: normalizeOptionsArray(opts), correct: 'A' }; } + } else { + return sock.sendMessage(from, { text: 'Usage: .quiz Q | optA | optB | ... | ' }, { quoted }); } - }; + } - try { - await sock.sendMessage(from, listMessage); - } catch (err) { - console.error('[quiz.execute] Failed to send list message:', err); - await sock.sendMessage(from, { text: 'Failed to send quiz. Try again later.' }); + // pick from bank (no repeats) + if (!questionObj) { + const bank = loadBank(); + if (!bank.length) { + questionObj = { question: 'In which case acceleration is zero but velocity is not zero?', options: normalizeOptionsArray(['Free fall','Uniform motion','Circular motion','Projectile motion']), correct: 'B' }; + } else { + const pool = bank.map((q, i) => ({ q, i })).filter(x => (x.q.level || '').toLowerCase() === 'jee' || (x.q.tags || []).map(t => t.toLowerCase()).includes('jee')); + const fallbackPool = bank.map((q, i) => ({ q, i })); + const chosenPool = pool.length ? pool : fallbackPool; + + if (!usedQuestions.has(from)) usedQuestions.set(from, new Set()); + const usedSet = usedQuestions.get(from); + let available = chosenPool.filter(x => !usedSet.has(x.i)); + if (!available.length) { usedSet.clear(); available = chosenPool; } + + const pick = available[Math.floor(Math.random() * available.length)]; + if (!pick) { + const first = chosenPool[0]; + questionObj = { question: first.q.question || 'Untitled', options: normalizeOptionsArray(first.q.options || []), correct: (first.q.correct || 'A').toUpperCase() }; + } else { + usedSet.add(pick.i); + questionObj = { question: pick.q.question || 'Untitled', options: normalizeOptionsArray(pick.q.options || []), correct: (pick.q.correct || 'A').toUpperCase() }; + } + } } + + await sendQuiz(sock, from, questionObj, quoted); }, - // Called by handler when a list selection arrives handleSelection: async (sock, msg, selectedRowId) => { try { - // selectedRowId format: quiz__ - const m = String(selectedRowId).match(/^quiz_(\d+)_(A|B|C|D)$/); - if (!m) return; // Not a quiz selection - + const m = String(selectedRowId).match(/^quiz_(\d+)_(A|B|C|D|E|F)$/); + if (!m) return; const qid = m[1]; const choice = m[2]; const chat = msg.key.remoteJid; const key = `${chat}:${qid}`; const state = activeQuizzes.get(key); - - if (!state) { - await sock.sendMessage(chat, { text: 'This quiz has expired or is not recognized.' }, { quoted: msg }); - return; - } - + if (!state) { await sock.sendMessage(chat, { text: 'This quiz has expired.' }, { quoted: msg }); return; } const correct = state.correct; const optionText = state.options.find(o => o.id === choice)?.title || choice; const correctText = state.options.find(o => o.id === correct)?.title || correct; - - let reply; - if (choice === correct) { - reply = `✅ Correct!\nYou selected ${optionText}`; - } else { - reply = `❌ Wrong.\nYou selected ${optionText}\nCorrect answer: ${correctText}`; - } - - // Reply to the user who selected (quote their selection message) + const reply = choice === correct ? `✅ Correct!\nYou selected ${optionText}` : `❌ Wrong.\nYou selected ${optionText}\nCorrect answer: ${correctText}`; await sock.sendMessage(chat, { text: reply }, { quoted: msg }); - - // Optionally keep quiz active to let others answer. If you want one-answer-per-user - // or disable after first answer, uncomment the following line to remove active quiz: - // activeQuizzes.delete(key); - - } catch (err) { - console.error('[quiz.handleSelection] Error handling selection:', err); - } + } catch (err) { console.error('[quiz.handleSelection]', err); } }, - // expose map for debugging/testing if needed - __activeQuizzes: activeQuizzes -}; \ No newline at end of file + __activeQuizzes: activeQuizzes, + __autoQuizzes: autoQuizzes +}; From 13a580df624cf73106e543d9b3de813cb3cba293 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Tue, 24 Mar 2026 00:31:54 +0530 Subject: [PATCH 25/27] Create quiz_bank.json --- database/quiz_bank.json | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 database/quiz_bank.json 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"]} +] From 1cfd5648a24e7772edb02c966587ea4b4b72448f Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Tue, 24 Mar 2026 00:41:57 +0530 Subject: [PATCH 26/27] Update handler.js --- handler.js | 54 +++++++++++++++++++++--------------------------------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/handler.js b/handler.js index ae3c904..8cac557 100644 --- a/handler.js +++ b/handler.js @@ -506,9 +506,9 @@ const handleMessage = async (sock, msg) => { 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 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) { @@ -624,7 +624,20 @@ if (!senderIsGroupOwner && !senderIsBotOwner && !senderIsAdmin) { 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) { @@ -996,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')) { @@ -1288,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); @@ -1324,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 From 1a162b9db553a3ccd6675e82031cf05f4e4c0778 Mon Sep 17 00:00:00 2001 From: Adi Putra Date: Tue, 24 Mar 2026 01:05:33 +0530 Subject: [PATCH 27/27] Enhance quiz feature to include images and MIME types Updated quiz functionality to send quizzes with images and proper MIME types. Refactored code for better organization and error handling. --- commands/fun/quiz.js | 228 ++++++++----------------------------------- 1 file changed, 38 insertions(+), 190 deletions(-) diff --git a/commands/fun/quiz.js b/commands/fun/quiz.js index a6b9a2e..4452a90 100644 --- a/commands/fun/quiz.js +++ b/commands/fun/quiz.js @@ -1,200 +1,48 @@ -// commands/fun/quiz.js -// Interactive JEE quiz with no-repeat until pool exhausted. -// Supports: inline question, random JEE bank, auto mode. const fs = require('fs'); -const path = require('path'); +const { prepareWAMessageMedia } = require('@whiskeysockets/baileys'); -const activeQuizzes = new Map(); // `${chat}:${qid}` => { correct, question, options, createdAt } -const autoQuizzes = new Map(); // `${chat}` => { intervalId, createdBy, intervalMs } -const usedQuestions = new Map(); // `${chat}` => Set of used indices to prevent repeats -const BANK_FILE = path.join(__dirname, '../../data/quiz_bank.json'); - -function ensureBankFile() { - const dir = path.dirname(BANK_FILE); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - if (!fs.existsSync(BANK_FILE)) fs.writeFileSync(BANK_FILE, '[]', 'utf8'); -} - -function loadBank() { - try { - ensureBankFile(); - const raw = fs.readFileSync(BANK_FILE, 'utf8').trim() || '[]'; - const arr = JSON.parse(raw); - return Array.isArray(arr) ? arr : []; - } catch (e) { - console.error('[quiz.loadBank]', e.message || e); - return []; - } -} - -function normalizeOptionsArray(options) { - return options.map((o, idx) => ({ id: String.fromCharCode(65 + idx), title: `${String.fromCharCode(65 + idx)}) ${o}` })); -} - -async function sendQuiz(sock, chat, questionObj, quotedMsg) { +async function sendQuiz(sock, jid) { try { - const qid = Date.now().toString(); - activeQuizzes.set(`${chat}:${qid}`, { correct: questionObj.correct, question: questionObj.question, options: questionObj.options, askedBy: null, createdAt: Date.now() }); - - const rows = questionObj.options.map(opt => ({ id: `quiz_${qid}_${opt.id}`, title: opt.title })); - const sections = [{ title: 'Options', rows }]; - - const listMessage = { - interactive: { - type: 'list', - header: { type: 'text', text: 'Quiz (JEE-level)' }, - body: { text: questionObj.question }, - footer: { text: 'Select the correct option' }, - action: { button: 'Choose option', sections } - } - }; - - await sock.sendMessage(chat, listMessage, quotedMsg ? { quoted: quotedMsg } : {}); - return qid; + // --- 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.sendQuiz] Error sending quiz:', err); - try { await sock.sendMessage(chat, { text: '⚠️ Failed to send quiz. Please try again later.' }); } catch (e) {} - return null; + 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: 'Sends an interactive JEE quiz with no repeats until pool exhausted. Supports auto mode and inline Qs.', - ownerOnly: false, - groupOnly: false, - - execute: async (sock, msg, args, ctx) => { - const from = ctx.from || msg.key.remoteJid; - const quoted = msg; - - // Auto mode: .quiz auto start | .quiz auto stop - if (args[0] && args[0].toLowerCase() === 'auto') { - const sub = (args[1] || '').toLowerCase(); - if (sub === 'start') { - const allowed = ctx.isOwner || ctx.isAdmin; - if (!allowed) return sock.sendMessage(from, { text: '🔒 Only owner or admins can start auto-quiz.' }, { quoted }); - const seconds = Math.max(30, parseInt(args[2], 10) || 3600); - const intervalMs = seconds * 1000; - if (autoQuizzes.has(from)) return sock.sendMessage(from, { text: 'Auto-quiz already running.' }, { quoted }); - - const intervalId = setInterval(async () => { - try { - const bank = loadBank(); - if (!bank.length) return; - // prefer JEE-level - const jeePool = bank.map((q, i) => ({ q, i })).filter(x => (x.q.level || '').toLowerCase() === 'jee' || (x.q.tags || []).map(t => t.toLowerCase()).includes('jee')); - const pool = jeePool.length ? jeePool : bank.map((q, i) => ({ q, i })); - - // Use usedQuestions to avoid repeats - if (!usedQuestions.has(from)) usedQuestions.set(from, new Set()); - const usedSet = usedQuestions.get(from); - - // build available list of indices excluding used - let available = pool.filter(x => !usedSet.has(x.i)); - if (!available.length) { - // reset used set if exhausted - usedSet.clear(); - available = pool; - } - - const pick = available[Math.floor(Math.random() * available.length)]; - if (!pick) return; - - const pickedQuestion = pick.q; - const idx = pick.i; - usedSet.add(idx); - - const opts = normalizeOptionsArray(pickedQuestion.options || []); - const questionObj = { question: pickedQuestion.question || 'Untitled', options: opts, correct: (pickedQuestion.correct || 'A').toUpperCase() }; - await sendQuiz(sock, from, questionObj, null); - } catch (e) { - console.error('[quiz.autoInterval] Error:', e); - } - }, intervalMs); - - autoQuizzes.set(from, { intervalId, createdBy: ctx.sender, intervalMs }); - return sock.sendMessage(from, { text: `✅ Auto-quiz started every ${seconds}s.` }, { quoted }); - } else if (sub === 'stop') { - const entry = autoQuizzes.get(from); - const allowed = ctx.isOwner || ctx.isAdmin || (entry && entry.createdBy === ctx.sender); - if (!allowed) return sock.sendMessage(from, { text: '🔒 Not allowed to stop auto-quiz.' }, { quoted }); - if (!entry) return sock.sendMessage(from, { text: 'Auto-quiz not running.' }, { quoted }); - clearInterval(entry.intervalId); - autoQuizzes.delete(from); - return sock.sendMessage(from, { text: '⛔ Auto-quiz stopped.' }, { quoted }); - } else { - return sock.sendMessage(from, { text: 'Usage: .quiz auto start | .quiz auto stop' }, { quoted }); - } - } - - // Inline question - const joined = args.join(' ').trim(); - let questionObj = null; - if (joined) { - const parts = joined.split('|').map(p => p.trim()).filter(p => p); - if (parts.length >= 3) { - const last = parts[parts.length - 1].toUpperCase(); - if (/^[A-Z]$/.test(last)) { - const correct = last; - const opts = parts.slice(1, parts.length - 1); - questionObj = { question: parts[0], options: normalizeOptionsArray(opts), correct }; - } else { - const opts = parts.slice(1); - questionObj = { question: parts[0], options: normalizeOptionsArray(opts), correct: 'A' }; - } - } else { - return sock.sendMessage(from, { text: 'Usage: .quiz Q | optA | optB | ... | ' }, { quoted }); - } - } - - // pick from bank (no repeats) - if (!questionObj) { - const bank = loadBank(); - if (!bank.length) { - questionObj = { question: 'In which case acceleration is zero but velocity is not zero?', options: normalizeOptionsArray(['Free fall','Uniform motion','Circular motion','Projectile motion']), correct: 'B' }; - } else { - const pool = bank.map((q, i) => ({ q, i })).filter(x => (x.q.level || '').toLowerCase() === 'jee' || (x.q.tags || []).map(t => t.toLowerCase()).includes('jee')); - const fallbackPool = bank.map((q, i) => ({ q, i })); - const chosenPool = pool.length ? pool : fallbackPool; - - if (!usedQuestions.has(from)) usedQuestions.set(from, new Set()); - const usedSet = usedQuestions.get(from); - let available = chosenPool.filter(x => !usedSet.has(x.i)); - if (!available.length) { usedSet.clear(); available = chosenPool; } - - const pick = available[Math.floor(Math.random() * available.length)]; - if (!pick) { - const first = chosenPool[0]; - questionObj = { question: first.q.question || 'Untitled', options: normalizeOptionsArray(first.q.options || []), correct: (first.q.correct || 'A').toUpperCase() }; - } else { - usedSet.add(pick.i); - questionObj = { question: pick.q.question || 'Untitled', options: normalizeOptionsArray(pick.q.options || []), correct: (pick.q.correct || 'A').toUpperCase() }; - } - } - } - - await sendQuiz(sock, from, questionObj, quoted); - }, - - handleSelection: async (sock, msg, selectedRowId) => { - try { - const m = String(selectedRowId).match(/^quiz_(\d+)_(A|B|C|D|E|F)$/); - if (!m) return; - const qid = m[1]; - const choice = m[2]; - const chat = msg.key.remoteJid; - const key = `${chat}:${qid}`; - const state = activeQuizzes.get(key); - if (!state) { await sock.sendMessage(chat, { text: 'This quiz has expired.' }, { quoted: msg }); return; } - const correct = state.correct; - const optionText = state.options.find(o => o.id === choice)?.title || choice; - const correctText = state.options.find(o => o.id === correct)?.title || correct; - const reply = choice === correct ? `✅ Correct!\nYou selected ${optionText}` : `❌ Wrong.\nYou selected ${optionText}\nCorrect answer: ${correctText}`; - await sock.sendMessage(chat, { text: reply }, { quoted: msg }); - } catch (err) { console.error('[quiz.handleSelection]', err); } - }, - - __activeQuizzes: activeQuizzes, - __autoQuizzes: autoQuizzes + description: 'Send a quiz with image and proper MIME type', + async execute(sock, msg) { + const jid = msg.key.remoteJid; + await sendQuiz(sock, jid); + } };