mirror of
https://github.com/bytedream/untisbot-discord.git
synced 2025-05-09 20:25:09 +02:00
Initial commit
This commit is contained in:
commit
e27bccc836
32
Dockerfile
Normal file
32
Dockerfile
Normal 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
159
README.md
Normal 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
31
dockerfiles/database.sql
Normal 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
14
dockerfiles/run.sh
Executable 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
|
87
src/org/bytedream/untisbot/Crypt.java
Normal file
87
src/org/bytedream/untisbot/Crypt.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
201
src/org/bytedream/untisbot/Main.java
Normal file
201
src/org/bytedream/untisbot/Main.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
79
src/org/bytedream/untisbot/Utils.java
Normal file
79
src/org/bytedream/untisbot/Utils.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
163
src/org/bytedream/untisbot/data/Data.java
Normal file
163
src/org/bytedream/untisbot/data/Data.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
571
src/org/bytedream/untisbot/data/DataConnector.java
Normal file
571
src/org/bytedream/untisbot/data/DataConnector.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
src/org/bytedream/untisbot/data/StoreType.java
Normal file
12
src/org/bytedream/untisbot/data/StoreType.java
Normal 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
|
||||||
|
}
|
43
src/org/bytedream/untisbot/discord/Discord.java
Normal file
43
src/org/bytedream/untisbot/discord/Discord.java
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
665
src/org/bytedream/untisbot/discord/DiscordCommandListener.java
Normal file
665
src/org/bytedream/untisbot/discord/DiscordCommandListener.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
40
src/org/bytedream/untisbot/language.json
Normal file
40
src/org/bytedream/untisbot/language.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
21
src/org/bytedream/untisbot/resources/logback.xml
Normal file
21
src/org/bytedream/untisbot/resources/logback.xml
Normal 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>
|
89
src/org/bytedream/untisbot/untis/CheckCallback.java
Normal file
89
src/org/bytedream/untisbot/untis/CheckCallback.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
171
src/org/bytedream/untisbot/untis/TimetableChecker.java
Normal file
171
src/org/bytedream/untisbot/untis/TimetableChecker.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user