Spaces:
Runtime error
Runtime error
File size: 8,679 Bytes
c4be319 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | 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;
|