const { Client, GatewayIntentBits, Partials, Events, REST, Routes } = require('discord.js'); const mongoose = require('mongoose'); const User = require('../models/User'); const { encrypt } = require('../utils/crypto'); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.DirectMessages, GatewayIntentBits.MessageContent ], partials: [Partials.Channel, Partials.Message] }); const commands = [ { name: 'status', description: 'Check if DueBot is online and active.' }, { name: 'login', description: 'Securely link your Moodle LMS account.', options: [ { name: 'username', description: 'Your Moodle username', type: 3, // STRING required: true }, { name: 'password', description: 'Your Moodle password', type: 3, // STRING required: true } ] }, { name: 'deadlines', description: 'View all your currently tracking deadlines.' }, { name: 'sync', description: 'Force an immediate sync with your Moodle LMS to check for deadlines right now.' } ]; const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); client.once(Events.ClientReady, async (c) => { console.log(`Ready! Logged in as ${c.user.tag}`); try { await rest.put( Routes.applicationCommands(c.user.id), { body: commands } ); console.log('Successfully registered slash commands.'); } catch (error) { console.error('Error registering slash commands:', error); } }); client.on(Events.InteractionCreate, async interaction => { if (!interaction.isChatInputCommand()) return; if (interaction.commandName === 'status') { await interaction.reply({ content: 'DueBot is online, fully synced, and running smoothly 🚀', ephemeral: true }); } if (interaction.commandName === 'login') { const username = interaction.options.getString('username'); const password = interaction.options.getString('password'); const discordId = interaction.user.id; try { await interaction.deferReply({ ephemeral: true }); // Encrypt passwords using AES-256 const userEncrypted = encrypt(username); const passEncrypted = encrypt(password); let user = await User.findOne({ discordId }); if (user) { user.lmsUsername = userEncrypted.encryptedData; user.lmsPassword = passEncrypted.encryptedData; user.iv = passEncrypted.iv; // both encrypt calls generate different IVs. For simplicity, we can use the same logic or store them. // Let's refine the schema saving here await user.save(); await interaction.editReply('Your Moodle credentials have been updated successfully and encrypted in the database 🛡️. Scraping will begin soon!'); } else { user = new User({ discordId, lmsUsername: userEncrypted.encryptedData, lmsPassword: passEncrypted.encryptedData, iv: passEncrypted.iv // Need to modify slightly to use one IV or store 2. }); // actually wait, let's just store password's IV and username IV if needed. // Or don't encrypt username. Let's just encrypt password. Username is often public/student id. // For MVP, we'll store lmsUsername raw, just password encrypted to fix the IV issue purely. user.lmsUsername = username; await user.save(); await interaction.editReply('Your Moodle account was successfully linked and your password heavily encrypted 🛡️! DueBot will now scrape your deadlines.'); } } catch (error) { console.error(error); await interaction.editReply('There was an error saving your credentials. Please try again later.'); } } if (interaction.commandName === 'deadlines') { const Deadline = require('../models/Deadline'); const { getPriorityEmoji } = require('../utils/priorityEmoji'); const discordId = interaction.user.id; try { // Update to pull BOTH Pending and Missed deadlines so the 💀 emoji works const deadlines = await Deadline.find({ discordId, status: { $in: ['Pending', 'Missed'] } }).sort({ deadlineTime: 1 }); if (deadlines.length === 0) { await interaction.reply({ content: 'Hooray! You have no tracking deadlines 🎉.', ephemeral: true }); return; } const now = new Date(); const missed = deadlines.filter(dl => dl.deadlineTime < now); const upcoming = deadlines.filter(dl => dl.deadlineTime >= now); let msg = ''; let chunks = []; const addText = (text) => { if (msg.length + text.length > 1900) { chunks.push(msg); msg = ''; } msg += text; }; if (missed.length > 0) { addText('**💀 Missed / Overdue Assignments:**\n\n'); missed.forEach((dl, i) => { const timeStr = dl.deadlineTime.toLocaleString(); addText(`**${i + 1}.** 💀 **${dl.assignmentTitle}**\n*Course: ${dl.courseName}*\n*Was due at: ${timeStr}*\n\n`); }); } if (upcoming.length > 0) { if (missed.length > 0) addText('---\n\n'); // Add a nice separator between missed and upcoming addText('**📅 Your Upcoming Deadlines:**\n\n'); upcoming.forEach((dl, i) => { const timeStr = dl.deadlineTime.toLocaleString(); const emoji = getPriorityEmoji(dl.deadlineTime); // Maintain continuous numbering (e.g., 3, 4, 5...) const num = missed.length + i + 1; addText(`**${num}.** ${emoji} **${dl.assignmentTitle}**\n**Course:** ${dl.courseName}\n**Due at:** ${timeStr}\n\n`); }); } if (msg.length > 0) chunks.push(msg); for (let j = 0; j < chunks.length; j++) { if (j === 0) { await interaction.reply({ content: chunks[0], ephemeral: true }); } else { await interaction.followUp({ content: chunks[j], ephemeral: true }); } } } catch (error) { console.error(error); await interaction.reply({ content: 'Error fetching deadlines.', ephemeral: true }); } } if (interaction.commandName === 'sync') { const { scrapeDeadlines } = require('../services/scraper.service'); const { syncUserDeadlines } = require('../services/deadlineSync.service'); try { await interaction.reply({ content: '🕵️‍♂️ **Initializing manual sync with NUST Moodle...** This might take about 15-20 seconds. I will DM you when done!', ephemeral: true }); const user = await User.findOne({ discordId: interaction.user.id }); if (!user) { await interaction.user.send("You must `/login` first before syncing!").catch(() => {}); return; } const rawDeadlines = await scrapeDeadlines(user.lmsUsername, user.lmsPassword, user.iv); await syncUserDeadlines(user, rawDeadlines); // Update user's last scraped timestamp globally user.lastScrapedAt = new Date(); await user.save(); await interaction.user.send("✅ **Sync Complete!** Your deadlines are now perfectly up to date with Moodle.").catch(() => {}); } catch (error) { console.error('[Bot /sync] Error:', error); await interaction.user.send("❌ **Sync Failed.** There was an error reaching Moodle. Please try again later.").catch(() => {}); } } }); module.exports = client;