From a9c3f42f17853bcc3d8e26d5ddf6d3dbae662e49 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 26 Mar 2021 10:51:30 +0100 Subject: [PATCH] support multiple users --- assets/error.png | Bin 0 -> 2083 bytes assets/info.png | Bin 0 -> 1925 bytes src/class/NotificationMessage.ts | 22 +++++ src/class/User.ts | 14 +++ src/index.ts | 116 ++++++++++++++++--------- src/modules/scanForNotifications.ts | 20 ++--- src/modules/scanForTimetableChanges.ts | 29 +++---- src/tasks/cleanDB.ts | 46 ++++++++++ 8 files changed, 183 insertions(+), 64 deletions(-) create mode 100644 assets/error.png create mode 100644 assets/info.png create mode 100644 src/class/NotificationMessage.ts create mode 100644 src/class/User.ts create mode 100644 src/tasks/cleanDB.ts diff --git a/assets/error.png b/assets/error.png new file mode 100644 index 0000000000000000000000000000000000000000..50e67400e7fbc30621f306632d7d0bf4d5d613bc GIT binary patch literal 2083 zcmV+;2;BFHP)XL3`;6vE3-hC=A{mV!`8gwS>b)EH5S2&LuW3kxy>2xA}8x%c>CpBbhzXW!S{JEifT z%!j#q?S0l>XP>>-UVH7t6jA0N%*~jC_u!zEL#tHrX39{N!Gx{T*?K-Zrb4fzQw^`< z$SUVzVa9yCuZijzUdZTA*i_=-3wgB@dbZU7t2l5P7veH3MB1jNhw)Ixoe4khVP~7# zZI1!&T^b`=T!jA_%EoARNI1qo;4=q8@kUt(D^ zD@MZxD03Vh(%5pgGovrz_N6?Mgk`O1dPz@Ye#R%T9Mc-8_8dOi!(SSCMneWDa}s`u z!|RA{&$zk5^(*;TBW*WInIjUe&bR{Ib;Mt;vS@jjA8Jz90DT;rh6(0~I-l5Y*(&Jx zn;K#ZGQJrrEF1xAIV)p>xPg9$b9(qsqsn%Ye5bhqr$t1n8S|Hg&v_3f7$A3VpTxXK zW1CByaUrj@s&FT`im$X7?jIbfQAQ20p6(2{7!Q;v@aRPlU=^qMlJ zhb?opsH`i*6U>U#-PX<{sJ%sg$ofj8cvhOkLrQOUNT2kQ)7!lgyv7SNIku>5 zkbkuES%f&p&al}lVxW|=wQ!LPpT}`{s+|huXZbI-v&~xUtnKJ+Np}QCA?EL&(lO1Z zh>VjXMb!g%KD{i2y9!TIYwQyDB?{vlc=3F8jO+&cizE`x?rmS9)0ERon&%iyAjvmL zDK&RYN1qr{-1bT4g|{C&)!s;cFp}`FaCkT&DpUu`5De&!p+}!N>^Z@aA7MCImi)2YNU!e`X+y$Iqs9?L%h* zBjh=YMV)6%Uq=nl%{>^5RQTL#j%k~A9cCS~Gd>$BeiiqR{4;8R^VyNau28cC#z`uQ{Y79Oq9=&5+Lyce^@`$8(uZ}H(8 zVjG?biPVKe4X}XWVV2^B8ja7z<7>F2p0e#`EtjPsRj<~hyk(f>z46bveyZ#`X6Mm^+^lq}uH2h*@usFy6g1GjbY!*=Hstmdc^U(P+e zn2S5Ywvs%Pl$tuX{^(My_dX)jG!h65U?wxzcYfb99{}p7tO>7Ue1rGH)cFh>rP-Ez?_qZu@>gRjC!BmSRNi`MKeB1OIKzF z=*2g*7A?x~Pr)M_Ig>cZJ6C2}xrS58`1WSL)7#`|Yx&aD8s;Y<6D#I1P8Cn$ik`3> zHciX%zGXb}1}7!q%G};KNPR8i^Rqa$)y~tVi<9>;+X@32w($xybnoJVJP$9LdDa5E@nFUsJGeh~>#P+zc6s_P4&N^rVPO`Mux2;$ypimberz0O zL#KA19W%gqR&iiAbE?cqcyE;%35R51Rd_Sut%Sh}TX(Xhoo~4o{{yvE@fpmb{&@fZ N002ovPDHLkV1jM3|Azno literal 0 HcmV?d00001 diff --git a/assets/info.png b/assets/info.png new file mode 100644 index 0000000000000000000000000000000000000000..99e15acb1c26dadc226396c3124ad90a8b09c140 GIT binary patch literal 1925 zcmZ{lc|6pK8^^z6-S^}cvNnSlI|srzTSOW;CoA_k1~IO3l@;TTa$jkNMMx>S40aGH zQ|>d9`zFjxF|u}k``_=6eO|B6^L{1R0I?7NNK6I*7Q2cCsgmWLa86;Z#K+`R*`?UBk?F8dvv?=Ol<}gv!Yo6G>el*l1_$q8B!CA>A5fvo2U??{j znjLc$0VnT^r{@hV3|h*e$h5`)ReHsBjUB3wVxtRb;*q@&F)?F(I{ae^~GR) zLN&Q{)6BX4*Or?WE$7POVArK0$DDu)g*>ATB~0r0hDu|ZXo$Yql_f`Gag?F`m~Mfr zuLA98*ydvd(JAh6Z{K!@vS4^#m5*0J=ePl;NQe~0=dZ$SdwxG>&=F4neb+I+)wwu6 zfrF_Y@F~dp?lz2SCaO(WM2SecKFKO^PW8w?|ED1PY=Q^h?%-|g=vXqnK(ugU#8*Z$ zALAnbved{I#6Bt0OUw{;zb%2P)EMT-h$rFhvYA7jGL;u3jo=e&nv;ZU`>iq-3Vo>-oPv|HR`p8Wy$X7CzPFb$SN3o>nKi zEgszSYEz|!lgHkT-;euRu4)3*V4aaB?ZM+aM4AwD5}>ora;0@59nKM?wu7|#-=8}h z@*_50O}^3UNg}B&Zy8O)X?13-S!*&M=C0(cn4C+R@p&d9OMRkLqnG?3nUii$9Vzqd zoH=E)KmSzgaL*3uUE5l0Tz4=#h#Lv(6Sii)wK-$I-;;N*p#`8wo$@<*#B(S}7m7~V z%shc?RhHw5Z}8_^xOsPtk@5JZ>grA%kP-5om?F~)-cP+VeD&2_Jd>lR=Tt^j^tzqM zKwXAI6To#w<s!`ktwmc$)8-HAiak!+87>)!oXX3FxF%UYD12io9G#!(UUVa+3fcsLyJmX4ZPH&p83i?W z%TbW&_4Fj@GOJKRu|-dL@%J7nnmadD$bfjSaftOOWOG;6A z@iDCB;5RqUcaS&_|1LlL~FY(%FVT z?_jbjZiUSC*RG%%&&O1jn1!ez+;+N9-a9FaAar4h1m6i!VTmXD$Zd(nD7R;9KSdnR zBZEN*@Jc2vw^At9ltJFp37Bx|R_W+pz06qLs#8Dz^_WQgf|H5E28O$>EPnFCaFqWA z8Tv?E`R*UnM|Ccn{k}S%kHtS;O4Trdbh(k*U$tKt`9MnGcke5!IS6pSX}E)l>@}ct zZ8coK*AjXgDy+`B1^v*W$zieuQ?|COSMHh36u}RVO)j1t&=T#J@nBjGsmHbwYKP~W zz*2J=@)Zi;A1}HPk5z1VZ@EzQjL&i8KIR_h!yvuEOZ3T_zTSBQCKx=IA^Ou{dl2Ub zc&$yh0(eU_SO!PitK$nZPy1uwYuxjt3HM_I zK)?`iC=3ZjXgk8S^tIvoa2<6ROdke|r9BP*KSM~UZ=iq7e;Wku@P1?)04sAFv!)B4 GN&f(h41du8 literal 0 HcmV?d00001 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