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;