commit e27bccc83686f64bdbefaa36e1b3d7c3fd09c055
Author: ByteDream <bytedream@protonmail.com>
Date:   Sat Nov 14 00:14:31 2020 +0100

    Initial commit

diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..79fd7f6
--- /dev/null
+++ b/Dockerfile
@@ -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"]
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ecf5dcd
--- /dev/null
+++ b/README.md
@@ -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.**
\ No newline at end of file
diff --git a/dockerfiles/database.sql b/dockerfiles/database.sql
new file mode 100644
index 0000000..25612de
--- /dev/null
+++ b/dockerfiles/database.sql
@@ -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;
diff --git a/dockerfiles/run.sh b/dockerfiles/run.sh
new file mode 100755
index 0000000..a55e75c
--- /dev/null
+++ b/dockerfiles/run.sh
@@ -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
\ No newline at end of file
diff --git a/src/org/bytedream/untisbot/Crypt.java b/src/org/bytedream/untisbot/Crypt.java
new file mode 100644
index 0000000..4184477
--- /dev/null
+++ b/src/org/bytedream/untisbot/Crypt.java
@@ -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);
+    }
+
+}
diff --git a/src/org/bytedream/untisbot/Main.java b/src/org/bytedream/untisbot/Main.java
new file mode 100644
index 0000000..bb95612
--- /dev/null
+++ b/src/org/bytedream/untisbot/Main.java
@@ -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;
+    }
+
+}
diff --git a/src/org/bytedream/untisbot/Utils.java b/src/org/bytedream/untisbot/Utils.java
new file mode 100644
index 0000000..8403475
--- /dev/null
+++ b/src/org/bytedream/untisbot/Utils.java
@@ -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;
+    }
+
+}
diff --git a/src/org/bytedream/untisbot/data/Data.java b/src/org/bytedream/untisbot/data/Data.java
new file mode 100644
index 0000000..e7119bd
--- /dev/null
+++ b/src/org/bytedream/untisbot/data/Data.java
@@ -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;
+        }
+
+    }
+
+}
diff --git a/src/org/bytedream/untisbot/data/DataConnector.java b/src/org/bytedream/untisbot/data/DataConnector.java
new file mode 100644
index 0000000..3a52487
--- /dev/null
+++ b/src/org/bytedream/untisbot/data/DataConnector.java
@@ -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);
+            }
+        }
+    }
+
+}
diff --git a/src/org/bytedream/untisbot/data/StoreType.java b/src/org/bytedream/untisbot/data/StoreType.java
new file mode 100644
index 0000000..afe1a9d
--- /dev/null
+++ b/src/org/bytedream/untisbot/data/StoreType.java
@@ -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
+}
diff --git a/src/org/bytedream/untisbot/discord/Discord.java b/src/org/bytedream/untisbot/discord/Discord.java
new file mode 100644
index 0000000..263c4cf
--- /dev/null
+++ b/src/org/bytedream/untisbot/discord/Discord.java
@@ -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();
+    }
+
+}
diff --git a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java
new file mode 100644
index 0000000..b9e8cd6
--- /dev/null
+++ b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java
@@ -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());
+    }
+}
diff --git a/src/org/bytedream/untisbot/language.json b/src/org/bytedream/untisbot/language.json
new file mode 100644
index 0000000..e859b24
--- /dev/null
+++ b/src/org/bytedream/untisbot/language.json
@@ -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"
+  }
+}
\ No newline at end of file
diff --git a/src/org/bytedream/untisbot/resources/logback.xml b/src/org/bytedream/untisbot/resources/logback.xml
new file mode 100644
index 0000000..f636472
--- /dev/null
+++ b/src/org/bytedream/untisbot/resources/logback.xml
@@ -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>
\ No newline at end of file
diff --git a/src/org/bytedream/untisbot/untis/CheckCallback.java b/src/org/bytedream/untisbot/untis/CheckCallback.java
new file mode 100644
index 0000000..43324cf
--- /dev/null
+++ b/src/org/bytedream/untisbot/untis/CheckCallback.java
@@ -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;
+    }
+}
diff --git a/src/org/bytedream/untisbot/untis/TimetableChecker.java b/src/org/bytedream/untisbot/untis/TimetableChecker.java
new file mode 100644
index 0000000..d2ce439
--- /dev/null
+++ b/src/org/bytedream/untisbot/untis/TimetableChecker.java
@@ -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;
+    }
+
+}