Spaces:
Runtime error
Runtime error
| 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; | |