Spaces:
Building
Building
Upload 13 files
Browse files- .env +7 -0
- package.json +26 -0
- src/config.js +22 -0
- src/connection.js +110 -0
- src/database.js +38 -0
- src/handlers/adminHandler.js +111 -0
- src/handlers/messageHandler.js +75 -0
- src/handlers/registeredUserHandler.js +37 -0
- src/handlers/registrationHandler.js +131 -0
- src/index.js +44 -0
- src/logger.js +12 -0
- src/services/whatsappService.js +93 -0
- src/utils.js +32 -0
.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 |
+
|