understanding commited on
Commit
14960b8
·
verified ·
1 Parent(s): 03229e8

Upload 13 files

Browse files
.env ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # .env
2
+ MONGODB_URI=mongodb+srv://loda:[email protected]/?retryWrites=true&w=majority&appName=userdata
3
+ # List Admin JIDs separated by commas ONLY (no spaces around commas)
4
5
+ LOG_LEVEL=info # Optional: set to 'debug' for more verbose logs, 'info' for standard
6
+
7
+
package.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "whatsapp-baileys-bot",
3
+ "version": "1.0.0",
4
+ "description": "WhatsApp bot using Baileys library",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js",
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "dependencies": {
11
+ "@whiskeysockets/baileys": "^6.7.16",
12
+ "dotenv": "^16.4.7",
13
+ "mongodb": "^6.15.0",
14
+ "mongoose": "^8.13.2",
15
+ "pino": "^8.21.0",
16
+ "qrcode-terminal": "^0.12.0"
17
+ },
18
+ "engines": {
19
+ "node": ">=20.0.0"
20
+ },
21
+ "author": "",
22
+ "license": "ISC",
23
+ "devDependencies": {
24
+ "pino-pretty": "^13.0.0"
25
+ }
26
+ }
src/config.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/config.js
2
+ require('dotenv').config();
3
+
4
+ const adminJidsString = process.env.ADMIN_JIDS || '';
5
+ const adminJids = adminJidsString.split(',')
6
+ .map(jid => jid.trim())
7
+ .filter(jid => jid);
8
+
9
+ module.exports = {
10
+ mongodbUri: process.env.MONGODB_URI,
11
+ logLevel: process.env.LOG_LEVEL || 'info',
12
+ adminJids: adminJids, // Exported as an array
13
+ // Default delays (can be overridden)
14
+ defaultMinDelay: 500,
15
+ defaultMaxDelay: 1500,
16
+ // Specific Delays
17
+ registrationMinDelay: 2000,
18
+ registrationMaxDelay: 3000,
19
+ attendanceMinDelay: 10000,
20
+ attendanceMaxDelay: 15000,
21
+ };
22
+
src/connection.js ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/connection.js
2
+ const makeWASocket = require('@whiskeysockets/baileys').default;
3
+ const { DisconnectReason, useMultiFileAuthState, fetchLatestBaileysVersion } = require('@whiskeysockets/baileys');
4
+ const logger = require('./logger');
5
+ const config = require('./config'); // Import config but botJid setting/clearing removed
6
+ const whatsapp = require('./services/whatsappService'); // Import service
7
+ const { processMessage } = require('./handlers/messageHandler'); // Import main handler
8
+ const path = require('path'); // Path is still needed for useMultiFileAuthState path construction
9
+
10
+ /**
11
+ * Establishes the WhatsApp connection using Baileys, initializes services,
12
+ * and sets up event listeners. Filters groups and channels. (Original Refactored Version)
13
+ */
14
+ async function connectWhatsApp() {
15
+ // Define auth folder path relative to src/ directory
16
+ const authFolderPath = path.join(__dirname, '..', 'auth');
17
+ const { state, saveCreds } = await useMultiFileAuthState(authFolderPath);
18
+ const { version } = await fetchLatestBaileysVersion();
19
+
20
+ logger.info(`Using Baileys version: ${version.join('.')}`);
21
+
22
+ const sock = makeWASocket({
23
+ printQRInTerminal: true,
24
+ auth: state,
25
+ version: version,
26
+ // getMessage: async key => { }, // Define if using message store features
27
+ logger // Pass pino logger to Baileys
28
+ });
29
+
30
+ // Initialize WhatsApp Service with sock instance
31
+ whatsapp.initialize(sock);
32
+
33
+ // Baileys Event Processing
34
+ sock.ev.process(async (events) => {
35
+
36
+ // ** Connection Logic **
37
+ if (events['connection.update']) {
38
+ // (Connection logic remains the same as previous - without auto-delete/auto-restart)
39
+ const { connection, lastDisconnect, qr } = events['connection.update'];
40
+ const statusCode = (lastDisconnect?.error)?.output?.statusCode;
41
+
42
+ if (connection === 'close') {
43
+ config.botJid = null; // Clear bot JID on close
44
+ const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
45
+ logger.warn({ err: lastDisconnect?.error, shouldReconnect }, `Connection closed. Status Code: ${statusCode}`);
46
+ if (shouldReconnect) {
47
+ logger.info("Attempting to reconnect in 5 seconds...");
48
+ setTimeout(connectWhatsApp, 5000);
49
+ } else {
50
+ logger.error('Connection closed: Logged Out or Invalid Session. Please delete "auth" folder manually and restart.');
51
+ process.exit(1);
52
+ }
53
+ } else if (connection === 'open') {
54
+ config.botJid = sock.user?.id;
55
+ logger.info({ botJid: config.botJid }, 'WhatsApp connection opened and service initialized.');
56
+ whatsapp.initialize(sock); // Re-initialize service if needed
57
+ }
58
+ if(qr) {
59
+ logger.info('QR code received, scan please!');
60
+ }
61
+ }
62
+
63
+ // ** Credentials Update **
64
+ if (events['creds.update']) {
65
+ await saveCreds();
66
+ }
67
+
68
+ // ** Message Handling **
69
+ if (events['messages.upsert']) {
70
+ const upsert = events['messages.upsert'];
71
+ // logger.trace({ upsert }, 'Received messages.upsert event');
72
+
73
+ if (upsert.type === 'notify') {
74
+ for (const msg of upsert.messages) {
75
+ // --- Basic Message Filtering ---
76
+ // Ignore messages without content, from self, or status broadcasts
77
+ if (!msg.message || msg.key.fromMe || msg.key.remoteJid === 'status@broadcast') {
78
+ // logger.trace({ msgId: msg.key.id }, 'Ignoring message (no content, fromMe, or status broadcast)');
79
+ continue;
80
+ }
81
+ // Ignore group messages
82
+ if (msg.key.remoteJid.endsWith('@g.us')) {
83
+ // logger.trace({ msgId: msg.key.id, group: msg.key.remoteJid }, 'Ignoring group message');
84
+ continue;
85
+ }
86
+ // *** ADDED: Ignore channel messages ***
87
+ if (msg.key.remoteJid.endsWith('@newsletter')) {
88
+ logger.trace({ msgId: msg.key.id, channel: msg.key.remoteJid }, 'Ignoring channel message');
89
+ continue;
90
+ }
91
+ // *** END ADDED FILTER ***
92
+
93
+ // Delegate processing asynchronously for valid user messages
94
+ setImmediate(() => {
95
+ processMessage(msg).catch(err => {
96
+ logger.error({ err, msgId: msg?.key?.id }, "Error caught in setImmediate for processMessage");
97
+ });
98
+ });
99
+
100
+ } // end for loop
101
+ } // end if notify
102
+ } // end messages.upsert
103
+
104
+ }); // End sock.ev.process
105
+
106
+ return sock;
107
+ } // End connectWhatsApp
108
+
109
+ module.exports = { connectWhatsApp };
110
+
src/database.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/database.js
2
+ const mongoose = require('mongoose');
3
+ const logger = require('./logger');
4
+ const config = require('./config');
5
+
6
+ if (!config.mongodbUri) {
7
+ logger.fatal("FATAL: MONGODB_URI not found in environment variables/config. Check .env file.");
8
+ process.exit(1);
9
+ }
10
+
11
+ // Schema remains the same
12
+ const userSchema = new mongoose.Schema({
13
+ remoteJid: { type: String, required: true, unique: true },
14
+ name: { type: String },
15
+ regNo: { type: String },
16
+ semester: { type: String },
17
+ isRegistered: { type: Boolean, default: false },
18
+ registrationTimestamp: { type: Date }
19
+ });
20
+
21
+ // Comment on Data Size
22
+ // Data per user is small (~<1KB). 2000 users (~2MB) is well within Atlas free tier limits.
23
+
24
+ const User = mongoose.model('User', userSchema);
25
+
26
+ async function connectDB() {
27
+ try {
28
+ await mongoose.connect(config.mongodbUri);
29
+ logger.info('Successfully connected to MongoDB!');
30
+ } catch (error) {
31
+ logger.fatal({ err: error }, 'Error connecting to MongoDB');
32
+ process.exit(1);
33
+ }
34
+ }
35
+
36
+ // Export model directly for handlers to import
37
+ module.exports = { connectDB, User };
38
+
src/handlers/adminHandler.js ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/handlers/adminHandler.js
2
+ const logger = require('../logger');
3
+ const whatsapp = require('../services/whatsappService');
4
+ const { User } = require('../database');
5
+ const config = require('../config');
6
+
7
+ // State to track pending admin actions requiring confirmation
8
+ // Key: adminJid, Value: { action: 'confirm_clear', semester: '...', timeoutId: ... }
9
+ const adminState = {};
10
+ const CONFIRMATION_TIMEOUT = 60000; // 60 seconds
11
+
12
+ /**
13
+ * Clears the pending state for an admin.
14
+ * @param {string} jid - Admin JID.
15
+ */
16
+ function clearAdminState(jid) {
17
+ if (adminState[jid]) {
18
+ clearTimeout(adminState[jid].timeoutId); // Clear the timeout
19
+ delete adminState[jid];
20
+ logger.info({ adminJid: jid }, `[AdminHandler] Cleared pending admin state.`);
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Handles commands sent by recognized admins, including confirmation steps.
26
+ * Returns true if an admin command (or confirmation) was processed, false otherwise.
27
+ */
28
+ async function handleAdminCommand(jid, messageContent) {
29
+ logger.debug({ adminJid: jid, command: messageContent }, `[AdminHandler] Processing potential admin command.`);
30
+
31
+ const pendingAction = adminState[jid];
32
+
33
+ // --- Step 1: Check if awaiting confirmation ---
34
+ if (pendingAction) {
35
+ const confirmation = messageContent.trim().toLowerCase();
36
+ const semesterToDelete = pendingAction.semester; // Get semester from stored state
37
+
38
+ if (pendingAction.action === 'confirm_clear') {
39
+ if (confirmation === 'yes') {
40
+ logger.warn({ adminJid: jid, semester: semesterToDelete }, `[AdminHandler] Confirmation 'yes' received for .clear`);
41
+ clearAdminState(jid); // Clear state before performing action
42
+ try {
43
+ const deleteResult = await User.deleteMany({
44
+ semester: { $regex: new RegExp(`^${semesterToDelete}$`, 'i') }
45
+ });
46
+ const replyText = `✅ Admin: Successfully deleted ${deleteResult.deletedCount} records matching semester "${semesterToDelete}".`;
47
+ logger.warn({ result: deleteResult, semester: semesterToDelete }, `[AdminHandler] .clear command executed successfully.`);
48
+ await whatsapp.sendMessageWithTyping(jid, { text: replyText });
49
+ } catch (cmdError) {
50
+ logger.error({ err: cmdError, semester: semesterToDelete }, `[AdminHandler] .clear command failed during execution.`);
51
+ await whatsapp.sendMessageWithTyping(jid, { text: `❌ Admin: Error executing .clear command for semester "${semesterToDelete}" after confirmation. Check logs.` });
52
+ }
53
+ } else { // Includes 'no' or any other reply
54
+ logger.info({ adminJid: jid, semester: pendingAction.semester, reply: confirmation }, `[AdminHandler] .clear command aborted by admin or invalid confirmation.`);
55
+ clearAdminState(jid);
56
+ await whatsapp.sendMessageWithTyping(jid, { text: `❌ Admin: Aborted deletion for semester "${semesterToDelete}".` });
57
+ }
58
+ } else {
59
+ // Handle other potential pending actions here if added later
60
+ logger.warn({ adminJid: jid, pendingAction }, `[AdminHandler] Unknown pending action found.`);
61
+ clearAdminState(jid); // Clear unknown state
62
+ }
63
+ return true; // Confirmation message was processed
64
+ }
65
+
66
+ // --- Step 2: Check for new commands if not awaiting confirmation ---
67
+
68
+ // ** .clear command **
69
+ if (messageContent.startsWith('.clear ')) {
70
+ const parts = messageContent.split(' ');
71
+ if (parts.length === 2 && parts[1]) {
72
+ const semesterToDelete = parts[1].trim();
73
+ logger.warn({ adminJid: jid, semester: semesterToDelete }, `[AdminHandler] .clear command received. Requesting confirmation.`);
74
+
75
+ // Set pending state with timeout
76
+ adminState[jid] = {
77
+ action: 'confirm_clear',
78
+ semester: semesterToDelete,
79
+ timeoutId: setTimeout(() => {
80
+ if (adminState[jid]?.action === 'confirm_clear') { // Check if still pending this action
81
+ logger.warn({ adminJid: jid, semester: semesterToDelete }, `[AdminHandler] Confirmation for .clear timed out.`);
82
+ delete adminState[jid]; // Clear state on timeout
83
+ whatsapp.sendMessageWithTyping(jid, { text: `⏰ Admin: Confirmation request for deleting semester "${semesterToDelete}" timed out.` }).catch(err => logger.error({err}, "Failed to send timeout message"));
84
+ }
85
+ }, CONFIRMATION_TIMEOUT)
86
+ };
87
+
88
+ // Ask for confirmation
89
+ const promptText = `❓ *Confirmation Needed* ❓\n\nAre you sure you want to delete ALL student data for semester *${semesterToDelete}*? This cannot be undone.\n\nReply with *Yes* or *No*. (Expires in 60 seconds)`;
90
+ await whatsapp.sendMessageWithTyping(jid, { text: promptText });
91
+
92
+ } else { // Invalid format
93
+ await whatsapp.sendMessageWithTyping(jid, { text: `❌ Admin: Invalid .clear command format. Use: \`.clear <semester>\` (e.g., \`.clear 3rd\`)` });
94
+ }
95
+ return true; // .clear command attempt was handled
96
+ }
97
+
98
+ // --- Add other admin commands here ---
99
+ // else if (messageContent.startsWith('.some_other_command')) {
100
+ // logger.info({ adminJid: jid }, `[AdminHandler] Handling other admin command.`);
101
+ // ... handle command ...
102
+ // return true;
103
+ // }
104
+
105
+ // If no known admin command matched
106
+ logger.debug({ adminJid: jid, command: messageContent }, `[AdminHandler] Not a recognized admin command.`);
107
+ return false; // No admin command was processed
108
+ }
109
+
110
+ module.exports = { handleAdminCommand };
111
+
src/handlers/messageHandler.js ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/handlers/messageHandler.js
2
+ const logger = require('../logger');
3
+ const config = require('../config');
4
+ const whatsapp = require('../services/whatsappService');
5
+ const { User } = require('../database');
6
+ const { handleAdminCommand } = require('./adminHandler');
7
+ const { handleRegistration } = require('./registrationHandler');
8
+ const { handleRegisteredUser } = require('./registeredUserHandler');
9
+
10
+ /**
11
+ * Processes an incoming message, filters, marks as read (non-blocking),
12
+ * and routes to the appropriate handler.
13
+ * @param {object} msg - The Baileys message object.
14
+ */
15
+ async function processMessage(msg) {
16
+ // Define jid early for potential use in top-level catch
17
+ const jid = msg?.key?.remoteJid || 'unknown';
18
+ try {
19
+ const messageContent = msg.message?.conversation || msg.message?.extendedTextMessage?.text || '';
20
+
21
+ // Ignore empty text messages
22
+ if (!messageContent.trim() && !msg.message?.buttonsResponseMessage) {
23
+ logger.trace({ jid }, "[MessageHandler] Ignoring empty message content.");
24
+ return;
25
+ }
26
+
27
+ // --- Mark as Read (Fire-and-Forget) ---
28
+ // Call readReceipt WITHOUT 'await'. This lets the function run
29
+ // in the background without blocking further execution.
30
+ // If it times out, it will log a warning (due to the try/catch
31
+ // inside whatsappService.readReceipt) but won't crash the app here.
32
+ whatsapp.readReceipt([msg.key]);
33
+ // --- End Mark as Read ---
34
+
35
+ // --- Routing ---
36
+ logger.info({ jid, msg: messageContent }, `[MessageHandler] Processing received message`);
37
+
38
+ // 1. Check if Admin Command
39
+ if (config.adminJids.length > 0 && config.adminJids.includes(jid)) {
40
+ const commandHandled = await handleAdminCommand(jid, messageContent);
41
+ if (commandHandled) {
42
+ logger.info({ jid, msg: messageContent }, `[MessageHandler] Admin command handled.`);
43
+ return; // Stop processing
44
+ }
45
+ logger.debug({ jid }, "[MessageHandler] Message from admin JID not a known admin command, proceeding.");
46
+ }
47
+
48
+ // 2. Check DB for User Status
49
+ let dbUser = await User.findOne({ remoteJid: jid });
50
+
51
+ // 3. Route to appropriate handler
52
+ if (dbUser && dbUser.isRegistered) {
53
+ logger.debug({ jid }, "[MessageHandler] Routing to RegisteredUserHandler.");
54
+ await handleRegisteredUser(jid, messageContent, dbUser);
55
+ } else {
56
+ logger.debug({ jid }, "[MessageHandler] Routing to RegistrationHandler.");
57
+ await handleRegistration(jid, messageContent);
58
+ }
59
+
60
+ } catch (error) {
61
+ // Catch errors from DB check or handler execution
62
+ logger.error({ jid, err: error }, `[MessageHandler] Error processing message`);
63
+ try {
64
+ // Use default delay for error message
65
+ if (jid !== 'unknown') {
66
+ await whatsapp.sendMessageWithTyping(jid, { text: "Sorry, an internal error occurred. Please try again." });
67
+ }
68
+ } catch (sendError) {
69
+ logger.error({ jid, err: sendError }, `[MessageHandler] CRITICAL: Failed to send error notification`);
70
+ }
71
+ }
72
+ }
73
+
74
+ module.exports = { processMessage };
75
+
src/handlers/registeredUserHandler.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/handlers/registeredUserHandler.js
2
+ const logger = require('../logger');
3
+ const whatsapp = require('../services/whatsappService');
4
+ const config = require('../config'); // Import config for delays
5
+
6
+ /**
7
+ * Handles incoming messages from already registered users. Uses whatsappService.
8
+ */
9
+ async function handleRegisteredUser(jid, messageContent, dbUser) {
10
+ logger.debug({ jid, name: dbUser.name, msg: messageContent }, `[RegisteredUserHandler] Handling message.`);
11
+
12
+ const trimmedLowerCaseMsg = messageContent.trim().toLowerCase();
13
+
14
+ // 1. Check for "attendance" keyword
15
+ if (trimmedLowerCaseMsg.includes('attendance')) {
16
+ logger.info({ jid, name: dbUser.name }, `[RegisteredUserHandler] Attendance keyword detected.`);
17
+ // Use specific attendance delay
18
+ const delayOptions = { minDelay: config.attendanceMinDelay, maxDelay: config.attendanceMaxDelay };
19
+ await whatsapp.sendMessageWithTyping(jid, { text: `Okay ${dbUser.name}, preparing your attendance details... (PDF generation coming soon!)` }, delayOptions);
20
+ return;
21
+ }
22
+
23
+ // 2. Check for exact "hi" or "hello"
24
+ if (trimmedLowerCaseMsg === 'hi' || trimmedLowerCaseMsg === 'hello') {
25
+ logger.info({ jid, name: dbUser.name }, `[RegisteredUserHandler] Greeting triggered.`);
26
+ // Use default delay for greeting
27
+ await whatsapp.sendMessageWithTyping(jid, { text: `Hello ${dbUser.name}! You are already registered.` });
28
+ return;
29
+ }
30
+
31
+ // No action/reply for other messages
32
+ logger.debug({ jid, name: dbUser.name, msg: messageContent }, `[RegisteredUserHandler] No specific action triggered. No reply sent.`);
33
+
34
+ }
35
+
36
+ module.exports = { handleRegisteredUser };
37
+
src/handlers/registrationHandler.js ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/handlers/registrationHandler.js (previously selfReg.js)
2
+ const logger = require('../logger');
3
+ const whatsapp = require('../services/whatsappService');
4
+ const { User } = require('../database'); // Import User model directly
5
+ const config = require('../config'); // Import config for delays
6
+ const { sleep } = require('../utils'); // Import sleep directly if needed
7
+
8
+ const userState = {};
9
+ const REGISTRATION_DELAY_MIN = 2000; // 2 seconds
10
+ const REGISTRATION_DELAY_MAX = 3000; // 3 seconds
11
+
12
+ /**
13
+ * Handles the multi-step registration process. Uses whatsappService for communication.
14
+ * /restart command functionality has been removed.
15
+ */
16
+ async function handleRegistration(jid, messageContent) { // Renamed remoteJid to jid for consistency
17
+ logger.debug({ jid }, `[RegistrationHandler] Handling step.`);
18
+
19
+ const currentState = userState[jid] || { step: 'START', data: {} };
20
+ const delayOptions = { minDelay: config.registrationMinDelay, maxDelay: config.registrationMaxDelay };
21
+
22
+ // REMOVED: Global /restart check is removed from here.
23
+
24
+ try { // Wrap steps in try-catch
25
+ switch (currentState.step) {
26
+ case 'START':
27
+ userState[jid] = { step: 'ASKING_NAME', data: {} };
28
+ // REMOVED: Mention of /restart removed from the prompt
29
+ await whatsapp.sendMessageWithTyping(jid, { text: "Hi there! Let's get you registered.\n\nWhat is your full Name?" }, delayOptions);
30
+ break;
31
+
32
+ case 'ASKING_NAME':
33
+ if (!messageContent.trim()) {
34
+ await whatsapp.sendMessageWithTyping(jid, { text: "Please enter a valid name." }, delayOptions);
35
+ break;
36
+ }
37
+ currentState.data.name = messageContent.trim();
38
+ currentState.step = 'ASKING_REGNO';
39
+ userState[jid] = currentState;
40
+ logger.debug({ jid, data: currentState.data }, `[RegistrationHandler] Name received.`);
41
+ await whatsapp.sendMessageWithTyping(jid, { text: `Got it, ${currentState.data.name}.\n\nWhat is your Registration Number?` }, delayOptions);
42
+ break;
43
+
44
+ case 'ASKING_REGNO':
45
+ if (!messageContent.trim()) {
46
+ await whatsapp.sendMessageWithTyping(jid, { text: "Please enter a valid registration number." }, delayOptions);
47
+ break;
48
+ }
49
+ currentState.data.regNo = messageContent.trim();
50
+ currentState.step = 'ASKING_SEMESTER';
51
+ userState[jid] = currentState;
52
+ logger.debug({ jid, data: currentState.data }, `[RegistrationHandler] RegNo received.`);
53
+ await whatsapp.sendMessageWithTyping(jid, { text: "Great!\n\nWhich Semester are you in? (e.g., 1st, 2nd, 3rd, 4th)" }, delayOptions);
54
+ break;
55
+
56
+ case 'ASKING_SEMESTER':
57
+ if (!messageContent.trim()) {
58
+ await whatsapp.sendMessageWithTyping(jid, { text: "Please enter a valid semester." }, delayOptions);
59
+ break;
60
+ }
61
+ currentState.data.semester = messageContent.trim();
62
+ currentState.step = 'CONFIRMING';
63
+ userState[jid] = currentState;
64
+ logger.debug({ jid, data: currentState.data }, `[RegistrationHandler] Semester received.`);
65
+
66
+ const confirmationText = `?? *Please confirm your details:* ??\n\n*Name:* ${currentState.data.name}\n*Reg No:* ${currentState.data.regNo}\n*Semester:* ${currentState.data.semester}\n\nIs this correct? Reply with *yes* or *no*.`;
67
+ await whatsapp.sendMessageWithTyping(jid, { text: confirmationText }, delayOptions);
68
+ break;
69
+
70
+ case 'CONFIRMING':
71
+ const confirmation = messageContent.trim().toLowerCase();
72
+ if (confirmation === 'yes') {
73
+ try {
74
+ logger.info({ jid, data: currentState.data }, `[RegistrationHandler] User confirmed. Saving data.`);
75
+ await sleep(300); // Small pause before DB write
76
+
77
+ const updateResult = await User.updateOne(
78
+ { remoteJid: jid },
79
+ {
80
+ $set: {
81
+ name: currentState.data.name,
82
+ regNo: currentState.data.regNo,
83
+ semester: currentState.data.semester,
84
+ isRegistered: true,
85
+ registrationTimestamp: new Date()
86
+ }
87
+ },
88
+ { upsert: true }
89
+ );
90
+ logger.info({ jid, result: updateResult }, `[RegistrationHandler] MongoDB update result.`);
91
+
92
+ if (updateResult.acknowledged) {
93
+ await whatsapp.sendMessageWithTyping(jid, { text: "✅ Registration successful! Your details have been saved." }, delayOptions);
94
+ delete userState[jid]; // Clear state
95
+ } else { throw new Error("Database update not acknowledged."); }
96
+ } catch (dbError) {
97
+ logger.error({ jid, err: dbError }, `[RegistrationHandler] Error saving data during confirmation.`);
98
+ // REMOVED: Mention of /restart in error message
99
+ await whatsapp.sendMessageWithTyping(jid, { text: "❌ Sorry, there was an error saving your details. Please reply 'yes' again or 'no' to start over." }, delayOptions);
100
+ }
101
+ } else if (confirmation === 'no') { // Only 'no' triggers restart now
102
+ logger.info({ jid }, `[RegistrationHandler] User replied 'no' at confirmation.`);
103
+ delete userState[jid];
104
+ userState[jid] = { step: 'ASKING_NAME', data: {} };
105
+ await whatsapp.sendMessageWithTyping(jid, { text: "Okay, let's start over.\n\nWhat is your full Name?" }, delayOptions);
106
+ } else {
107
+ await whatsapp.sendMessageWithTyping(jid, { text: "Please reply with *yes* or *no*." }, delayOptions);
108
+ }
109
+ break;
110
+
111
+ default: // Unhandled state
112
+ logger.warn({ jid, state: currentState.step }, `[RegistrationHandler] Unhandled state step.`);
113
+ delete userState[jid];
114
+ await whatsapp.sendMessageWithTyping(jid, { text: "Something went wrong during registration. Please send any message to try starting again." }, delayOptions); // Updated generic error
115
+ break;
116
+ }
117
+ } catch (stepError) { // Catch errors in the switch/case logic itself
118
+ logger.error({ jid, err: stepError, step: currentState.step }, "[RegistrationHandler] Error during registration step execution.");
119
+ try {
120
+ // REMOVED: Mention of /restart in generic error message
121
+ await whatsapp.sendMessageWithTyping(jid, { text: "An unexpected error occurred during registration. Please try starting again by sending any message." });
122
+ delete userState[jid]; // Reset state on error
123
+ } catch (sendErr) {
124
+ logger.error({ jid, err: sendErr }, "[RegistrationHandler] Failed to send error message during step error handling.");
125
+ }
126
+ }
127
+ }
128
+
129
+ module.exports = { handleRegistration };
130
+
131
+
src/index.js ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/index.js - Main Entry Point
2
+ require('dotenv').config(); // Ensure env variables are loaded first
3
+
4
+ const logger = require('./logger');
5
+ const { connectDB } = require('./database');
6
+ const { connectWhatsApp } = require('./connection');
7
+
8
+ /**
9
+ * Starts the application by connecting to the database
10
+ * and then establishing the WhatsApp connection.
11
+ */
12
+ async function start() {
13
+ logger.info("===================================");
14
+ logger.info(" Starting WhatsApp Bot ");
15
+ logger.info("===================================");
16
+ try {
17
+ await connectDB();
18
+ await connectWhatsApp();
19
+ } catch (error) {
20
+ logger.fatal({ err: error }, "Critical error during application startup sequence.");
21
+ process.exit(1); // Exit if essential connections fail
22
+ }
23
+ }
24
+
25
+ // Execute startup sequence
26
+ start().catch(err => {
27
+ // This catch is unlikely to be hit if errors in start() are handled
28
+ // but provides a final safety net.
29
+ logger.fatal({ err: err }, "FATAL UNHANDLED ERROR during startup!");
30
+ process.exit(1);
31
+ });
32
+
33
+ // Optional: Graceful shutdown handling
34
+ process.on('SIGINT', () => {
35
+ logger.warn("Received SIGINT. Shutting down gracefully...");
36
+ // Add cleanup logic here if needed (e.g., close DB connection)
37
+ process.exit(0);
38
+ });
39
+ process.on('SIGTERM', () => {
40
+ logger.warn("Received SIGTERM. Shutting down gracefully...");
41
+ // Add cleanup logic here
42
+ process.exit(0);
43
+ });
44
+
src/logger.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/logger.js
2
+ const pino = require('pino');
3
+ const config = require('./config');
4
+
5
+ const logger = pino({
6
+ level: config.logLevel,
7
+ // Pino automatically handles pretty printing if pino-pretty is installed
8
+ // and output is piped to it (node src/index.js | pino-pretty)
9
+ });
10
+
11
+ module.exports = logger;
12
+
src/services/whatsappService.js ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/services/whatsappService.js
2
+ const { randomDelay, sleep } = require('../utils');
3
+ const logger = require('../logger');
4
+ const config = require('../config');
5
+
6
+ let sockInstance = null;
7
+
8
+ /**
9
+ * Initializes the WhatsApp Service with the Baileys socket instance.
10
+ * Should be called once after connection is established.
11
+ * @param {object} sock - The Baileys socket instance.
12
+ */
13
+ function initialize(sock) {
14
+ if (!sockInstance) {
15
+ sockInstance = sock;
16
+ logger.info('WhatsApp Service Initialized.');
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Sends a presence update (typing, paused, available, unavailable).
22
+ * @param {'composing' | 'paused' | 'available' | 'unavailable'} status - The presence status.
23
+ * @param {string} jid - The target JID.
24
+ */
25
+ async function sendPresenceUpdate(status, jid) {
26
+ if (!sockInstance) return logger.error('WhatsApp Service not initialized for sendPresenceUpdate');
27
+ try {
28
+ await sockInstance.sendPresenceUpdate(status, jid);
29
+ } catch (err) {
30
+ logger.error({ err, jid, status }, '[WhatsAppService] Failed to send presence update');
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Marks specific messages as read.
36
+ * @param {Array<object>} keys - An array of message key objects.
37
+ */
38
+ async function readReceipt(keys) {
39
+ if (!sockInstance) return logger.error('WhatsApp Service not initialized for readReceipt');
40
+ try {
41
+ await sockInstance.readMessages(keys);
42
+ // Log the first key for brevity if needed
43
+ if (keys && keys[0]) {
44
+ logger.debug({ jid: keys[0].remoteJid, msgId: keys[0].id }, `[WhatsAppService] Marked message(s) as read`);
45
+ }
46
+ } catch (err) {
47
+ const jid = keys && keys[0] ? keys[0].remoteJid : 'unknown';
48
+ logger.warn({ err, jid }, '[WhatsAppService] Failed to mark message(s) as read');
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Sends a message with simulated typing and random delay.
54
+ * @param {string} jid - The target JID.
55
+ * @param {object} message - The Baileys message object (e.g., { text: 'Hello' }).
56
+ * @param {object} [options={}] - Options object.
57
+ * @param {number} [options.minDelay] - Minimum delay (defaults to config.defaultMinDelay).
58
+ * @param {number} [options.maxDelay] - Maximum delay (defaults to config.defaultMaxDelay).
59
+ * @returns {Promise<object|null>} - The result from Baileys sendMessage or null on init error.
60
+ */
61
+ async function sendMessageWithTyping(jid, message, options = {}) {
62
+ if (!sockInstance) {
63
+ logger.error({ jid, message }, 'WhatsApp Service not initialized for sendMessage');
64
+ return null; // Indicate failure
65
+ }
66
+
67
+ // Use provided delays or fall back to config defaults
68
+ const minDelayMs = options.minDelay ?? config.defaultMinDelay;
69
+ const maxDelayMs = options.maxDelay ?? config.defaultMaxDelay;
70
+
71
+ try {
72
+ await sendPresenceUpdate('composing', jid);
73
+ await randomDelay(minDelayMs, maxDelayMs);
74
+ await sendPresenceUpdate('paused', jid); // Clear composing state before sending
75
+
76
+ const result = await sockInstance.sendMessage(jid, message);
77
+ logger.debug({ jid, msgContent: message.text || '[Non-Text]' }, '[WhatsAppService] Message sent');
78
+ return result;
79
+ } catch (err) {
80
+ logger.error({ err, jid, msgContent: message.text || '[Non-Text]' }, '[WhatsAppService] Failed to send message');
81
+ // Re-throw error for the calling handler to potentially manage
82
+ // Avoid sending another message from here to prevent loops
83
+ throw err;
84
+ }
85
+ }
86
+
87
+ module.exports = {
88
+ initialize,
89
+ sendMessageWithTyping,
90
+ readReceipt,
91
+ // Only export specific actions needed by handlers
92
+ };
93
+
src/utils.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/utils.js
2
+
3
+ /**
4
+ * Creates a promise that resolves after a specified number of milliseconds.
5
+ * @param {number} ms - The number of milliseconds to sleep.
6
+ * @returns {Promise<void>}
7
+ */
8
+ function sleep(ms) {
9
+ return new Promise(resolve => setTimeout(resolve, ms));
10
+ }
11
+
12
+ /**
13
+ * Waits for a random amount of time within a specified range.
14
+ * Uses defaults from config if no arguments provided.
15
+ * @param {number} [minMs] - Minimum delay in milliseconds.
16
+ * @param {number} [maxMs] - Maximum delay in milliseconds.
17
+ * @returns {Promise<void>}
18
+ */
19
+ // Import config inside if needed for defaults, or keep simple defaults here
20
+ const config = require('./config'); // Import config to use defaults
21
+
22
+ async function randomDelay(minMs = config.defaultMinDelay, maxMs = config.defaultMaxDelay) {
23
+ // Ensure min is not greater than max
24
+ if (minMs > maxMs) {
25
+ [minMs, maxMs] = [maxMs, minMs]; // Swap if necessary
26
+ }
27
+ const delay = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;
28
+ await sleep(delay);
29
+ }
30
+
31
+ module.exports = { randomDelay, sleep };
32
+