forked from Lea/UntisBot
Compare commits
2 commits
master
...
multi-user
Author | SHA1 | Date | |
---|---|---|---|
518a0d14eb | |||
a9c3f42f17 |
BIN
assets/error.png
Normal file
BIN
assets/error.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2 KiB |
BIN
assets/info.png
Normal file
BIN
assets/info.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
7
src/class/ConnectionModule.ts
Normal file
7
src/class/ConnectionModule.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { Connection } from './User';
|
||||||
|
import NotificationMessage from './NotificationMessage';
|
||||||
|
|
||||||
|
export default class ConnectionModule {
|
||||||
|
run: (message: NotificationMessage, connection: Connection) => void;
|
||||||
|
init: () => void;
|
||||||
|
}
|
22
src/class/NotificationMessage.ts
Normal file
22
src/class/NotificationMessage.ts
Normal file
|
@ -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;
|
||||||
|
}
|
16
src/class/User.ts
Normal file
16
src/class/User.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default class User {
|
||||||
|
public creds: {
|
||||||
|
schoolName: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
baseURL: string,
|
||||||
|
};
|
||||||
|
public connections: Array<Connection>;
|
||||||
|
public ID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Connection {
|
||||||
|
type: string;
|
||||||
|
userID: string;
|
||||||
|
channelID: string;
|
||||||
|
}
|
57
src/connections/discord.ts
Normal file
57
src/connections/discord.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import Discord from 'discord.js';
|
||||||
|
const client = new Discord.Client();
|
||||||
|
|
||||||
|
import User, { Connection } from '../class/User';
|
||||||
|
import NotificationMessage from '../class/NotificationMessage';
|
||||||
|
|
||||||
|
let run = async (message: NotificationMessage, connection: Connection) => {
|
||||||
|
if (!process.env.BOT_TOKEN) return; // Only run when client is enabled
|
||||||
|
if (!client.readyAt) await new Promise(r => client.once('ready', r)) as void; // Wait for client to log in
|
||||||
|
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
} catch(e) {
|
||||||
|
console.warn(e);
|
||||||
|
|
||||||
|
// If error didn't occur while sending an error message, attempt to send an error to the user.
|
||||||
|
if (!message.isError) {
|
||||||
|
let msg = new NotificationMessage()
|
||||||
|
.setTitle('Error during delivery')
|
||||||
|
.setBody(`\`\`\`js\n${e}\`\`\``);
|
||||||
|
|
||||||
|
run(msg.error(), connection)
|
||||||
|
.catch(console.warn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let init = async () => {
|
||||||
|
if (!process.env.BOT_TOKEN) return console.log('Discord: No token specified');
|
||||||
|
|
||||||
|
client.login(process.env.BOT_TOKEN);
|
||||||
|
client.on('ready', () => console.log(`Discord Bot: Logged in as ${client.user.tag}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { run, init };
|
109
src/index.ts
109
src/index.ts
|
@ -1,76 +1,95 @@
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
import Discord from 'discord.js';
|
|
||||||
import Enmap from 'enmap';
|
import Enmap from 'enmap';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import * as WebUntis from 'webuntis';
|
import * as WebUntis from 'webuntis';
|
||||||
|
import User from './class/User';
|
||||||
const untis = new WebUntis.default(
|
import NotificationMessage from './class/NotificationMessage';
|
||||||
process.env.SCHOOLNAME,
|
import ConnectionModule from './class/ConnectionModule';
|
||||||
process.env.USERNAME,
|
|
||||||
process.env.PASSWORD,
|
|
||||||
process.env.BASEURL
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Discord Bot: Logging in');
|
|
||||||
const client = new Discord.Client();
|
|
||||||
client.login(process.env.BOT_TOKEN);
|
|
||||||
client.on('ready', () => console.log(`Discord Bot: Logged in as ${client.user.tag}`));
|
|
||||||
|
|
||||||
const seenMessages = new Enmap({ name: "seenMessages" });
|
const seenMessages = new Enmap({ name: "seenMessages" });
|
||||||
const knownLessons = new Enmap({ name: "knownLessons" });
|
const knownLessons = new Enmap({ name: "knownLessons" });
|
||||||
|
|
||||||
const defaultEmbedColor = 0xFF9A00;
|
const usersDB = new Enmap({ name: "users" }) as Enmap<string|number, User>;
|
||||||
|
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
|
// Periodically attempt to connect to untis and fetch timetable updates
|
||||||
let fetchUpdates = () => {
|
let fetchUpdates = () => {
|
||||||
console.log('Untis: Logging in');
|
console.log(`Fetching ${untisInstances.length} users.`);
|
||||||
try {
|
untisInstances.forEach(({ untis, user }) => {
|
||||||
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);
|
|
||||||
try {
|
try {
|
||||||
let embed = new Discord.MessageEmbed()
|
// Create database entries
|
||||||
.setTitle('An error has occurred.')
|
if (!knownLessons.get(user.ID)) knownLessons.set(user.ID, {});
|
||||||
.setDescription(`\`\`\`js\n${e}\`\`\``)
|
if (!seenMessages.get(user.ID)) seenMessages.set(user.ID, {});
|
||||||
.setColor(0xff0000);
|
|
||||||
sendEmbed(embed);
|
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) {
|
} catch(e) {
|
||||||
console.error(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();
|
fetchUpdates();
|
||||||
setInterval(fetchUpdates, 60000);
|
setInterval(fetchUpdates, 60000);
|
||||||
|
|
||||||
|
|
||||||
async function sendEmbed(embed: Discord.MessageEmbed) {
|
async function sendMessage(message: NotificationMessage, user: User) {
|
||||||
const channel = await client.channels.fetch(process.env.CHANNEL) as Discord.DMChannel;
|
user.connections.forEach(async connection => {
|
||||||
if (!channel) throw `ERROR: Could not find channel`;
|
let path = require('path').join(__dirname, 'connections', connection.type + '.js') as string;
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
if (!embed.timestamp) embed.setTimestamp();
|
try {
|
||||||
channel.send(embed).catch(console.error);
|
require(path).run(message, connection);
|
||||||
|
} catch(e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Unknown connection type: ${connection.type}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
untis: untis,
|
untisInstances,
|
||||||
bot: client,
|
usersDB,
|
||||||
defaultEmbedColor: defaultEmbedColor,
|
sendEmbed: sendMessage,
|
||||||
sendEmbed: sendEmbed,
|
|
||||||
db: {
|
db: {
|
||||||
seenMessages: seenMessages,
|
seenMessages: seenMessages,
|
||||||
knownLessons: knownLessons
|
knownLessons: knownLessons
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Initialize clients in dist/connections/*
|
||||||
|
fs.readdirSync(`${__dirname}/connections`).filter(file => file.endsWith('.js')).forEach(file => {
|
||||||
|
(require(`${__dirname}/connections/${file}`) as ConnectionModule).init();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run tasks in dist/tasks/*
|
||||||
|
fs.readdirSync(`${__dirname}/tasks`).filter(file => file.endsWith('.js')).forEach(file =>
|
||||||
|
require(`${__dirname}/tasks/${file}`));
|
|
@ -1,23 +1,23 @@
|
||||||
import * as WebUntis from 'webuntis';
|
import * as WebUntis from 'webuntis';
|
||||||
import main from '../index';
|
import main from '../index';
|
||||||
import Discord from 'discord.js';
|
import User from '../class/User';
|
||||||
const { bot, defaultEmbedColor, untis, db, sendEmbed } = main;
|
import NotificationMessage from '../class/NotificationMessage';
|
||||||
|
const { db, sendEmbed } = main;
|
||||||
|
|
||||||
export async function run() {
|
export async function run(untis: WebUntis.default, user: User) {
|
||||||
let news = await untis.getNewsWidget(new Date(), true);
|
let news = await untis.getNewsWidget(new Date(), true);
|
||||||
if (!news) return;
|
if (!news) return;
|
||||||
|
|
||||||
news.messagesOfDay.forEach(message => {
|
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);
|
console.log('New message found: ' + message.text);
|
||||||
let embed = new Discord.MessageEmbed()
|
let notif = new NotificationMessage()
|
||||||
.setAuthor('Notification')
|
.setTitle('Notification')
|
||||||
.setColor(defaultEmbedColor)
|
|
||||||
.setTitle(message.subject)
|
.setTitle(message.subject)
|
||||||
.setDescription(message.text.replace(/\<br\>/g, "\n"))
|
.setBody(message.text.replace(/\<br\>/g, "\n"))
|
||||||
.setFooter(`Notification ID: ${message.id}`);
|
.setFooter(`Notification ID: ${message.id}`);
|
||||||
|
|
||||||
sendEmbed(embed);
|
sendEmbed(notif, user);
|
||||||
db.seenMessages.set(`${message.id}`, true);
|
db.seenMessages.set(user.ID, true, `${message.id}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -1,15 +1,16 @@
|
||||||
import * as WebUntis from 'webuntis';
|
import * as WebUntis from 'webuntis';
|
||||||
import main from '../index';
|
import main from '../index';
|
||||||
import Discord from 'discord.js';
|
import User from '../class/User';
|
||||||
const { bot, defaultEmbedColor, untis, db, sendEmbed } = main;
|
import NotificationMessage from '../class/NotificationMessage';
|
||||||
|
const { 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 timetable = await untis.getOwnTimetableForRange(new Date(Date.now() - 86400000), new Date(Date.now() + (86400000 * 7)), true);
|
||||||
|
|
||||||
let sentClasses = {};
|
let sentClasses = {};
|
||||||
|
|
||||||
timetable.forEach(lesson => {
|
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 (kLesson && hasChanged(lesson, kLesson)) {
|
||||||
if (sentClasses['' + (lesson.sg || lesson.su?.[0]?.id) + ' -- ' + lesson.date]) return;
|
if (sentClasses['' + (lesson.sg || lesson.su?.[0]?.id) + ' -- ' + lesson.date]) return;
|
||||||
let dateInfo = {
|
let dateInfo = {
|
||||||
|
@ -24,10 +25,8 @@ export async function run() {
|
||||||
|
|
||||||
let weekDay = ['Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
|
let weekDay = ['Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
|
||||||
|
|
||||||
let embed = new Discord.MessageEmbed()
|
let notif = new NotificationMessage()
|
||||||
.setTitle(`Lesson updated`)
|
.setTitle(`${weekDay}, ${date.getDate()}. ${date.getMonth() + 1}.: ${lesson.sg || '(Unnamed lesson)'} - ${lesson.te[0].longname || 'No teacher'}`);
|
||||||
.setColor(defaultEmbedColor)
|
|
||||||
.setAuthor(`${weekDay}, ${date.getDate()}. ${date.getMonth() + 1}.: ${lesson.sg || '(Unnamed lesson)'} - ${lesson.te[0].longname || 'No teacher'}`);
|
|
||||||
|
|
||||||
let desc = ``;
|
let desc = ``;
|
||||||
|
|
||||||
|
@ -69,15 +68,15 @@ export async function run() {
|
||||||
// Change the embed color when teacher ID is 0.
|
// Change the embed color when teacher ID is 0.
|
||||||
// Teacher ID 0 means that the class is canelled (at least on my school),
|
// 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.
|
// 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;
|
sentClasses['' + (lesson.sg || lesson.su?.[0]?.id) + ' -- ' + lesson.date] = true;
|
||||||
sendEmbed(embed);
|
sendEmbed(notif, user);
|
||||||
console.log(`Sent timetable update`);
|
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}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
46
src/tasks/cleanDB.ts
Normal file
46
src/tasks/cleanDB.ts
Normal file
|
@ -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
|
Loading…
Reference in a new issue