diff --git a/assets/error.png b/assets/error.png new file mode 100644 index 0000000..50e6740 Binary files /dev/null and b/assets/error.png differ diff --git a/assets/info.png b/assets/info.png new file mode 100644 index 0000000..99e15ac Binary files /dev/null and b/assets/info.png differ diff --git a/src/class/NotificationMessage.ts b/src/class/NotificationMessage.ts new file mode 100644 index 0000000..99113d3 --- /dev/null +++ b/src/class/NotificationMessage.ts @@ -0,0 +1,22 @@ +export default class NotificationMessage { + constructor() { + this.title = ''; + this.body = ''; + this.footer = null; + this.isError = false; + this.isImportant = false; + this.color = 0xFF9A00; + } + setTitle(title: string) { this.title = title; return this; } + setBody(body: string) { this.body = body; return this; } + setFooter(footer: string | null) { this.footer = footer; return this; } + setColor(color?: string | number | null) { this.color = color == null ? 0xFF9A00 : color; return this; } + error(isError?: boolean | null) { this.isError = isError == null ? true : isError; return this; } + important(isImportant?: boolean | null) { this.isImportant = isImportant == null ? true : isImportant; return this; } + public title: string; + public body: string; + public footer: string | null; + public isError: boolean; + public isImportant: boolean; + public color: string | number; +} \ No newline at end of file diff --git a/src/class/User.ts b/src/class/User.ts new file mode 100644 index 0000000..45507af --- /dev/null +++ b/src/class/User.ts @@ -0,0 +1,14 @@ +export default class User { + public creds: { + schoolName: string, + username: string, + password: string, + baseURL: string, + }; + public connections: Array<{ + type: string, + userID: string, + channelID: string, + }>; + public ID: string; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index beb4009..8a4486a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,13 +5,8 @@ import Discord from 'discord.js'; import Enmap from 'enmap'; import fs from 'fs'; import * as WebUntis from 'webuntis'; - -const untis = new WebUntis.default( - process.env.SCHOOLNAME, - process.env.USERNAME, - process.env.PASSWORD, - process.env.BASEURL -); +import User from './class/User'; +import NotificationMessage from './class/NotificationMessage'; console.log('Discord Bot: Logging in'); const client = new Discord.Client(); @@ -21,56 +16,99 @@ client.on('ready', () => console.log(`Discord Bot: Logged in as ${client.user.ta const seenMessages = new Enmap({ name: "seenMessages" }); const knownLessons = new Enmap({ name: "knownLessons" }); -const defaultEmbedColor = 0xFF9A00; - +const usersDB = new Enmap({ name: "users" }) as Enmap; +const untisInstances = [] as Array<{ untis: WebUntis.default, user: User }>; +usersDB.forEach(user => untisInstances.push({ + untis: new WebUntis.default( + user.creds.schoolName, + user.creds.username, + user.creds.password, + user.creds.baseURL + ), + user: user +})); // Periodically attempt to connect to untis and fetch timetable updates let fetchUpdates = () => { - console.log('Untis: Logging in'); - try { - untis.login() - .then(() => { - console.log(`Untis: Logged in`) - - fs.readdirSync(`${__dirname}/modules`).filter(file => file.endsWith('.js')).forEach(file => { - require(`${__dirname}/modules/${file}`).run(); - }); - - setTimeout(() => untis.logout().then(() => console.log('Untis: Logged out')), 10000); - }); - } catch(e) { - console.error(e); + console.log(`Fetching ${untisInstances.length} users.`); + untisInstances.forEach(({ untis, user }) => { try { - let embed = new Discord.MessageEmbed() - .setTitle('An error has occurred.') - .setDescription(`\`\`\`js\n${e}\`\`\``) - .setColor(0xff0000); - sendEmbed(embed); + // Create database entries + if (!knownLessons.get(user.ID)) knownLessons.set(user.ID, {}); + if (!seenMessages.get(user.ID)) seenMessages.set(user.ID, {}); + + untis.login() + .then(() => { + fs.readdirSync(`${__dirname}/modules`).filter(file => file.endsWith('.js')).forEach(file => { + require(`${__dirname}/modules/${file}`).run(untis, user); + }); + + setTimeout(() => untis.logout().then(() => console.log('Untis: Logged out')), 10000); + }); } catch(e) { console.error(e); + try { + sendMessage(new NotificationMessage() + .setTitle('An error has occurred.') + .setBody(`\`\`\`js\n${e}\`\`\``) + .error(), + user); + } catch(e) { + console.error(e); + } } - } + }); } fetchUpdates(); setInterval(fetchUpdates, 60000); -async function sendEmbed(embed: Discord.MessageEmbed) { - const channel = await client.channels.fetch(process.env.CHANNEL) as Discord.DMChannel; - if (!channel) throw `ERROR: Could not find channel`; - - if (!embed.timestamp) embed.setTimestamp(); - channel.send(embed).catch(console.error); +async function sendMessage(message: NotificationMessage, user: User) { + user.connections.forEach(async connection => { + switch(connection.type) { + case 'discord': + const channel = await client.channels.fetch(connection.channelID).catch(()=>{}) as Discord.DMChannel; + if (!channel) return console.log(`Could not find channel: ${channel?.id}`); + + let embed = new Discord.MessageEmbed() + .setAuthor(message.title) + .setDescription(message.body) + .setColor(message.color); + + if (message.footer) embed.setFooter(message.footer); + + // Couldn't figure out how to attach the images in the assets folder, so I use hardcoded URLs instead + if (message.isImportant) { + embed.setAuthor(embed.author.name, 'https://cdn.discordapp.com/attachments/637695358373199879/824676476405940284/info.png'); + } + + if (message.isError) { + embed.color = 0xff0000; + embed.setAuthor(embed.author.name, 'https://cdn.discordapp.com/attachments/637695358373199879/824676465051828334/error.png' ); + } + + if (!embed.timestamp) embed.setTimestamp(); + channel.send(embed) + .catch(console.error); + break; + default: console.log(`Unknown connection type: ${connection.type}`); + } + }); } export default { - untis: untis, + untisInstances, + usersDB, bot: client, - defaultEmbedColor: defaultEmbedColor, - sendEmbed: sendEmbed, + sendEmbed: sendMessage, db: { seenMessages: seenMessages, knownLessons: knownLessons } -}; \ No newline at end of file +}; + + +// Run tasks in dist/tasks/* +fs.readdirSync(`${__dirname}/tasks`).filter(file => file.endsWith('.js')).forEach(file => + require(`${__dirname}/tasks/${file}`)); \ No newline at end of file diff --git a/src/modules/scanForNotifications.ts b/src/modules/scanForNotifications.ts index 745e17a..31501d5 100644 --- a/src/modules/scanForNotifications.ts +++ b/src/modules/scanForNotifications.ts @@ -1,23 +1,23 @@ import * as WebUntis from 'webuntis'; import main from '../index'; -import Discord from 'discord.js'; -const { bot, defaultEmbedColor, untis, db, sendEmbed } = main; +import User from '../class/User'; +import NotificationMessage from '../class/NotificationMessage'; +const { bot, untisInstances, db, sendEmbed } = main; -export async function run() { +export async function run(untis: WebUntis.default, user: User) { let news = await untis.getNewsWidget(new Date(), true); if (!news) return; news.messagesOfDay.forEach(message => { - if (db.seenMessages.get(`${message.id}`)) return; + if (db.seenMessages.get(user.ID, `${message.id}`)) return; console.log('New message found: ' + message.text); - let embed = new Discord.MessageEmbed() - .setAuthor('Notification') - .setColor(defaultEmbedColor) + let notif = new NotificationMessage() + .setTitle('Notification') .setTitle(message.subject) - .setDescription(message.text.replace(/\/g, "\n")) + .setBody(message.text.replace(/\/g, "\n")) .setFooter(`Notification ID: ${message.id}`); - sendEmbed(embed); - db.seenMessages.set(`${message.id}`, true); + sendEmbed(notif, user); + db.seenMessages.set(user.ID, true, `${message.id}`); }); } \ No newline at end of file diff --git a/src/modules/scanForTimetableChanges.ts b/src/modules/scanForTimetableChanges.ts index bd9520b..85450cd 100644 --- a/src/modules/scanForTimetableChanges.ts +++ b/src/modules/scanForTimetableChanges.ts @@ -1,15 +1,16 @@ import * as WebUntis from 'webuntis'; import main from '../index'; -import Discord from 'discord.js'; -const { bot, defaultEmbedColor, untis, db, sendEmbed } = main; +import User from '../class/User'; +import NotificationMessage from '../class/NotificationMessage'; +const { bot, untisInstances, db, sendEmbed } = main; -export async function run() { +export async function run(untis: WebUntis.default, user: User) { let timetable = await untis.getOwnTimetableForRange(new Date(Date.now() - 86400000), new Date(Date.now() + (86400000 * 7)), true); - + let sentClasses = {}; - + timetable.forEach(lesson => { - let kLesson = db.knownLessons.get(`${lesson.id}`); + let kLesson = db.knownLessons.get(user.ID, `${lesson.id}`); if (kLesson && hasChanged(lesson, kLesson)) { if (sentClasses['' + (lesson.sg || lesson.su?.[0]?.id) + ' -- ' + lesson.date]) return; let dateInfo = { @@ -24,10 +25,8 @@ export async function run() { let weekDay = ['Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()]; - let embed = new Discord.MessageEmbed() - .setTitle(`Lesson updated`) - .setColor(defaultEmbedColor) - .setAuthor(`${weekDay}, ${date.getDate()}. ${date.getMonth() + 1}.: ${lesson.sg || '(Unnamed lesson)'} - ${lesson.te[0].longname || 'No teacher'}`); + let notif = new NotificationMessage() + .setTitle(`${weekDay}, ${date.getDate()}. ${date.getMonth() + 1}.: ${lesson.sg || '(Unnamed lesson)'} - ${lesson.te[0].longname || 'No teacher'}`); let desc = ``; @@ -69,15 +68,15 @@ export async function run() { // Change the embed color when teacher ID is 0. // Teacher ID 0 means that the class is canelled (at least on my school), // although I don't know if this is always the case. - if (lesson.code == 'irregular' || lesson.te[0].id == 0) embed.setColor('A781B4'); + if (lesson.code == 'irregular' || lesson.te[0].id == 0) notif.setColor('A781B4'); - embed.setDescription(desc); + notif.setBody(desc); sentClasses['' + (lesson.sg || lesson.su?.[0]?.id) + ' -- ' + lesson.date] = true; - sendEmbed(embed); + sendEmbed(notif, user); console.log(`Sent timetable update`); - db.knownLessons.set(`${lesson.id}`, lesson); + db.knownLessons.set(user.ID, lesson, `${lesson.id}`); } - else db.knownLessons.set(`${lesson.id}`, lesson); + else db.knownLessons.set(user.ID, lesson, `${lesson.id}`); }); } diff --git a/src/tasks/cleanDB.ts b/src/tasks/cleanDB.ts new file mode 100644 index 0000000..9a3c239 --- /dev/null +++ b/src/tasks/cleanDB.ts @@ -0,0 +1,46 @@ +import main from '../index'; +const { db } = main; + +/** + * To-Do: + * - Remove invalid entries from usersDB + * - Clean up seenMessages DB + */ + +// Deletes old, unneeded entries from the database +const run = async () => { + // Known lessons DB + try { + let deleted = 0; + db.knownLessons.forEach(userDB => { + Object.entries(userDB).forEach(data => { + let key: string | number = data[0]; + let entry: any = data[1]; + if (!entry.date || unfuckDate(entry.date).getTime() < Date.now() - (86400000 * 2)) { + deleted++; + db.knownLessons.delete(key); + } + }); + }); + if (deleted > 0) console.log(`Cleared ${deleted} entries from knownLessons`); + } catch(e) { + console.error(e); + } +} + +let unfuckDate = (date: number): Date => { + let dateInfo = { + year: Number(`${date}`.substr(0, 4)), + month: Number(`${date}`.substr(4, 2)), + day: Number(`${date}`.substr(6, 2)) + } + let newDate = new Date() + newDate.setFullYear(dateInfo.year); + newDate.setMonth(dateInfo.month - 1); + newDate.setDate(dateInfo.day); + + return newDate; +} + +run(); +setInterval(run, 1000 * 60 * 60 * 12); // Run every 12 hours \ No newline at end of file