Initial commit

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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