Initial commit

This commit is contained in:
ByteDream 2020-11-14 00:14:31 +01:00
commit e27bccc836
16 changed files with 2378 additions and 0 deletions

32
Dockerfile Normal file
View File

@ -0,0 +1,32 @@
FROM yobasystems/alpine-mariadb:latest
ENV token ""
ENV password "toor"
ENV encrypt ""
ENV MYSQL_DATABASE "Untis"
ENV MYSQL_ROOT_PASSWORD $password
RUN mkdir /untisbot-discord/ && \
mkdir /untisbot-discord/lib && \
mkdir /untisbot-discord/out && \
mkdir /untisbot-discord/src
RUN apk add --no-cache openjdk8 curl && \
rm -f /var/cache/apk/*
RUN wget -O /untisbot-discord/lib/logback-core.jar https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar && \
wget -O /untisbot-discord/lib/logback-classic.jar https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar && \
wget -O /untisbot-discord/lib/mariadb-java-client.jar https://downloads.mariadb.com/Connectors/java/connector-java-2.7.0/mariadb-java-client-2.7.0.jar && \
wget -O /untisbot-discord/lib/untis4j.jar $(curl -s https://api.github.com/repos/ByteDream/untis4j/releases/latest | grep "browser_download_url" | grep "withDependencies.jar" | cut -d '"' -f 4) && \
wget -O /untisbot-discord/lib/JDA.jar $(curl -s https://api.github.com/repos/DV8FromTheWorld/JDA/releases/latest | grep "browser_download_url" | grep "withDependencies-min.jar" | cut -d '"' -f 4)
ADD dockerfiles/run.sh /untisbot-discord/
ADD dockerfiles/database.sql /untisbot-discord/
ADD src/ /untisbot-discord/src
EXPOSE 3306
VOLUME ["/var/lib/mysql"]
ENTRYPOINT ["/untisbot-discord/run.sh"]

159
README.md Normal file
View File

@ -0,0 +1,159 @@
### UntisBot
**UntisBot** is a java programmed discord bot, which uses the [WebUntis](https://webuntis.com/) timetable software / api to automatically sends messages when the timetable from a given account or class changes.
You can invite the bot right [here](https://github.com/ByteDream/untisbot-discord/releases/tag/v1.0/UntisBot-1.0.jar) or [host it yourself](#Self-hosting).
## Commands
The default prefix for the bot is `!untis `, so you have to call every command with `!untis <command>`.
To see all available commands and get infos about it, simply type `help`
`channel` - In the channel where this command is entered, the bot shows the timetable changes | eg. `channel`.
`clear` - Clears the given untis data, given from the `data` command | eg. `clear`.
`data <username> <password> <login page url> [class name]` - Sets the data with which the bot logs in to untis and checks for timetable changes. The data is stored encrypted on the server.
`username` and `password` are the normal untis login data with which one also logs in to the untis website / app. To gain the login page url you have to go to webuntis.com, type in your school and choose it.
Then you will be redirected to the untis login page, The url of this page is the login page url, for example `https://example.webuntis.com/WebUntis/?school=myschool#/basic/main`.
`class name` is just the name of the class you want to check (eg. `12AB`). If `class name` is not specified, the bot tries to get the default class which is assigned to the given account.
eg. `data myname secure https://example.webuntis.com/WebUntis/?school=example#/basic/main 12AB`.
`help <command>` - Displays help to a given command | eg. `help data`.
`language <language>` - Changes the language in which the timetable information are displayed. Currently only 'de' (german) and 'en' (english) are supported | eg. `language de` | default: `en`.
`prefix <new prefix>` - Changes the prefix with which commands are called | eg. `prefix $` | default: `!untis `.
`stats` - Displays a message with some stats (total cancelled lessons, etc.) | eg. `stats`.
`<>` = required; `[]` = optional
Note: All commands except for `help <command>` and `<stats>` can only be executed by a member with admin rights.
## Self-hosting
If you want to host **UntisBot** on your own server / pc you have the choice between two types of hosting:
- Run the bot in a [docker container](#Docker)
- Run it [natively](#Natively) on your machine
## Docker
Download this repository with `git clone https://github.com/ByteDream/untisbot-discord.git` and go into the cloned directory.
Then run `docker build -t untisbot-discord .` to build the docker image and if this is done, type `docker run -d --name untisbot-discord -e token=<your discord token> untisbot-discord` to run it.
Note: You can declare more [environment variables](#Run-options-for-docker-container) besides `token`.
## Natively
When you run the bot natively you can choose from 2 types of data storage:
- [In-memory](#In-memory-storage) (simpler)
- [Database storage](#MariaDB) (MariaDB)
### In-memory storage
In memory data storage is pretty simple: Just download the [jar]() and run it with `java -jar UntisBot-1.0.jar token=<your discord bot token>`.
The simple things have unfortunately also often disadvantages: The user data is only stored as long as the bot is running. If you shut it down, all data will be lost.
If you want to keep the data even after a shutdown, you should use [database storage](#MariaDB).
### MariaDB
**_Note_: This description is only for linux, but the most things should also work on windows**
With MariaDB you can store the data safely in a sql database, and they won't be lost after a shutdown.
If you haven't installed MariaDB, you can follow the instructions from [here](https://linuxize.com/post/how-to-install-mariadb-on-ubuntu-18-04/) (this tutorial is for ubuntu, but it should work with every debian distro).
To set up the database, you have two options to choose from.
##### The short one:
```bash
mysql --user=<user> --password=<password> -e "CREATE DATABASE Untis;" && https://raw.githubusercontent.com/ByteDream/untisbot-discord/master/src/org/bytedream/untisbot/dockerfiles/database.sql | mysql --user=<user> --password=<password> Untis
```
Just copy this and replace `<user>` with the sql user which should manage the database and `<password>` with the user's password.
---
##### And the long one:
First you have to connect you with MariaDB. When you are connected, enter the following commands (without the '>'):
```sql
> CREATE DATABASE Untis;
> USE Untis;
> CREATE TABLE Guilds (GUILDID BIGINT NOT NULL, LANGUAGE TINYTEXT, USERNAME TINYTEXT, PASSWORD TEXT, SERVER TINYTEXT, SCHOOL TINYTEXT, KLASSEID SMALLINT, CHANNELID BIGINT, PREFIX VARCHAR(7) DEFAULT '!untis ' NOT NULL, SLEEPTIME BIGINT DEFAULT 3600000 NOT NULL, ISCHECKACTIVE BOOLEAN DEFAULT FALSE NOT NULL, LASTCHECKED DATE);
> CREATE TABLE Stats (GUILDID BIGINT NOT NULL, TOTALREQUESTS INT DEFAULT 0 NOT NULL, TOTALDAYS SMALLINT DEFAULT 0 NOT NULL, TOTALLESSONS INT DEFAULT 0 NOT NULL, TOTALCANCELLEDLESSONS SMALLINT DEFAULT 0 NOT NULL, TOTALMOVEDLESSONS SMALLINT DEFAULT 0 NOT NULL, AVERAGECANCELLEDLESSONS FLOAT DEFAULT 0 NOT NULL, AVERAGEMOVEDLESSONS FLOAT DEFAULT 0 NOT NULL);
> CREATE TABLE AbsentTeachers (GUILDID BIGINT NOT NULL, TEACHERNAME TINYTEXT NOT NULL, ABSENTLESSONS SMALLINT NOT NULL);
```
---
Now you have set up the database and are ready to go. Download the [jar]() and run it with `java -jar UntisBot-1.0.jar <your discord bot token> mariadb`.
## Run options
The syntax of the following arguments / run option is very simple: `key=value`.
### Run options for docker container
When you start the container you can declare several environment variables:
- `token` (required!) - The discord bot token
- `password` (optional) - Password for the given user | default: `toor`
- `encrypt` (optional) - A password to encrypt the user's untis username and password | default: `password`
(always remember when declaring a new environment variable `-e` must be prefixed)
---
Example:
- `docker run -d -e token=BLySFrzvz3tAHtquQevY1FF5W8CT0UMyMNmCSUCbJAPdNAmnnqYVBzaPTkz -e password=very_secure untisbot-discord`
### Run options for native hosting
There are several arguments to start the bot with:
- `token` (required!) - The discord bot token
- `encrypt` (optional) - A password to encrypt the user's untis username and password | default: `password`
- `lng` (optional) - Path to a language file | default: `` (uses the [internal](src/org/bytedream/untisbot/language.json) language file)
The following arguments are only for MariaDB user:
- `user` (optional) - The user who should connect to the mariadb database | default: `root`
- `password` (optional) - Password for the given mariadb user | default: ``
- `port` (optional) - Port of mariadb | default: `3306`
- `ip` (optional) - IP address of mariadb | default: `127.0.0.1`
If you want to use MariaDB as store type you have to add the argument `mariadb` (without any value).
---
Alternatively, you can write the arguments in a `json` file and load this via `java -jar UntisBot-1.0.jar file=<file where the arguments are in>`
Example:
```json
{
"token": "BLySFrzvz3tAHtquQevY1FF5W8CT0UMyMNmCSUCbJAPdNAmnnqYVBzaPTkz",
"lng": "lng.json"
}
```
If you use this, you can still declare arguments on the command line, but if they are also in the json file, they will be overwritten.
This might be useful when you run the bot on a server and won't that your token or other args are shown when, for example, [htop](https://github.com/htop-dev/htop/) is running where you can see the cli arguments.
---
In-memory examples:
- `UntisBot-1.0.jar token=BLySFrzvz3tAHtquQevY1FF5W8CT0UMyMNmCSUCbJAPdNAmnnqYVBzaPTkz`
- `UntisBot-1.0.jar token=BLySFrzvz3tAHtquQevY1FF5W8CT0UMyMNmCSUCbJAPdNAmnnqYVBzaPTkz encrypt=super_secure_password lng=/home/user/more_languages.json`
MariaDB examples:
- `UntisBot-1.0.jar mariadb token=BLySFrzvz3tAHtquQevY1FF5W8CT0UMyMNmCSUCbJAPdNAmnnqYVBzaPTkz encrypt=super_ultra_secure_password`
- `UntisBot-1.0.jar mariadb token=BLySFrzvz3tAHtquQevY1FF5W8CT0UMyMNmCSUCbJAPdNAmnnqYVBzaPTkz encrypt=super_ultra_secure_password user=untis password=toor`
## Dependencies
- Java 8 or higher
- [Discord library](https://github.com/DV8FromTheWorld/JDA) (JDA)
- [Untis library](https://github.com/ByteDream/untis4j) (untis4j)
- [MariaDB client](https://github.com/mariadb-corporation/mariadb-connector-j) (mariadb java client)
- [Logger](https://github.com/qos-ch/logback) (logback-core and logback-classic)
**_Note_: The [UntisBot jar file](https://github.com/ByteDream/untisbot-discord/releases/tag/v1.0/UntisBot-1.0.jar) and the [Dockerfile](Dockerfile) are containing all dependencies.**

31
dockerfiles/database.sql Normal file
View File

@ -0,0 +1,31 @@
CREATE TABLE IF NOT EXISTS `AbsentTeachers` (
`GUILDID` bigint(20) NOT NULL,
`TEACHERNAME` tinytext NOT NULL,
`ABSENTLESSONS` smallint(6) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `Guilds` (
`GUILDID` bigint(20) NOT NULL,
`LANGUAGE` tinytext DEFAULT NULL,
`USERNAME` tinytext DEFAULT NULL,
`PASSWORD` text DEFAULT NULL,
`SERVER` tinytext DEFAULT NULL,
`SCHOOL` tinytext DEFAULT NULL,
`KLASSEID` smallint(6),
`CHANNELID` bigint(20) DEFAULT NULL,
`PREFIX` varchar(7) NOT NULL DEFAULT '!untis ',
`SLEEPTIME` bigint(20) NOT NULL DEFAULT 3600000,
`ISCHECKACTIVE` tinyint(1) NOT NULL DEFAULT 0,
`LASTCHECKED` date DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `Stats` (
`GUILDID` bigint(20) NOT NULL,
`TOTALREQUESTS` int(11) NOT NULL DEFAULT 0,
`TOTALDAYS` smallint(6) NOT NULL DEFAULT 0,
`TOTALLESSONS` int(11) NOT NULL DEFAULT 0,
`TOTALCANCELLEDLESSONS` smallint(6) NOT NULL DEFAULT 0,
`TOTALMOVEDLESSONS` smallint(6) NOT NULL DEFAULT 0,
`AVERAGECANCELLEDLESSONS` float NOT NULL DEFAULT 0,
`AVERAGEMOVEDLESSONS` float NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

14
dockerfiles/run.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/sh
/scripts/run.sh &
sleep 10
mariadb --user=root --password="$MYSQL_ROOT_PASSWORD" -h 127.0.0.1 Untis < "/untisbot-discord/database.sql"
/usr/lib/jvm/java-1.8-openjdk/bin/javac -cp "/untisbot-discord/lib/*" $(find /untisbot-discord/src/ -name '*.java')
cp -r /untisbot-discord/src/* /untisbot-discord/out/
rm -r $(find /untisbot-discord/out/ -name '*.java')
java -Dfile.encoding=UTF-8 -cp "/untisbot-discord/out:/untisbot-discord/lib/*" org.bytedream.untisbot.Main mariadb token=$token user=root password=$MYSQL_ROOT_PASSWORD encrypt=$encrypt

View File

@ -0,0 +1,87 @@
package org.bytedream.untisbot;
import javax.crypto.*;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
/**
* Class to en- / decrypt strings {@see https://github.com/ByteDream/cryptoGX}
*
* @version 1.0
* @since 1.0
*/
public class Crypt {
private final String key;
private final String secretKeyFactoryAlgorithm = "PBKDF2WithHmacSHA512";
private final int keySize = 256;
private final int iterations = 65536;
public Crypt(String key) {
this.key = key;
}
/**
* Generates a new secret key for en- / decryption
*
* @return the secret key
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @since 1.0
*/
private byte[] createSecretKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKeyFactoryAlgorithm);
PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray(), new byte[16], iterations, keySize);
return factory.generateSecret(keySpec).getEncoded();
}
/**
* Encrypts a given string
*
* @param string string to encrypt
* @return the encrypted string
* @throws BadPaddingException
* @throws NoSuchAlgorithmException
* @throws IllegalBlockSizeException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws InvalidKeySpecException
* @since 1.0
*/
public String encrypt(String string) throws BadPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, NoSuchPaddingException, InvalidKeyException, InvalidKeySpecException {
Key secretKey = new SecretKeySpec(createSecretKey(), "AES");
Cipher encryptCipher = Cipher.getInstance("AES");
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey);
return Base64.getEncoder().encodeToString(encryptCipher.doFinal(string.getBytes(StandardCharsets.UTF_8)));
}
/**
* Decrypts a given string
*
* @param string string to decrypt
* @return the decypted string
* @throws BadPaddingException
* @throws IllegalBlockSizeException
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws InvalidKeyException
*/
public String decrypt(String string) throws BadPaddingException, IllegalBlockSizeException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
Key secretKey = new SecretKeySpec(createSecretKey(), "AES");
Cipher decryptCipher = Cipher.getInstance("AES");
decryptCipher.init(Cipher.DECRYPT_MODE, secretKey);
return new String(decryptCipher.doFinal(Base64.getDecoder().decode(string)), StandardCharsets.UTF_8);
}
}

View File

@ -0,0 +1,201 @@
package org.bytedream.untisbot;
import ch.qos.logback.classic.Logger;
import org.bytedream.untisbot.data.StoreType;
import org.bytedream.untisbot.discord.Discord;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.slf4j.LoggerFactory;
import javax.security.auth.login.LoginException;
import java.io.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
/**
* Main class
*/
public class Main {
private static Logger logger;
private static Connection connection;
public static void main(String[] args) throws ClassNotFoundException, SQLException, LoginException {
String os = System.getProperty("os.name").toLowerCase();
File logFile;
if (os.contains("linux") || os.contains("unix")) {
logFile = new File("/var/log/untis.log");
} else {
logFile = new File("untis.log");
}
if (!logFile.exists()) {
try {
logFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
System.setProperty("LOG_FILE", logFile.getAbsolutePath());
logger = (Logger) LoggerFactory.getLogger("Untis");
Discord discord;
String token = null;
StoreType storeType = StoreType.MEMORY;
String dataEncryptPassword = "password";
String user = "root";
String password = "";
String databaseIP = "127.0.0.1";
String languageFile = "";
int databasePort = 3306;
String argsFile = Arrays.stream(args).filter(s -> s.trim().toLowerCase().startsWith("file=")).findAny().orElse(null);
if (argsFile != null) {
FileInputStream configReader;
try {
configReader = new FileInputStream(argsFile.substring(5));
} catch (FileNotFoundException e) {
e.printStackTrace();
return;
}
HashSet<String> argsAsSet = new HashSet<>(Arrays.asList(args));
JSONTokener jsonTokener = new JSONTokener(configReader);
JSONObject jsonObject = new JSONObject(jsonTokener);
for (String s : jsonObject.keySet()) {
argsAsSet.add(s + "=" + jsonObject.getString(s));
}
args = argsAsSet.toArray(new String[0]);
}
for (String arg : args) {
try {
String[] realArgs = arg.trim().split("=");
String realValue = realArgs[1].trim();
switch (realArgs[0].trim().toLowerCase()) {
case "token":
token = realValue;
break;
case "user":
user = realValue;
logger.info("Set custom database user");
break;
case "password":
password = realValue;
logger.info("Set custom database password");
break;
case "ip":
if (!Utils.isIPValid(realValue)) {
System.err.println("IP is not valid");
return;
} else {
databaseIP = realValue;
logger.info("Set custom database ip");
}
break;
case "port":
try {
databasePort = Integer.parseInt(realValue);
logger.info("Set custom database port");
} catch (NumberFormatException e) {
System.err.println(realValue + " is not a number");
return;
}
case "encrypt":
dataEncryptPassword = realValue;
logger.info("Set custom database encrypt password");
break;
case "lng":
File file = new File(realValue);
if (!file.exists()) {
System.err.println("The file '" + realValue + "' doesn't exists");
return;
}
if (!file.isFile()) {
System.err.println("'" + realValue + "' must be a file");
return;
}
languageFile = realValue;
logger.info("Set custom language file");
}
} catch (ArrayIndexOutOfBoundsException ignore) {
if (arg.trim().toLowerCase().equals("mariadb")) {
storeType = StoreType.MARIADB;
logger.info("Using mariadb for data storage");
}
}
}
if (token == null) {
System.err.println("Token is missing. Run me again and use your discord bot token as argument (e.g. token=BLySFrzvz3tAHtquQevY1FF5W8CT0UMyMNmCSUCbJAPdNAmnnqYVBzaPTkz)");
return;
}
if (storeType == StoreType.MARIADB) {
Class.forName("org.mariadb.jdbc.Driver");
String finalDatabaseIP = databaseIP;
int finalDatabasePort = databasePort;
String finalUser = user;
String finalPassword = password;
connection = DriverManager.getConnection(Utils.advancedFormat("jdbc:mariadb://{databaseIP}:{databasePort}/Untis?user={user}&password={password}", new HashMap<String, Object>() {{
put("databaseIP", finalDatabaseIP);
put("databasePort", finalDatabasePort);
put("user", finalUser);
put("password", finalPassword);
}}));
logger.info("Connected to mariadb");
}
InputStream languageFileReader;
if (languageFile.isEmpty()) {
languageFileReader = Main.class.getResourceAsStream("language.json");
if (languageFileReader == null) {
System.err.println("Cannot load internal language file");
return;
}
} else {
try {
languageFileReader = new FileInputStream(languageFile);
} catch (FileNotFoundException e) {
e.printStackTrace();
return;
}
}
JSONTokener jsonTokener = new JSONTokener(languageFileReader);
discord = new Discord(token, storeType, dataEncryptPassword, new JSONObject(jsonTokener));
discord.start();
logger.info("Started bot");
//https://discord.com/api/oauth2/authorize?client_id=768841979433451520&permissions=268437504&scope=bot
}
/**
* Returns the logger
*
* @return the logger
* @since 1.0
*/
public static Logger getLogger() {
return logger;
}
/**
* Returns the database connection
*
* @return the database connection
* @since 1.0
*/
public static Connection getConnection() {
return connection;
}
}

View File

@ -0,0 +1,79 @@
package org.bytedream.untisbot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
public class Utils {
/**
* An alternative way to format a string
*
* @param stringToFormat string that should be formatted
* @param args args to format the string
* @return the formatted string
* @since 1.0
*/
public static String advancedFormat(String stringToFormat, Map<String, Object> args) {
for (Map.Entry<String, Object> entry : args.entrySet()) {
stringToFormat = stringToFormat.replace("{" + entry.getKey() + "}", entry.getValue().toString());
}
return stringToFormat;
}
/**
* Creates a new logger
*
* @return the logger
* @since 1.0
*/
public static Logger createLogger() {
return LoggerFactory.getLogger("root");
}
/**
* Checks a given ip for its validity
*
* @param ip ip to check
* @return if the ip is valid
* @since 1.0
*/
public static boolean isIPValid(String ip) {
if (ip == null || ip.isEmpty()) {
return false;
}
String[] parts = ip.split("\\.");
if (parts.length != 4 || ip.startsWith(".") || ip.endsWith(".")) {
return false;
}
for (String s : parts) {
try {
int i = Integer.parseInt(s);
if (i < 0 || i > 255) {
return false;
}
} catch (NumberFormatException e) {
return false;
}
}
return true;
}
/**
* Rounds numbers to a given decimal place
*
* @param value number to round
* @param decimalPoints decimal places to round
* @return the rounded number
* @since 1.0
*/
public static double round(double value, int decimalPoints) {
double d = Math.pow(10, decimalPoints);
return Math.rint(value * d) / d;
}
}

View File

@ -0,0 +1,163 @@
package org.bytedream.untisbot.data;
import org.bytedream.untisbot.Crypt;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.time.LocalDate;
import java.util.HashMap;
/**
* Class to store given guild / user data
*
* @version 1.0
* @since 1.0
*/
public class Data {
/**
* Class to store guild data
*
* @version 1.0
* @since 1.0
*/
public static class Guild {
private final Crypt crypt;
private Object[] data;
public Guild(Object[] data, Crypt crypt) {
this.data = data;
this.crypt = crypt;
}
public Object[] getData() {
return data;
}
public long getGuildId() {
return (long) data[0];
}
public String getLanguage() {
return (String) (data[1]);
}
public String getUsername() {
try {
return crypt.decrypt((String) (data[2]));
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidKeySpecException ignore) {
return null;
}
}
public String getPassword() {
try {
return crypt.decrypt((String) (data[3]));
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidKeySpecException ignore) {
return null;
}
}
public String getServer() {
return (String) data[4];
}
public String getSchool() {
return (String) data[5];
}
public Short getKlasseId() {
return (short) data[6];
}
public Long getChannelId() {
return (Long) data[7];
}
public String getPrefix() {
return (String) data[8];
}
public long getSleepTime() {
return (long) data[9];
}
public boolean isCheckActive() {
return (boolean) data[10];
}
public LocalDate getLastChecked() {
return (LocalDate) data[11];
}
protected void update(Object[] data) {
this.data = data;
}
}
/**
* Class to store guild stats
*
* @version 1.0
* @since 1.0
*/
public static class Stats {
private Object[] data;
public Stats(Object[] data) {
this.data = data;
}
public Object[] getData() {
return data;
}
public long getGuildId() {
return (long) data[0];
}
public int getTotalRequests() {
return (int) data[1];
}
public short getTotalDays() {
return (short) data[5];
}
public int getTotalLessons() {
return (int) data[3];
}
public short getTotalCancelledLessons() {
return (short) data[5];
}
public short getTotalMovedLessons() {
return (short) data[5];
}
public float getAverageCancelledLessonsPerWeek() {
return (float) data[6];
}
public float getAverageMovedLessonsPerWeek() {
return (float) data[7];
}
public HashMap<String, Short> getAbsentTeachers() {
return (HashMap<String, Short>) data[8];
}
protected void update(Object[] data) {
this.data = data;
}
}
}

View File

@ -0,0 +1,571 @@
package org.bytedream.untisbot.data;
import org.bytedream.untisbot.Crypt;
import org.bytedream.untisbot.Main;
import java.security.GeneralSecurityException;
import java.sql.*;
import java.sql.Date;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* Base class to manage all data
*
* @version 1.0
* @since 1.0
*/
public class DataConnector {
private final StoreType storeType;
private final Crypt crypt;
public DataConnector(StoreType storeType, Crypt crypt) {
this.storeType = storeType;
this.crypt = crypt;
}
public Guild guildConnector() {
return new Guild(storeType, crypt);
}
public Stats statsConnector() {
return new Stats(storeType);
}
/**
* Class to manage all the guild data
*
* @version 1.0
* @since 1.0
*/
public static class Guild {
private final StoreType storeType;
private final Crypt crypt;
private final Map<Long, Data.Guild> memoryData = new HashMap<>();
private Connection connection;
/**
* Initializes the guild data connector and connects to the database if {@code storeType} is database
*
* @param storeType type how to store the given untis data {@link StoreType}
* @param crypt {@link Crypt} class to en- / decrypt the untis account passwords
* @since 1.0
*/
private Guild(StoreType storeType, Crypt crypt) {
this.storeType = storeType;
this.crypt = crypt;
if (storeType == StoreType.MARIADB) {
connection = Main.getConnection();
}
}
/**
* Creates a new guild data entry
*
* @param guildId guild id of the new entry
* @since 1.0
*/
public void add(long guildId) {
if (storeType == StoreType.MARIADB) {
try {
connection.createStatement().executeUpdate("INSERT INTO Guilds (GUILDID) VALUES (" + guildId + ")");
} catch (SQLException e) {
e.printStackTrace();
}
} else {
Object[] data = new Object[12];
data[0] = guildId;
data[1] = null;
data[2] = null;
data[3] = null;
data[4] = null;
data[5] = null;
data[6] = null;
data[7] = null;
data[8] = "!untis ";
data[9] = 3600000L;
data[10] = false;
data[11] = null;
memoryData.put(guildId, new Data.Guild(data, crypt));
}
}
/**
* Returns the guild data from a guild id
*
* @param guildId to get the data from
* @return the guild data
* @since 1.0
*/
public Data.Guild get(long guildId) {
Object[] data = new Object[12];
if (storeType == StoreType.MARIADB) {
try {
ResultSet resultSet = connection.createStatement().executeQuery("SELECT * FROM Guilds WHERE GUILDID=" + guildId);
while (resultSet.next()) {
ResultSetMetaData metaData = resultSet.getMetaData();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
switch (metaData.getColumnType(i)) {
case 5: //small int
data[i - 1] = resultSet.getShort(i);
break;
case 91: //date
Date date = resultSet.getDate(i);
if (date != null) {
data[i - 1] = date.toLocalDate();
} else {
data[i - 1] = null;
}
break;
default:
data[i - 1] = resultSet.getObject(i);
}
}
}
} catch (SQLException e) {
e.printStackTrace();
return null;
}
} else {
data = memoryData.get(guildId).getData();
}
return new Data.Guild(data, crypt);
}
/**
* Returns all stored guild data
*
* @return all stored guild data
* @since 1.0
*/
public HashSet<Data.Guild> getAll() {
HashSet<Data.Guild> allData = new HashSet<>();
if (storeType == StoreType.MARIADB) {
try {
ResultSet resultSet = connection.createStatement().executeQuery("SELECT * FROM Guilds");
while (resultSet.next()) {
Object[] data = new Object[12];
int maxColumns = resultSet.getMetaData().getColumnCount();
for (int i = 1; i <= maxColumns; i++) {
Object object = resultSet.getObject(i);
data[i - 1] = object;
}
allData.add(new Data.Guild(data, crypt));
}
} catch (SQLException e) {
e.printStackTrace();
}
} else {
allData.addAll(memoryData.values());
}
return allData;
}
/**
* Updates the guild data for a specific guild id
*
* @param guildId guild id from which the data should be updated
* @param language new language in which the timetable changes should be displayed
* @param username new untis username
* @param password new untis password
* @param server new untis server
* @param school new untis school
* @param channelId new channel id in which the timetable changes are sent
* @param prefix new command prefix
* @param sleepTime new sleep time between every timetable check
* @param isCheckActive new boolean to say if the timetable should be checked
* @param lastChecked new date on which the timetable was last checked
* @since 1.0
*/
public void update(long guildId, String language, String username, String password, String server, String school, Short klasseId, Long channelId, String prefix, Long sleepTime, Boolean isCheckActive, LocalDate lastChecked) {
LinkedHashMap<String, Object> args = new LinkedHashMap<>();
args.put("GUILDID", guildId);
args.put("LANGUAGE", language);
if (username != null) {
if (username.isEmpty()) {
args.put("USERNAME", "NULL");
} else {
try {
args.put("USERNAME", crypt.encrypt(username));
} catch (GeneralSecurityException ignore) {
args.put("USERNAME", null);
}
}
} else {
args.put("USERNAME", null);
}
if (password != null) {
if (password.isEmpty()) {
args.put("PASSWORD", "NULL");
} else {
try {
args.put("PASSWORD", crypt.encrypt(password));
} catch (GeneralSecurityException ignore) {
args.put("PASSWORD", null);
}
}
} else {
args.put("PASSWORD", null);
}
if (server != null) {
if (server.isEmpty()) {
args.put("SERVER", "NULL");
} else {
args.put("SERVER", server);
}
} else {
args.put("SERVER", null);
}
if (school != null) {
if (school.isEmpty()) {
args.put("SCHOOL", "NULL");
} else {
args.put("SCHOOL", school);
}
} else {
args.put("SCHOOL", null);
}
args.put("KLASSEID", klasseId);
args.put("CHANNELID", channelId);
args.put("PREFIX", prefix);
args.put("SLEEPTIME", sleepTime);
args.put("ISCHECKACTIVE", isCheckActive);
args.put("LASTCHECKED", lastChecked);
if (storeType == StoreType.MARIADB) {
StringBuilder stringBuilder = new StringBuilder("UPDATE Guilds SET ");
for (Map.Entry<String, Object> entry : args.entrySet()) {
Object value = entry.getValue();
if (value != null) {
if (String.class.isAssignableFrom(value.getClass())) {
stringBuilder.append(entry.getKey()).append("='").append((String) value).append("',");
} else if (LocalDate.class.isAssignableFrom(value.getClass())) {
stringBuilder.append(entry.getKey()).append("='").append(((LocalDate) value).format(DateTimeFormatter.ISO_LOCAL_DATE)).append("',");
} else {
stringBuilder.append(entry.getKey()).append("=").append(value).append(",");
}
}
}
String preFinalQuery = stringBuilder.toString();
preFinalQuery = preFinalQuery.substring(0, preFinalQuery.length() - 1);
try {
connection.createStatement().executeUpdate(preFinalQuery + " WHERE GUILDID=" + guildId);
} catch (SQLException e) {
e.printStackTrace();
}
} else {
Object[] data = memoryData.get(guildId).getData();
Iterator<Object> iterator = args.values().iterator();
int index = 0;
while (iterator.hasNext()) {
Object o = iterator.next();
if (o != null) {
data[index] = o;
}
index++;
}
memoryData.replace(guildId, new Data.Guild(data, crypt));
}
}
/**
* Checks if the given guild id exist in the guild data
*
* @param guildId to check
* @return if the guild id exists
* @since 1.0
*/
public boolean has(long guildId) {
if (storeType == StoreType.MARIADB) {
try {
return connection.createStatement().executeQuery("SELECT GUILDID FROM Guilds WHERE GUILDID=" + guildId).first();
} catch (SQLException e) {
e.printStackTrace();
return true;
}
} else {
return memoryData.containsKey(guildId);
}
}
/**
* Removes a guild data entry
*
* @param guildId guild id of the entry to be removed
* @since 1.0
*/
public void remove(long guildId) {
if (storeType == StoreType.MARIADB) {
try {
connection.createStatement().executeUpdate("DELETE FROM Guilds WHERE GUILDID=" + guildId);
} catch (SQLException e) {
e.printStackTrace();
}
} else {
memoryData.remove(guildId);
}
}
}
/**
* Class to manage all the guild stats
*
* @version 1.0
* @since 1.0
*/
public static class Stats {
private final StoreType storeType;
private final Map<Long, Data.Stats> memoryData = new HashMap<>();
private Connection connection;
/**
* Initializes the stats data connector and connects to the database if {@code storeType} is database
*
* @param storeType type how to store the given untis data {@link StoreType}
* @since 1.0
*/
private Stats(StoreType storeType) {
this.storeType = storeType;
if (storeType == StoreType.MARIADB) {
connection = Main.getConnection();
}
}
/**
* Creates a new stats data entry
*
* @param guildId guild id of the new entry
* @since 1.0
*/
public void add(long guildId) {
if (storeType == StoreType.MARIADB) {
try {
connection.createStatement().executeUpdate("INSERT INTO Stats (GUILDID) VALUES (" + guildId + ");");
} catch (SQLException e) {
e.printStackTrace();
}
} else {
Object[] data = new Object[10];
data[0] = guildId;
data[1] = 0;
data[2] = 0;
data[3] = 0;
data[4] = (short) 0;
data[5] = (short) 0;
data[6] = 0f;
data[7] = 0f;
data[8] = new HashMap<String, Short>();
memoryData.put(guildId, new Data.Stats(data));
}
}
/**
* Returns the stats data from a guild id
*
* @param guildId to get the data from
* @return the stats data
* @since 1.0
*/
public Data.Stats get(long guildId) {
if (storeType == StoreType.MARIADB) {
Object[] data = new Object[9];
try {
ResultSet resultSet = connection.createStatement().executeQuery("SELECT * FROM Stats WHERE GUILDID=" + guildId);
while (resultSet.next()) {
ResultSetMetaData metaData = resultSet.getMetaData();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
switch (metaData.getColumnType(i)) {
case 5: //small int
data[i - 1] = resultSet.getShort(i);
break;
case 6: //float
data[i - 1] = resultSet.getFloat(i);
break;
default:
data[i - 1] = resultSet.getObject(i);
}
}
}
resultSet = connection.createStatement().executeQuery("SELECT * FROM AbsentTeachers WHERE GUILDID=" + guildId);
HashMap<String, Short> absentTeachers = new HashMap<>();
while (resultSet.next()) {
absentTeachers.put(resultSet.getString("TEACHERNAME"), resultSet.getShort("ABSENTLESSONS"));
}
data[0] = guildId;
data[8] = absentTeachers;
return new Data.Stats(data);
} catch (SQLException e) {
e.printStackTrace();
return null;
}
} else {
return memoryData.get(guildId);
}
}
/**
* Updates the stats data for a specific guild id
*
* @param guildId guild id from which the data should be updated
* @param totalRequests new total timetable requests
* @param totalDays new total days that have been checked
* @param totalLessons new total lessons that have been checked
* @param totalCancelledLessons new total cancelled lessons that have been checked
* @param totalMovedLessons new total moved lessons that have been checked
* @param averageCancelledLessonsPerWeek new average cancelled lessons per week
* @param averageMovedLessonsPerWeek new average moved lessons per week
* @since 1.0
*/
public void update(long guildId, Integer totalRequests, Short totalDays, Integer totalLessons, Short totalCancelledLessons, Short totalMovedLessons, Float averageCancelledLessonsPerWeek, Float averageMovedLessonsPerWeek) {
LinkedHashMap<String, Object> args = new LinkedHashMap<>();
args.put("GUILDID", guildId);
args.put("TOTALREQUESTS", totalRequests);
args.put("TOTALDAYS", totalDays);
args.put("TOTALLESSONS", totalLessons);
args.put("TOTALCANCELLEDLESSONS", totalCancelledLessons);
args.put("TOTALMOVEDLESSONS", totalMovedLessons);
args.put("AVERAGECANCELLEDLESSONS", averageCancelledLessonsPerWeek);
args.put("AVERAGEMOVEDLESSONS", averageMovedLessonsPerWeek);
if (storeType == StoreType.MARIADB) {
String[] argsClasses = new String[]{"Long", "Integer", "Short", "Integer", "Short", "Short", "Float", "Float"};
StringBuilder stringBuilder = new StringBuilder("UPDATE Stats SET ");
int index = 0;
for (Map.Entry<String, Object> entry : args.entrySet()) {
Object value = entry.getValue();
if (value != null) {
switch (argsClasses[index]) {
case "Float":
if (Float.isNaN((Float) value)) {
value = 0f;
}
case "Integer":
case "Short":
stringBuilder.append(entry.getKey()).append("=").append(value).append(",");
break;
}
}
index++;
}
String preFinalQuery = stringBuilder.toString();
preFinalQuery = preFinalQuery.substring(0, preFinalQuery.length() - 1);
try {
connection.createStatement().executeUpdate(preFinalQuery + " WHERE GUILDID=" + guildId);
} catch (SQLException e) {
e.printStackTrace();
}
} else {
Data.Stats stats = memoryData.get(guildId);
Object[] data = stats.getData();
Iterator<Object> iterator = args.values().iterator();
int index = 0;
while (iterator.hasNext()) {
Object o = iterator.next();
if (o != null) {
data[index] = o;
}
index++;
}
data[9] = stats.getAbsentTeachers();
memoryData.replace(guildId, new Data.Stats(data));
}
}
/**
* Updates the absent teachers data for a specific guild id
*
* @param guildId guild id from which the data should be updated
* @param teacherName teacher name that should be updated
* @param absentLessons new number of lessons where the teacher were absent
* @since 1.0
*/
public void updateAbsentTeachers(long guildId, String teacherName, short absentLessons) {
if (storeType == StoreType.MARIADB) {
try {
if (!connection.createStatement().executeQuery("SELECT GUILDID FROM AbsentTeachers WHERE GUILDID=" + guildId + " AND TEACHERNAME='" + teacherName + "'").first()) {
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO AbsentTeachers (GUILDID, TEACHERNAME, ABSENTLESSONS) VALUES (?, ?, ?)");
preparedStatement.setLong(1, guildId);
preparedStatement.setString(2, teacherName);
preparedStatement.setShort(3, absentLessons);
preparedStatement.executeUpdate();
} else {
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE AbsentTeachers SET ABSENTLESSONS=? WHERE GUILDID=? AND TEACHERNAME=?");
preparedStatement.setShort(1, absentLessons);
preparedStatement.setLong(2, guildId);
preparedStatement.setString(3, teacherName);
preparedStatement.executeUpdate();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
} else {
memoryData.get(guildId).getAbsentTeachers().computeIfPresent(teacherName, (s, aShort) -> memoryData.get(guildId).getAbsentTeachers().put(s, aShort));
memoryData.get(guildId).getAbsentTeachers().putIfAbsent(teacherName, absentLessons);
}
}
/**
* Checks if the given guild id exist in the stats data
*
* @param guildId to check
* @return if the guild id exists
* @since 1.0
*/
public boolean has(long guildId) {
if (storeType == StoreType.MARIADB) {
try {
return connection.createStatement().executeQuery("SELECT GUILDID FROM Stats WHERE GUILDID=" + guildId).first();
} catch (SQLException e) {
e.printStackTrace();
return false;
}
} else {
return memoryData.containsKey(guildId);
}
}
/**
* Removes a guild data entry
*
* @param guildId guild id of the entry to be removed
* @since 1.0
*/
public void remove(long guildId) {
if (storeType == StoreType.MARIADB) {
try {
connection.createStatement().executeUpdate("DELETE FROM Stats WHERE GUILDID=" + guildId);
} catch (SQLException e) {
e.printStackTrace();
}
try {
connection.createStatement().executeUpdate("DELETE FROM AbsentTeachers WHERE GUILDID=" + guildId);
} catch (SQLException e) {
e.printStackTrace();
}
} else {
memoryData.remove(guildId);
}
}
}
}

View File

@ -0,0 +1,12 @@
package org.bytedream.untisbot.data;
/**
* Simple enum to differ between store types
*
* @version 1.0
* @since 1.0
*/
public enum StoreType {
MARIADB,
MEMORY
}

View File

@ -0,0 +1,43 @@
package org.bytedream.untisbot.discord;
import net.dv8tion.jda.api.JDABuilder;
import org.bytedream.untisbot.Crypt;
import org.bytedream.untisbot.data.StoreType;
import org.json.JSONObject;
import javax.security.auth.login.LoginException;
/**
* Base class to start the bot
*
* @version 1.0
* @since 1.0
*/
public class Discord {
private final JDABuilder jdaBuilder;
/**
* Configures the bot to make it ready to launch
*
* @param token bot token
* @param storeType type how to store the given untis data {@link StoreType}
* @param encryptPassword password to encrypt all passwords from the untis accounts
* @since 1.0
*/
public Discord(String token, StoreType storeType, String encryptPassword, JSONObject languages) {
jdaBuilder = JDABuilder.createDefault(token);
jdaBuilder.addEventListeners(new DiscordCommandListener(storeType, new Crypt(encryptPassword), languages));
}
/**
* Starts the bot
*
* @throws LoginException if the given login credentials are invalid
* @since 1.0
*/
public void start() throws LoginException {
jdaBuilder.build();
}
}

View File

@ -0,0 +1,665 @@
package org.bytedream.untisbot.discord;
import ch.qos.logback.classic.Logger;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.events.ReadyEvent;
import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
import net.dv8tion.jda.api.events.guild.GuildLeaveEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.bytedream.untis4j.LoginException;
import org.bytedream.untis4j.Session;
import org.bytedream.untis4j.responseObjects.Teachers;
import org.bytedream.untis4j.responseObjects.TimeUnits;
import org.bytedream.untis4j.responseObjects.Timetable;
import org.bytedream.untisbot.Crypt;
import org.bytedream.untisbot.Main;
import org.bytedream.untisbot.Utils;
import org.bytedream.untisbot.data.Data;
import org.bytedream.untisbot.data.DataConnector;
import org.bytedream.untisbot.data.StoreType;
import org.bytedream.untisbot.untis.CheckCallback;
import org.bytedream.untisbot.untis.TimetableChecker;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
import java.awt.*;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* Adapter to handle all events
*
* @version 1.0
* @since 1.0
*/
public class DiscordCommandListener extends ListenerAdapter {
private final DataConnector.Guild guildDataConnector;
private final DataConnector.Stats statsDataConnector;
private final JSONObject languages;
private final HashMap<Long, Timer> allTimetableChecker = new HashMap<>();
private final Logger logger = Main.getLogger();
/**
* Sets up the adapter
*
* @param storeType type how to store the given untis data {@link StoreType}
* @param crypt {@link Crypt} object to encrypt all passwords from the untis accounts
* @param languages {@link JSONObject} containing different languages to print out when the timetable is checked
* @since 1.0
*/
public DiscordCommandListener(StoreType storeType, Crypt crypt, JSONObject languages) {
DataConnector dataConnector = new DataConnector(storeType, crypt);
guildDataConnector = dataConnector.guildConnector();
statsDataConnector = dataConnector.statsConnector();
this.languages = languages;
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Color.GREEN);
}
/**
* Checks the timetable from the given guild and sends an embed if the timetable has changes
*
* @param guild guild to send the timetable
* @since 1.0
*/
public void runTimetableChecker(Guild guild) {
long guildId = guild.getIdLong();
Timer timer = new Timer();
Data.Guild data = guildDataConnector.get(guildId);
TimetableChecker timetableChecker;
TextChannel textChannel = guild.getTextChannelById(data.getChannelId());
if (textChannel == null) {
textChannel = guild.getDefaultChannel();
if (textChannel != null) {
guildDataConnector.update(guildId, null, null, null, null, null, null, textChannel.getIdLong(), null, null, null, null);
textChannel.sendMessage("It seems like, that the channel where I should send the timetable messages in doesn't exists anymore." +
"I'll send the changes now in this channel." +
"If you want that I send these messages into another channel, type `" + data.getPrefix() + "channel` in the channel where I should send the messages in").queue();
}
}
try {
timetableChecker = new TimetableChecker(data.getUsername(), data.getPassword(), data.getServer(), data.getSchool(), data.getKlasseId());
} catch (LoginException e) {
e.printStackTrace();
logger.warn(guild.getName() + " failed to login", e);
textChannel.sendMessage("Failed to login. Please try to re-set your data").queue();
return;
} catch (IOException e) {
e.printStackTrace();
logger.warn(guild.getName() + " ran into an exception while trying to setup the timetable checker", e);
textChannel.sendMessage("An error occurred while trying to setup the timetable checking process." +
"You should try to re-set your data or trying to contact my author <@650417934073593886> (:3) if the problem won't go away").queue();
return;
}
timer.scheduleAtFixedRate(new TimerTask() {
private int latestImportTime = 0;
private void main() {
Data.Guild data = guildDataConnector.get(guildId);
TextChannel textChannel = guild.getTextChannelById(data.getChannelId());
if (textChannel == null) {
textChannel = guild.getDefaultChannel();
if (textChannel == null) {
return;
} else {
guildDataConnector.update(guildId, null, null, null, null, null, null, textChannel.getIdLong(), null, null, null, null);
textChannel.sendMessage("It seems like, that the channel where I should send the timetable messages in doesn't exists anymore. " +
"I'll send the changes now in this channel." +
"If you want that I send these messages into another channel, type `" + data.getPrefix() + "set-channel` in the channel where I should send the messages in").queue();
}
}
boolean error = false;
Data.Stats stats = statsDataConnector.get(guildId);
String setLanguage = data.getLanguage();
if (setLanguage == null) {
setLanguage = "en";
}
JSONObject language = languages.getJSONObject(setLanguage);
LocalDate now = LocalDate.now();
int i = 0;
int daysToCheck = 6;
try {
CheckCallback checkCallback = timetableChecker.check(now);
Timetable allLessons = checkCallback.getAllLessons();
if (Timetable.sortByStartTime(allLessons).get(allLessons.size() - 1).getEndTime().isBefore(LocalTime.now())) {
// checks if all lessons are over, and if so, it stops checking the timetable for today
i++;
daysToCheck++;
}
} catch (IOException e) {
e.printStackTrace();
}
for (; i <= daysToCheck; i++) {
LocalDate localDate = now.plusDays(i);
try {
CheckCallback checkCallback = timetableChecker.check(localDate);
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Color.CYAN);
embedBuilder.setTitle(Utils.advancedFormat(language.getString("title"), new HashMap<String, Object>() {{
put("weekday", language.getString(localDate.getDayOfWeek().name().toLowerCase()));
put("date", localDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")));
}}));
ArrayList<Timetable.Lesson> cancelledLessons = checkCallback.getCancelled();
ArrayList<Timetable.Lesson[]> movedLessons = checkCallback.getMoved();
ArrayList<Timetable.Lesson> notCancelledLessons = checkCallback.getNotCancelled();
ArrayList<Timetable.Lesson[]> notMovedLessons = checkCallback.getNotMoved();
for (Timetable.Lesson lesson : cancelledLessons) {
TimeUnits.TimeUnitObject timeUnitObject = lesson.getTimeUnitObject();
HashMap<String, Object> formatMap = new HashMap<String, Object>() {{
put("lesson-name", timeUnitObject.getName());
put("date", lesson.getDate());
put("start-time", timeUnitObject.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm")));
put("end-time", timeUnitObject.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm")));
put("teachers", String.join(", ", lesson.getTeachers().getFullNames()));
put("subjects", String.join(", ", lesson.getSubjects().getLongNames()));
put("rooms", String.join(", ", lesson.getRooms().getLongNames()));
}};
embedBuilder.addField(Utils.advancedFormat(language.getString("cancelled-title"), formatMap),
Utils.advancedFormat(language.getString("cancelled-body"), formatMap), false);
}
for (Timetable.Lesson[] lesson : movedLessons) {
TimeUnits.TimeUnitObject timeUnitObject = lesson[0].getTimeUnitObject();
Timetable.Lesson to = lesson[0];
Timetable.Lesson from = lesson[1];
HashMap<String, Object> formatMap = new HashMap<String, Object>() {{
put("from-lesson-name", from.getTimeUnitObject().getName());
put("to-lesson-name", to.getTimeUnitObject().getName());
put("date", from.getDate());
put("start-time", timeUnitObject.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm")));
put("end-time", timeUnitObject.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm")));
put("teachers", String.join(", ", from.getTeachers().getFullNames()));
put("subjects", String.join(", ", from.getSubjects().getLongNames()));
put("rooms", String.join(", ", from.getRooms().getLongNames()));
}};
embedBuilder.addField(Utils.advancedFormat(language.getString("moved-title"), formatMap),
Utils.advancedFormat(language.getString("moved-body"), formatMap), false);
}
for (Timetable.Lesson lesson : notCancelledLessons) {
TimeUnits.TimeUnitObject timeUnitObject = lesson.getTimeUnitObject();
HashMap<String, Object> formatMap = new HashMap<String, Object>() {{
put("lesson-name", timeUnitObject.getName());
put("date", lesson.getDate());
put("start-time", timeUnitObject.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm")));
put("end-time", timeUnitObject.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm")));
put("teachers", String.join(", ", lesson.getTeachers().getFullNames()));
put("subjects", String.join(", ", lesson.getSubjects().getLongNames()));
put("rooms", String.join(", ", lesson.getRooms().getLongNames()));
}};
embedBuilder.addField(Utils.advancedFormat(language.getString("not-cancelled-title"), formatMap),
Utils.advancedFormat(languages.getString("not-cancelled-body"), formatMap), false);
}
for (Timetable.Lesson[] lesson : notMovedLessons) {
TimeUnits.TimeUnitObject timeUnitObject = lesson[0].getTimeUnitObject();
Timetable.Lesson from = lesson[0];
Timetable.Lesson to = lesson[1];
HashMap<String, Object> formatMap = new HashMap<String, Object>() {{
put("from-lesson-name", from.getTimeUnitObject().getName());
put("to-lesson-name", to.getTimeUnitObject().getName());
put("date", from.getDate());
put("start-time", timeUnitObject.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm")));
put("end-time", timeUnitObject.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm")));
put("teachers", String.join(", ", from.getTeachers().getFullNames()));
put("subjects", String.join(", ", from.getSubjects().getLongNames()));
put("rooms", String.join(", ", from.getRooms().getLongNames()));
}};
embedBuilder.addField(Utils.advancedFormat(language.getString("not-moved-title"), formatMap),
Utils.advancedFormat(language.getString("not-moved-body"), formatMap), false);
}
if (!embedBuilder.getFields().isEmpty()) {
textChannel.sendMessage(embedBuilder.build()).queue();
}
LocalDate lastChecked = guildDataConnector.get(guildId).getLastChecked();
Short totalDays = stats.getTotalDays();
int totalLessons = stats.getTotalLessons();
if (lastChecked == null || lastChecked.isBefore(now.plusDays(i))) {
totalDays++;
totalLessons += checkCallback.getAllLessons().size();
guildDataConnector.update(guildId, null, null, null, null, null, null, null, null, null, null, now.plusDays(i));
}
short totalCancelledLessons = (short) (stats.getTotalCancelledLessons() + cancelledLessons.size() - notCancelledLessons.size());
short totalMovedLessons = (short) (stats.getTotalMovedLessons() + movedLessons.size() - notMovedLessons.size());
statsDataConnector.update(guildId, stats.getTotalRequests() + 1, totalDays, totalLessons, totalCancelledLessons, totalMovedLessons,
(float) Utils.round((float) totalCancelledLessons / totalLessons, 3) * 5,
(float) Utils.round((float) totalMovedLessons / totalLessons, 3) * 5);
for (Timetable.Lesson lesson : checkCallback.getCancelled()) {
HashMap<String, Short> teachers = stats.getAbsentTeachers();
for (Teachers.TeacherObject teacher : lesson.getTeachers()) {
String name = teacher.getFullName();
statsDataConnector.updateAbsentTeachers(guildId, name, (short) (teachers.getOrDefault(name, (short) 0) + 1));
}
}
for (Timetable.Lesson lesson : checkCallback.getNotCancelled()) {
HashMap<String, Short> teachers = stats.getAbsentTeachers();
for (Teachers.TeacherObject teacher : lesson.getTeachers()) {
String name = teacher.getFullName();
statsDataConnector.updateAbsentTeachers(guildId, name, (short) (teachers.getOrDefault(name, (short) 0) - 1));
}
}
stats = statsDataConnector.get(guildId);
if (error) {
error = false;
}
} catch (Exception e) {
logger.warn(guild.getName() + " ran into an exception while trying to check the timetable for the " + localDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")), e);
if (!error) {
textChannel.sendMessage("An error occurred while trying to check the timetable." +
"You can try to re-set your data or trying to contact my author <@650417934073593886> (:3) if the problem won't go away").queue();
error = true;
}
try {
Thread.sleep((i + 1) * 5000);
} catch (InterruptedException ignore) {
}
}
}
}
@Override
public void run() {
try {
Session session = timetableChecker.getSession();
session.reconnect();
if (latestImportTime < session.getLatestImportTime().getLatestImportTime()) {
latestImportTime = session.getLatestImportTime().getLatestImportTime();
main();
} else {
main();
}
} catch (IOException e) {
main();
}
}
}, 0, data.getSleepTime());
allTimetableChecker.put(guildId, timer);
logger.info(guild.getName() + " started timetable listening");
}
@Override
public void onReady(ReadyEvent event) {
ArrayList<Long> allGuilds = new ArrayList<>();
for (Guild guild : event.getJDA().getGuilds()) {
long guildId = guild.getIdLong();
if (!guildDataConnector.has(guildId)) {
guildDataConnector.add(guildId);
}
if (!statsDataConnector.has(guildId)) {
statsDataConnector.add(guildId);
}
if (guildDataConnector.get(guildId).isCheckActive()) {
runTimetableChecker(guild);
}
allGuilds.add(guildId);
}
for (Data.Guild data : guildDataConnector.getAll()) {
if (!allGuilds.contains(data.getGuildId())) {
guildDataConnector.remove(data.getGuildId());
statsDataConnector.remove(data.getGuildId());
}
}
logger.info("Bot is ready | Total guilds: " + guildDataConnector.getAll().size());
}
@Override
public void onGuildMessageReceived(GuildMessageReceivedEvent event) {
new Thread(() -> {
long guildId = event.getGuild().getIdLong();
Data.Guild data = guildDataConnector.get(guildId);
try {
if (event.getAuthor().isBot() || event.getAuthor().isFake() || !event.getMessage().getContentDisplay().startsWith(data.getPrefix())) {
return;
}
} catch (StringIndexOutOfBoundsException e) {
// if (for example) a picture is sent, the bot checks for the first letter from the message an a because a picture has no letters, this error gets thrown
return;
}
String guildName = event.getGuild().getName();
Guild guild = event.getGuild();
String userInput = event.getMessage().getContentDisplay().substring(data.getPrefix().length()).trim().replaceAll(" +", " ");
String userInputLow = userInput.toLowerCase();
String[] splitCommand = userInputLow.split(" ");
String command = splitCommand[0];
String[] args = Arrays.copyOfRange(splitCommand, 1, splitCommand.length);
TextChannel channel = event.getChannel();
try {
if (command.equals("stats")) {
if (args.length == 0) {
Data.Stats stats = statsDataConnector.get(guildId);
EmbedBuilder embedBuilder = new EmbedBuilder();
if (guildName.trim().endsWith("s")) {
embedBuilder.setTitle(guild.getName() + " untis status");
} else {
embedBuilder.setTitle(guild.getName() + "'s untis status");
}
ArrayList<String> mostMissedTeachers = new ArrayList<>();
short missedLessons = 0;
for (Map.Entry<String, Short> entry : stats.getAbsentTeachers().entrySet()) {
if (entry.getValue() > missedLessons) {
mostMissedTeachers.clear();
mostMissedTeachers.add(entry.getKey());
missedLessons = entry.getValue();
} else if (entry.getValue() == missedLessons) {
mostMissedTeachers.add(entry.getKey());
}
}
String mostMissedTeachersText;
if (missedLessons == 0) {
mostMissedTeachersText = "n/a";
} else {
mostMissedTeachersText = String.join(", ", mostMissedTeachers) + " - " + missedLessons + " missed lessons";
}
String timetableChecking;
if (data.isCheckActive()) {
timetableChecking = "\uD83D\uDFE2 Active";
embedBuilder.setColor(Color.GREEN);
} else {
timetableChecking = "\uD83D\uDD34 Inactive";
embedBuilder.setFooter("To start timetable checking, type `" + data.getPrefix() + "set-data <username> <password> <loginpage url>` - type `" + data.getPrefix() + "help` for more details");
embedBuilder.setColor(Color.RED);
}
embedBuilder.addField("Timetable checking", timetableChecking, true);
//embedBuilder.addField("Checking interval", data.getSleepTime() / 60000 + " minutes", true);
embedBuilder.addField("Total timetable requests", String.valueOf(stats.getTotalRequests()), true);
embedBuilder.addField("Total lessons checked", String.valueOf(stats.getTotalLessons()), true);
embedBuilder.addField("Total weeks checked", String.valueOf((int) (Math.floor((float) stats.getTotalDays() / 7))), true);
embedBuilder.addField("Total cancelled lessons", String.valueOf(stats.getTotalCancelledLessons()), true);
embedBuilder.addField("Total moved lessons", String.valueOf(stats.getTotalMovedLessons()), true);
embedBuilder.addField("Average cancelled lessons per week", String.valueOf(stats.getAverageCancelledLessonsPerWeek()), true);
embedBuilder.addField("Average moved lessons per week", String.valueOf(stats.getAverageMovedLessonsPerWeek()), true);
embedBuilder.addField("Most missed teacher", mostMissedTeachersText, false);
channel.sendMessage(embedBuilder.build()).queue();
} else {
channel.sendMessage("Wrong number of arguments were given, type `" + data.getPrefix() + "help` for help").queue();
}
} else if (event.getMember().getPermissions().contains(Permission.ADMINISTRATOR)) {
switch (command) {
case "channel": // `channel` command
if (args.length == 0) {
guildDataConnector.update(guild.getIdLong(), null, null, null, null, null, null, channel.getIdLong(), null, null, null, null);
logger.info(guildName + " set a new channel to send the timetable changes to");
channel.sendMessage("This channel is now set as the channel where I send the timetable changes in").queue();
} else {
channel.sendMessage("Wrong number of arguments were given (expected 0, got " + args.length + "), type `" + data.getPrefix() + "help channel` for help").queue();
}
break;
case "clear": // `clear` command
if (args.length == 0) {
guildDataConnector.update(guild.getIdLong(), null, "", "", "", "", (short) 0, null, null, null, false, null);
logger.info(guildName + " cleared their data");
channel.sendMessage("Cleared untis data and stopped timetable listening").queue();
} else {
channel.sendMessage("Wrong number of arguments were given (expected 0, got " + args.length + "), type `" + data.getPrefix() + "help clear` for help").queue();
}
break;
case "data": // `data <username> <password> <server> <school name>` command
if (args.length >= 3 && args.length <= 4) {
String schoolName;
try {
schoolName = new URL(args[2]).getQuery().split("=")[1];
} catch (MalformedURLException | ArrayIndexOutOfBoundsException e) {
channel.sendMessage("The given login data is invalid").queue();
return;
}
String server = args[2].replace("https://", "").replace("http://", "");
server = "https://" + server.substring(0, server.indexOf("/"));
short klasseId;
try {
channel.sendMessage("Verifying data...").queue();
Session session = Session.login(args[0], args[1], server, schoolName);
if (args.length == 3) {
klasseId = (short) session.getInfos().getKlasseId();
} else {
try {
klasseId = (short) session.getKlassen().findByName(args[3]).getId();
} catch (NullPointerException e) {
channel.sendMessage("❌ Cannot find the given class").queue();
return;
}
}
session.logout();
} catch (IOException e) {
channel.sendMessage("❌ The given login data is invalid").queue();
return;
}
boolean isCheckActive = data.isCheckActive();
if (data.getChannelId() == null) {
guildDataConnector.update(guildId, null, args[0], args[1], server, schoolName, klasseId, channel.getIdLong(), null, null, true, null);
} else {
guildDataConnector.update(guildId, null, args[0], args[1], server, schoolName, klasseId, null, null, null, true, null);
}
if (isCheckActive) {
Timer timer = allTimetableChecker.get(guildId);
timer.cancel();
timer.purge();
allTimetableChecker.remove(guildId);
runTimetableChecker(guild);
channel.sendMessage("✅ Updated data and restarted timetable listening").queue();
} else {
runTimetableChecker(guild);
channel.sendMessage("✅ Timetable listening has been started").queue();
}
logger.info(guildName + " set new data");
} else {
channel.sendMessage("Wrong number of arguments were given (expected 3 or 4, got " + args.length + "), type `" + data.getPrefix() + "help data` for help").queue();
}
break;
case "language": // `language <language>` command
if (args.length == 1) {
String language = args[0];
if (!languages.has(language)) {
channel.sendMessage("The language `" + language + "` is not supported. Type `" + data.getPrefix() + "help` to see all available languages").queue();
} else {
guildDataConnector.update(guildId, language, null, null, null, null, null, null, null, null, null, null);
logger.info(guildName + " set their language to " + language);
channel.sendMessage("Updated language to `" + language + "`").queue();
}
} else {
channel.sendMessage("Wrong number of arguments were given (expected 1, got " + args.length + "), type `" + data.getPrefix() + "help language` for help").queue();
}
break;
case "prefix": // `prefix <new prefix>` command
if (args.length == 1) {
String prefix = args[0];
if (prefix.length() == 0 || prefix.length() > 6) {
channel.sendMessage("The prefix must be between 1 and 6 characters long").queue();
} else {
String note = "";
if (prefix.contains("'") || prefix.contains("\"")) {
channel.sendMessage("Cannot use `'` or `\"` in prefix").queue();
return;
}
if (prefix.length() == 1) {
if ("!?$¥§%&@€#|/\\=.:-_+,;*+~<>^°".indexOf(prefix.charAt(0)) == -1) {
note += "\n_Note_: Because the prefix is not in `!?$¥§%&@€#|/\\=.:-_+,;*+~<>^°` you have to call commands with a blank space between it and the prefix";
}
} else {
prefix += " ";
note += "\n_Note_: Because the prefix is longer than 1 character you have to call commands with a blank space between it and the prefix";
}
guildDataConnector.update(guildId, null, null, null, null, null, null, null, prefix, null, null, null);
logger.info(guildName + " set their prefix to " + prefix);
channel.sendMessage("Updated prefix to `" + prefix + "`" + note).queue();
}
} else {
channel.sendMessage("Wrong number of arguments were given (expected 3, got " + args.length + "), type `" + data.getPrefix() + "help prefix` for help").queue();
}
break;
default:
}
}
} catch (NullPointerException ignore) {
}
}).start();
}
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event) { // only for `help` command
new Thread(() -> {
if (event.getAuthor().isBot()) {
return;
}
String message = event.getMessage().getContentDisplay().trim().toLowerCase();
MessageChannel channel = event.getChannel();
EmbedBuilder embedBuilder = new EmbedBuilder();
String prefix;
if (message.contains("help")) { // `help` command
if (event.isFromGuild()) {
prefix = guildDataConnector.get(event.getGuild().getIdLong()).getPrefix();
embedBuilder.setFooter("Note: Every command must be called with the set prefix ('" + prefix + "')");
if (!event.getMessage().getContentDisplay().startsWith(prefix + "help")) {
return;
}
} else if (message.equals("help") || message.startsWith("help ")) {
prefix = "";
} else {
return;
}
} else {
return;
}
String[] splitMessage = message.substring(prefix.length()).split(" ");
String[] args = Arrays.copyOfRange(splitMessage, 1, splitMessage.length);
String help = "Use `" + prefix + "help <command>` to get help / information about a command.\n\n" +
"All available commands are:\n" +
"`channel` `clear` `data` `help` `language` `prefix` `stats`";
if (args.length > 1) {
channel.sendMessage("Wrong number of arguments are given (expected 0 or 1, got " + splitMessage.length + "). " + help).queue();
} else if (args.length == 0) {
channel.sendMessage(help).queue();
} else {
String title;
String description;
String example;
String _default = null;
switch (args[0]) {
case "channel":
title = "`channel` command";
description = "In the channel where this command is entered, the bot shows the timetable changes";
example = "`channel`";
break;
case "clear":
title = "`clear` command";
description = "Clears the given untis data, given from the `data` command";
example = "`clear`";
break;
case "data":
title = "`data <username> <password> <login page url>` command";
description = "Sets the data with which the bot logs in to untis and checks for timetable changes. The data is stored encrypted on the server.\n" +
"`username` and `password` are the normal untis login data with which one also logs in to the untis website / app. To gain the login page url you have to go to webuntis.com, type in your school and choose it.\n" +
"Then you will be redirected to the untis login page, The url of this page is the login page url, for example `https://example.webuntis.com/WebUntis/?school=myschool#/basic/main`.\n" +
"`class name` is just the name of the class you want to check (eg. `12AB`). If `class name` is not specified, the bot tries to get the default class which is assigned to the given account.";
example = "`data myname secure https://example.webuntis.com/WebUntis/?school=example#/basic/main 12AB`";
_default = "`en`";
break;
case "help":
title = "`help <command>` command";
description = "Displays help to a given command";
example = "`help data`";
break;
case "language":
title = "`language <language>` command";
description = "Changes the language in which the timetable information are displayed. Currently only 'de' (german) and 'en' (english) are supported";
example = "`language de`";
_default = "`en`";
break;
case "prefix":
title = "`prefix <new prefix>` command";
description = "Changes the prefix with which commands are called";
example = "`prefix $`";
_default = "`!untis `";
break;
case "stats":
title = "`stats` command";
description = "Displays a message with some stats (total cancelled lessons, etc.)";
example = "`stats`";
break;
default:
channel.sendMessage("Unknown command was given. " + help).queue();
return;
}
embedBuilder.setColor(Color.CYAN);
embedBuilder.setTitle(title);
embedBuilder.addField("Description", description, false);
embedBuilder.addField("Example", example, false);
if (_default != null) {
embedBuilder.addField("Default", _default, false);
}
embedBuilder.setFooter("`<>` = required; `[]` = optional");
channel.sendMessage(embedBuilder.build()).queue();
}
}).start();
}
@Override
public void onGuildJoin(GuildJoinEvent event) {
Guild guild = event.getGuild();
long guildId = guild.getIdLong();
if (!guildDataConnector.has(guildId)) {
guildDataConnector.add(guildId);
}
if (!statsDataConnector.has(guildId)) {
statsDataConnector.add(guildId);
}
logger.info("Joined new guild - Name: " + event.getGuild().getName() + " | Total guilds: " + guildDataConnector.getAll().size());
}
@Override
public void onGuildLeave(@NotNull GuildLeaveEvent event) {
long guildId = event.getGuild().getIdLong();
guildDataConnector.remove(guildId);
statsDataConnector.remove(guildId);
logger.info("Left guild - Name: " + event.getGuild().getName() + " | Total guilds: " + guildDataConnector.getAll().size());
}
}

View File

@ -0,0 +1,40 @@
{
"de": {
"language": "German",
"title": "Stunden Ausfall Information für {weekday}, den {date}",
"cancelled-title": "Ausfall {lesson-name}. Stunde ({start-time} Uhr - {end-time} Uhr)",
"cancelled-body": "Ausfall bei {teachers}, in {subjects}, in der {lesson-name}. Stunde",
"moved-title": "{from-lesson-name}. Stunde wird zur {to-lesson-name}. Stunde umverlegt",
"moved-body": "Die {from-lesson-name}. Stunde bei {teachers} in {subjects} wird zur {to-lesson-name}. Stunde umverlegt",
"not-cancelled-title": "KEIN Ausfall {lesson-name}. Stunde ({start-time} Uhr - {end-time} Uhr)",
"not-cancelled-body": "KEIN Ausfall bei {teachers}, in {subjects}, in der {lesson-name}. Stunde",
"not-moved-title": "{from-lesson-name}. Stunde wird NICHT zur {to-lesson-name}. Stunde umverlegt",
"not-moved-body": "Die {from-lesson-name}. Stunde bei {teachers} wird NICHT zur {to-lesson-name}. Stunde umverlegt",
"monday": "Montag",
"tuesday": "Dienstag",
"wednesday": "Mittwoch",
"thursday": "Donnerstag",
"friday": "Freitag",
"saturday": "Samstag",
"sunday": "Sonntag"
},
"en": {
"language": "English",
"title": "Irregular lesson information for {weekday}, {date}",
"cancelled-title": "Cancelled {lesson-name}. lesson ({start-time} - {end-time})",
"cancelled-body": "The {lesson-name}. lesson with {teachers} in {subjects} is cancelled",
"moved-title": "The {from-lesson-name}. lesson is moved to {to-lesson-name}. lesson",
"moved-body": "The {from-lesson-name}. lesson with {teachers} in {subjects} is moved to the {to-lesson-name}. lesson",
"not-cancelled-title": "{lesson-name}. lesson ({start-time} - {end-time}) is NO cancelled",
"not-cancelled-body": "The {lesson-name}. lesson with {teachers} in {subjects} is NOT cancelled",
"not-moved-title": "The {from-lesson-name}. lesson is NOT moved to {to-lesson-name}.",
"not-moved-body": "The {from-lesson-name}. lesson with {teachers} in {subjects} is NOT moved to the {to-lesson-name}. lesson",
"monday": "monday",
"tuesday": "tuesday",
"wednesday": "wednesday",
"thursday": "thursday",
"friday": "friday",
"saturday": "saturday",
"sunday": "sunday"
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<charset>UTF-8</charset>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${LOG_FILE}</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>

View File

@ -0,0 +1,89 @@
package org.bytedream.untisbot.untis;
import org.bytedream.untis4j.responseObjects.Timetable;
import java.sql.Time;
import java.time.LocalDate;
import java.util.ArrayList;
/**
* Callback of {@link TimetableChecker#check(LocalDate)}
*
* @version 1.0
* @since 1.0
*/
public class CheckCallback {
private final Timetable allLessons;
private final ArrayList<Timetable.Lesson> cancelled;
private final ArrayList<Timetable.Lesson[]> moved;
private final ArrayList<Timetable.Lesson> notCancelled;
private final ArrayList<Timetable.Lesson[]> notMoved;
/**
* Initialize the {@link CheckCallback} class
*
* @param cancelled all cancelled messages
* @param moved all moved messages
* @param notCancelled all not cancelled messages
* @param notMoved all not moved messages
* @since 1.0
*/
public CheckCallback(Timetable allLessons, ArrayList<Timetable.Lesson> cancelled, ArrayList<Timetable.Lesson[]> moved, ArrayList<Timetable.Lesson> notCancelled, ArrayList<Timetable.Lesson[]> notMoved) {
this.allLessons = allLessons;
this.cancelled = cancelled;
this.moved = moved;
this.notCancelled = notCancelled;
this.notMoved = notMoved;
}
/**
* Returns all that were checked
*
* @return all that were checked
* @since 1.0
*/
public Timetable getAllLessons() {
return allLessons;
}
/**
* Returns all cancelled lessons
*
* @return all cancelled lessons
* @since 1.0
*/
public ArrayList<Timetable.Lesson> getCancelled() {
return cancelled;
}
/**
* Returns all moved lessons
*
* @return all moved lessons
* @since 1.0
*/
public ArrayList<Timetable.Lesson[]> getMoved() {
return moved;
}
/**
* Returns all not cancelled lessons
*
* @return all not cancelled lessons
* @since 1.0
*/
public ArrayList<Timetable.Lesson> getNotCancelled() {
return notCancelled;
}
/**
* Returns all not moved lessons
*
* @return all not moved lessons
* @since 1.0
*/
public ArrayList<Timetable.Lesson[]> getNotMoved() {
return notMoved;
}
}

View File

@ -0,0 +1,171 @@
package org.bytedream.untisbot.untis;
import org.bytedream.untis4j.LoginException;
import org.bytedream.untis4j.RequestManager;
import org.bytedream.untis4j.Session;
import org.bytedream.untis4j.UntisUtils;
import org.bytedream.untis4j.responseObjects.Timetable;
import org.bytedream.untisbot.Main;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
/**
* Class to check the untis timetable
*
* @version 1.0
* @since 1.0
*/
public class TimetableChecker {
private final Session session;
private final int klasseId;
private final LocalDate[] cancelledLessonsDay = new LocalDate[7];
private final LocalDate[] ignoredLessonsDay = new LocalDate[7];
private final LocalDate[] movedLessonsDay = new LocalDate[7];
private final Timetable[] cancelledLessons = new Timetable[]{new Timetable(), new Timetable(), new Timetable(), new Timetable(), new Timetable(), new Timetable(), new Timetable()};
private final Timetable[] ignoredLessons = new Timetable[]{new Timetable(), new Timetable(), new Timetable(), new Timetable(), new Timetable(), new Timetable(), new Timetable()};
private final ArrayList<HashMap<Timetable.Lesson, Timetable.Lesson>> movedLessons = new ArrayList<>();
/**
* Sets all necessary configurations and connects to the untis account with the given untis credentials
*
* @param username username of the untis account
* @param password user password of the untis account
* @param server the server from the school as URL
* @param schoolName name of the school
* @throws IOException if any {@link IOException} while the login occurs
* @since 1.0
*/
public TimetableChecker(String username, String password, String server, String schoolName, int klasseId) throws IOException {
session = Session.login(username, password, server, schoolName);
this.klasseId = klasseId;
for (LocalDate[] localDates : new HashSet<LocalDate[]>() {{
add(cancelledLessonsDay);
add(ignoredLessonsDay);
add(movedLessonsDay);
}}) {
for (int i = 0; i < 7; i++) {
localDates[i] = LocalDate.now().plusDays(i + 1);
}
}
for (int i = 0; i < 7; i++) {
movedLessons.add(new HashMap<>());
}
}
/**
* Checks the timetable on a specific date. Automatically deletes cached lessons from the past, so you should not call the method in descending date order
*
* @param dateToCheck date which should be checked
* @return {@link CheckCallback} with information about the timetable (if anything has changed)
* @throws IOException if any {@link IOException} occurs
* @since 1.0
*/
public CheckCallback check(LocalDate dateToCheck) throws IOException {
Timetable timetable = session.getTimetableFromKlasseId(dateToCheck, dateToCheck, klasseId);
timetable.sortByStartTime();
int dayOfWeekInArray = dateToCheck.getDayOfWeek().getValue() - 1;
Timetable allCancelledLessons = cancelledLessons[dayOfWeekInArray];
Timetable allIgnoredLessons = ignoredLessons[dayOfWeekInArray];
HashMap<Timetable.Lesson, Timetable.Lesson> allMovedLessons = movedLessons.get(dayOfWeekInArray);
Timetable totalLessons = new Timetable();
ArrayList<Timetable.Lesson> cancelledLesson = new ArrayList<>();
ArrayList<Timetable.Lesson[]> movedLesson = new ArrayList<>();
ArrayList<Timetable.Lesson> notCancelledLessons = new ArrayList<>();
ArrayList<Timetable.Lesson[]> notMovedLessons = new ArrayList<>();
for (Timetable.Lesson lesson : timetable) {
totalLessons.add(lesson);
if (lesson.getCode() == UntisUtils.LessonCode.CANCELLED && !allCancelledLessons.contains(lesson) && !allIgnoredLessons.contains(lesson)) {
Timetable specificLessons = timetable.searchByStartTime(lesson.getStartTime());
specificLessons.remove(lesson);
switch (specificLessons.size()) {
case 0: // lesson is cancelled
allCancelledLessons.add(lesson);
cancelledLesson.add(lesson);
break;
case 1: // lesson is maybe moved
if (specificLessons.get(0).getCode() == UntisUtils.LessonCode.IRREGULAR) { // lesson is moved
Timetable.Lesson irregularLesson = specificLessons.get(0);
for (Timetable.Lesson lesson1 : timetable.searchByTeachers(irregularLesson.getTeachers())) {
if (lesson1.getCode() == UntisUtils.LessonCode.CANCELLED && !allIgnoredLessons.contains(lesson1)) {
allIgnoredLessons.add(lesson1);
allCancelledLessons.remove(lesson1);
allMovedLessons.put(lesson, lesson1);
movedLesson.add(new Timetable.Lesson[]{lesson, lesson1});
break;
}
}
} else { // lesson is not moved but cancelled
allCancelledLessons.add(lesson);
cancelledLesson.add(lesson);
}
break;
}
} else if (lesson.getCode() == UntisUtils.LessonCode.IRREGULAR && timetable.searchByStartTime(lesson.getStartTime()).size() == 1 && !allIgnoredLessons.contains(lesson)) {
// lesson is maybe moved
for (Timetable.Lesson lesson1 : timetable) {
// checks if another lesson exist with the same 'stats' and if it's cancelled
if (lesson1.getCode() == UntisUtils.LessonCode.CANCELLED && !allIgnoredLessons.contains(lesson1) && lesson.getSubjects().containsAll(lesson1.getSubjects())) {
allIgnoredLessons.add(lesson1);
allMovedLessons.put(lesson, lesson1);
movedLesson.add(new Timetable.Lesson[]{lesson, lesson1});
break;
}
}
} else if (allMovedLessons.containsKey(lesson) && lesson.getCode() == UntisUtils.LessonCode.REGULAR) { // checks if a moved lesson takes place again
Timetable.Lesson value = allMovedLessons.get(lesson);
allIgnoredLessons.remove(value);
allMovedLessons.remove(lesson);
notMovedLessons.add(new Timetable.Lesson[]{lesson, value});
break;
} else if (allCancelledLessons.contains(lesson) && lesson.getCode() == UntisUtils.LessonCode.REGULAR) { // checks if a cancelled lesson takes place again
allCancelledLessons.remove(lesson);
notCancelledLessons.add(lesson);
break;
}
}
if (cancelledLessonsDay[dayOfWeekInArray].compareTo(dateToCheck) > 0) {
cancelledLessonsDay[dayOfWeekInArray] = dateToCheck;
}
if (ignoredLessonsDay[dayOfWeekInArray].compareTo(dateToCheck) > 0) {
ignoredLessonsDay[dayOfWeekInArray] = dateToCheck;
}
if (movedLessonsDay[dayOfWeekInArray].compareTo(dateToCheck) > 0) {
movedLessonsDay[dayOfWeekInArray] = dateToCheck;
}
cancelledLessons[dayOfWeekInArray] = allCancelledLessons;
ignoredLessons[dayOfWeekInArray] = allIgnoredLessons;
movedLessons.remove(dayOfWeekInArray);
movedLessons.add(dayOfWeekInArray, allMovedLessons);
return new CheckCallback(totalLessons, cancelledLesson, movedLesson, notCancelledLessons, notMovedLessons);
}
/**
* Returns the session
*
* @return the session
* @since 1.0
*/
public Session getSession() {
return session;
}
}