From 88ad913bcaf74387559f4cb6b746bf5c34d99f9b Mon Sep 17 00:00:00 2001 From: ByteDream Date: Fri, 4 Dec 2020 18:19:59 +0100 Subject: [PATCH 01/12] Moved commands to extra function; added `timetable`, `start`, `stop` commands; general changes --- Dockerfile | 0 LICENCE | 0 README.md | 13 +- files/database.sql | 0 src/org/bytedream/untisbot/Crypt.java | 0 src/org/bytedream/untisbot/Main.java | 0 src/org/bytedream/untisbot/Utils.java | 0 src/org/bytedream/untisbot/data/Data.java | 2 +- .../untisbot/data/DataConnector.java | 0 .../bytedream/untisbot/data/StoreType.java | 0 .../bytedream/untisbot/discord/Discord.java | 0 .../discord/DiscordCommandListener.java | 725 ++++++++++++------ src/org/bytedream/untisbot/language.json | 46 +- .../bytedream/untisbot/resources/logback.xml | 0 .../untisbot/untis/CheckCallback.java | 0 .../untisbot/untis/TimetableChecker.java | 17 +- 16 files changed, 516 insertions(+), 287 deletions(-) mode change 100644 => 100755 Dockerfile mode change 100644 => 100755 LICENCE mode change 100644 => 100755 README.md mode change 100644 => 100755 files/database.sql mode change 100644 => 100755 src/org/bytedream/untisbot/Crypt.java mode change 100644 => 100755 src/org/bytedream/untisbot/Main.java mode change 100644 => 100755 src/org/bytedream/untisbot/Utils.java mode change 100644 => 100755 src/org/bytedream/untisbot/data/Data.java mode change 100644 => 100755 src/org/bytedream/untisbot/data/DataConnector.java mode change 100644 => 100755 src/org/bytedream/untisbot/data/StoreType.java mode change 100644 => 100755 src/org/bytedream/untisbot/discord/Discord.java mode change 100644 => 100755 src/org/bytedream/untisbot/discord/DiscordCommandListener.java mode change 100644 => 100755 src/org/bytedream/untisbot/language.json mode change 100644 => 100755 src/org/bytedream/untisbot/resources/logback.xml mode change 100644 => 100755 src/org/bytedream/untisbot/untis/CheckCallback.java mode change 100644 => 100755 src/org/bytedream/untisbot/untis/TimetableChecker.java diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 diff --git a/LICENCE b/LICENCE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index e0a07a3..e807197 --- a/README.md +++ b/README.md @@ -23,15 +23,18 @@ To see all available commands and get infos about it, simply type `help`. | command | usage | example | default | | --- | --- | --- | --- | -| `channel` | In the channel where this command is entered, the bot shows the timetable changes | `channel` | - -| `clear` | Clears the given untis data, given from the `data` command | `clear` | - -| `data [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. | `data myname secure https://example.webuntis.com/WebUntis/?school=example#/basic/main 12AB` | - | -| `help` | Displays help to a given command | `help data` | - | +| `channel` | In the channel where this command is entered, the bot shows the timetable changes | `channel` | - | +| `clear` | Clears the given untis data, given from the `data` command | `clear` | - | +| `data [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`). As `class name` you can use any class from your school. If it isn't specified, the bot tries to get the default class which is assigned to the given account. | `data myname secure https://example.webuntis.com/WebUntis/?school=example#/basic/main 12AB` | - | +| `help [command]` | Displays help to a given command | `help data` | - | | `language ` | Changes the language in which the timetable information are displayed. Currently only `de` (german) and `en` (english) are supported | `language de` | `en` | | `prefix ` | Changes the prefix with which commands are called | `prefix $` | `!untis ` | | `stats` | Displays a message with some stats (total cancelled lessons, etc.) | `stats` | - | +| `start` | Starts the stopped timetable listener. Only works if data was set with the `data` command | `start` | - | +| `stop` | Stops timetable listening. Only works if data was set with the `data` command | `stop` | - | +| `timetable [date] [class name]` | Displays the timetable for a specific date. As `date` you can use 3 formats. 1: Only the day (`12`); 2. Day and month (`13.04`); 3. Day, month and year (`31.12.2020`). Only works if data was set with the `data` command. If no date is given, the timetable for the current date is displayed. As `class name` you can use any class from your school. If class is not given, the class which was assigned in the `data` command is used | `timetable 11.11` | - | -Note: All commands except for `help ` and `` can only be executed by a member with admin rights. +Note: All commands except for `help [command]`, `timetable [] [class]` and `` can only be executed by a member with admin rights. ## Self-hosting diff --git a/files/database.sql b/files/database.sql old mode 100644 new mode 100755 diff --git a/src/org/bytedream/untisbot/Crypt.java b/src/org/bytedream/untisbot/Crypt.java old mode 100644 new mode 100755 diff --git a/src/org/bytedream/untisbot/Main.java b/src/org/bytedream/untisbot/Main.java old mode 100644 new mode 100755 diff --git a/src/org/bytedream/untisbot/Utils.java b/src/org/bytedream/untisbot/Utils.java old mode 100644 new mode 100755 diff --git a/src/org/bytedream/untisbot/data/Data.java b/src/org/bytedream/untisbot/data/Data.java old mode 100644 new mode 100755 index e7119bd..28e574c --- a/src/org/bytedream/untisbot/data/Data.java +++ b/src/org/bytedream/untisbot/data/Data.java @@ -72,7 +72,7 @@ public class Data { } public Short getKlasseId() { - return (short) data[6]; + return (Short) data[6]; } public Long getChannelId() { diff --git a/src/org/bytedream/untisbot/data/DataConnector.java b/src/org/bytedream/untisbot/data/DataConnector.java old mode 100644 new mode 100755 diff --git a/src/org/bytedream/untisbot/data/StoreType.java b/src/org/bytedream/untisbot/data/StoreType.java old mode 100644 new mode 100755 diff --git a/src/org/bytedream/untisbot/discord/Discord.java b/src/org/bytedream/untisbot/discord/Discord.java old mode 100644 new mode 100755 diff --git a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java old mode 100644 new mode 100755 index 81667a9..5af49a7 --- a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java +++ b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java @@ -12,7 +12,6 @@ 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.Klassen; import org.bytedream.untis4j.responseObjects.Teachers; @@ -39,12 +38,11 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; /** * Adapter to handle all events * - * @version 1.0 + * @version 1.1 * @since 1.0 */ public class DiscordCommandListener extends ListenerAdapter { @@ -53,6 +51,7 @@ public class DiscordCommandListener extends ListenerAdapter { private final DataConnector.Stats statsDataConnector; private final JSONObject languages; + private final HashMap allUntisSessions = new HashMap<>(); private final HashMap allTimetableChecker = new HashMap<>(); private final Logger logger = Main.getLogger(); @@ -77,13 +76,411 @@ public class DiscordCommandListener extends ListenerAdapter { embedBuilder.setColor(Color.GREEN); } + /** + * Runs a command + * + * @param guild guild from which the command came + * @param channel channel from which the command came + * @param permission if true, commands which needs (admin) permission to run, can be executed + * @param command command to execute + * @param args extra arguments for the command + * + * @since 1.1 + */ + private void runCommand(Guild guild, TextChannel channel, boolean permission, String command, String[] args) { + long guildId = guild.getIdLong(); + String guildName = guild.getName(); + Data.Guild data = guildDataConnector.get(guildId); + + switch (command) { + case "timetable": // `timetable [day | [day.month] | [day.month.year]] [class name]` command + if (args.length < 3) { + Session session = allUntisSessions.get(guildId); + LocalDate now = LocalDate.now(); + Short classId = null; + LocalDate date = null; + + if (data.getServer() == null && data.getSchool() == null) { + channel.sendMessage("Please set your data with the `data` command first, before you use this command. Type `" + data.getPrefix() + "help data` to get information").queue(); + return; + } + + if (args.length == 0) { + classId = data.getKlasseId(); + date = now; + } else { + for (String arg : args) { + if (date == null) { + Integer number = null; + try { + number = Integer.parseInt(arg); + } catch (NumberFormatException ignore) { + } + if (number != null && number <= 31 && number >= 1) { + date = LocalDate.of(now.getYear(), now.getMonth(), number); + continue; + } else if (arg.contains(".")) { + String[] splitDate = args[0].split("\\."); + try { + switch (splitDate.length) { + case 1: + date = LocalDate.of(now.getYear(), now.getMonth(), Integer.parseInt(splitDate[0])); + break; + case 2: + date = LocalDate.of(now.getYear(), Integer.parseInt(splitDate[1]), Integer.parseInt(splitDate[0])); + break; + case 3: + date = LocalDate.of(Integer.parseInt(splitDate[2]), Integer.parseInt(splitDate[1]), Integer.parseInt(splitDate[0])); + break; + default: + channel.sendMessage("Couldn't get date. Type `" + data.getPrefix() + "help timetable` for help").queue(); + } + continue; + } catch (NumberFormatException e) { + channel.sendMessage("Couldn't get date. Type `" + data.getPrefix() + "help timetable` for help").queue(); + return; + } + } + } + System.out.println("sss"); + try { + classId = (short) session.getKlassen().findByName(arg).getId(); + } catch (IOException e) { + logger.warn(guildId + " ran into an exception while trying to receive classes for a timetable", e); + channel.sendMessage("Couldn't search the class. Try again (later) or contact my author <@650417934073593886>, if the problem won't go away").queue(); + } catch (NullPointerException e) { + channel.sendMessage("Couldn't find any class with the name '" + arg + "'").queue(); + } + } + } + EmbedBuilder embedBuilder = new EmbedBuilder(); + embedBuilder.setColor(new Color(138, 43, 226)); + JSONObject language; + if (data.getLanguage() == null) { + language = languages.getJSONObject("en"); + } else { + language = languages.getJSONObject(data.getLanguage()); + } + String className = "-"; + try { + className = session.getKlassen().findById(data.getKlasseId()).getName(); + } catch (IOException ignore) {} + String finalClassName = className; // yea java... + LocalDate finalDate = date; // yea java part two... + embedBuilder.setTitle(Utils.advancedFormat(language.getString("timetable-title"), new HashMap() {{ + put("class", finalClassName); + put("date", finalDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); + }})); + + LocalTime lastStartTime = LocalTime.MIN; + boolean multipleLessonAtOnce = false; + TreeMap> lessons = new TreeMap<>(); + try { + for (Timetable.Lesson lesson : Timetable.sortByStartTime(allUntisSessions.get(guildId).getTimetableFromKlasseId(date, date, classId))) { + lessons.putIfAbsent(lesson.getStartTime(), new ArrayList<>()); + lessons.get(lesson.getStartTime()).add(lesson); + if (lastStartTime.equals(lesson.getStartTime())) { + multipleLessonAtOnce = true; + } + lastStartTime = lesson.getStartTime(); + } + if (multipleLessonAtOnce) { + for (ArrayList listLessons: lessons.values()) { + for (Timetable.Lesson lesson: listLessons) { + embedBuilder.addField(Utils.advancedFormat(language.getString("timetable-lesson-title"), new HashMap() {{ + put("lesson-number", lesson.getTimeUnitObject().getName()); + put("start-time", lesson.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm"))); + put("end-time", lesson.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm"))); + }}), Utils.advancedFormat(language.getString("timetable-teachers"), new HashMap() {{ + put("teachers", String.join(", ", lesson.getTeachers().getFullNames())); + }}) + "\n" + Utils.advancedFormat(language.getString("timetable-subjects"), new HashMap() {{ + put("subjects", String.join(", ", lesson.getSubjects().getLongNames())); + }}) + "\n" + Utils.advancedFormat(language.getString("timetable-rooms"), new HashMap() {{ + put("rooms", String.join(", ", lesson.getRooms().getLongNames())); + }}), listLessons.size() > 1); + } + } + } else { + /*int halfSize = (int) Math.ceil((lessons.values().size() / 2)); + for (int i = 0; i <= halfSize; i++) { + int j = i + 1; + Timetable.Lesson lesson = ((ArrayList) lessons.values().toArray()[i]).get(0); + Timetable.Lesson[] leftRightLessons; + + if (j + halfSize > lessons.values().size() - 1) { + leftRightLessons = new Timetable.Lesson[]{lesson}; + } else { + leftRightLessons = new Timetable.Lesson[]{lesson, ((ArrayList) lessons.values().toArray()[j + halfSize]).get(0)}; + } + + for (Timetable.Lesson lesson1: leftRightLessons) { + embedBuilder.addField(Utils.advancedFormat(language.getString("timetable-lesson-title"), new HashMap() {{ + put("lesson-number", lesson1.getTimeUnitObject().getName()); + put("start-time", lesson1.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm"))); + put("end-time", lesson1.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm"))); + }}), Utils.advancedFormat(language.getString("timetable-teachers"), new HashMap() {{ + put("teachers", String.join(", ", lesson1.getTeachers().getFullNames())); + }}) + "\n" + Utils.advancedFormat(language.getString("timetable-subjects"), new HashMap() {{ + put("subjects", String.join(", ", lesson1.getSubjects().getLongNames())); + }}) + "\n" + Utils.advancedFormat(language.getString("timetable-rooms"), new HashMap() {{ + put("rooms", String.join(", ", lesson1.getRooms().getLongNames())); + }}), true); + } + + embedBuilder.addBlankField(true); + }*/ + for (ArrayList listLesson: lessons.values()) { + Timetable.Lesson lesson = listLesson.get(0); + embedBuilder.addField(Utils.advancedFormat(language.getString("timetable-lesson-title"), new HashMap() {{ + put("lesson-number", lesson.getTimeUnitObject().getName()); + put("start-time", lesson.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm"))); + put("end-time", lesson.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm"))); + }}), Utils.advancedFormat(language.getString("timetable-teachers"), new HashMap() {{ + put("teachers", String.join(", ", lesson.getTeachers().getFullNames())); + }}) + "\n" + Utils.advancedFormat(language.getString("timetable-subjects"), new HashMap() {{ + put("subjects", String.join(", ", lesson.getSubjects().getLongNames())); + }}) + "\n" + Utils.advancedFormat(language.getString("timetable-rooms"), new HashMap() {{ + put("rooms", String.join(", ", lesson.getRooms().getLongNames())); + }}), true); + } + } + channel.sendMessage(embedBuilder.build()).queue(); + } catch (IOException e) { + logger.warn(guildId + " ran into an exception while trying to receive a timetable", e); + channel.sendMessage("Couldn't get timetable. Try again (later) or contact my author <@650417934073593886>, if the problem won't go away").queue(); + } + } else { + channel.sendMessage("Wrong number of arguments were given (expected 0 or 1, got " + args.length + "), type `" + data.getPrefix() + "help timetable` for help").queue(); + } + return; + case "stats": // `stats` command + 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 mostMissedTeachers = new ArrayList<>(); + short missedLessons = 0; + for (Map.Entry 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 ` - 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 (expected 0, got " + args.length + "), type `" + data.getPrefix() + "help stats` for help").queue(); + } + return; + } + if (permission) { + 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 ` command + if (args.length >= 3 && args.length <= 4) { + if (dataUpdated.getOrDefault(guildId, LocalDateTime.MIN).plusMinutes(1).isAfter(LocalDateTime.now())) { + // this gives the server a little decay time and prevents additional load (because of the untis data encryption) caused by spamming + channel.sendMessage("The data was changed recently, try again in about one minute").queue(); + } else { + dataUpdated.put(guildId, LocalDateTime.now()); + String schoolName; + String className; + 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(); + className = session.getKlassen().findById(klasseId).getName(); + } else { + try { + Klassen.KlasseObject klasse = session.getKlassen().findByName(args[3]); + klasseId = (short) klasse.getId(); + className = klasse.getName(); + } catch (NullPointerException e) { + channel.sendMessage("❌ Cannot find the given class").queue(); + return; + } + } + allUntisSessions.putIfAbsent(guildId, session); + } catch (IOException e) { + channel.sendMessage("❌ The given login data is invalid").queue(); + return; + } + + 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 (data.isCheckActive()) { + Timer timer = allTimetableChecker.get(guildId); + allTimetableChecker.remove(guildId); + timer.cancel(); + timer.purge(); + runTimetableChecker(guild); + channel.sendMessage("✅ Updated data and restarted timetable listening for class " + className).queue(); + } else if (data.getLastChecked() != null) { + channel.sendMessage("✅ Updated data. Timetable listening were manually stopped a while ago. To re-enable it, type `" + data.getPrefix() + "start`").queue(); + } else { + runTimetableChecker(guild); + channel.sendMessage("✅ Timetable listening has been started for class " + className).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 ` 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 ` 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; + case "start": // `start` command + if (args.length == 0) { + if (data.isCheckActive()) { + channel.sendMessage("Timetable listening already started").queue(); + } else { + runTimetableChecker(guild); + logger.info(guildName + " started timetable listening"); + channel.sendMessage("✅ Timetable listening has been started").queue(); + } + } else { + channel.sendMessage("Wrong number of arguments were given (expected 0, got " + args.length + "), type `" + data.getPrefix() + "help start` for help").queue(); + } + break; + case "stop": // `stop` command + if (args.length == 0) { + if (data.isCheckActive()) { + Timer timer = allTimetableChecker.get(guildId); + allTimetableChecker.remove(guildId); + timer.cancel(); + timer.purge(); + logger.info(guildName + " stopped timetable listening"); + channel.sendMessage("Stopped timetable listening. Use `" + data.getPrefix() + "start` to re-enable it").queue(); + } else { + channel.sendMessage("Timetable listening is already stopped").queue(); + } + } else { + channel.sendMessage("Wrong number of arguments were given (expected 0, got " + args.length + "), type `" + data.getPrefix() + "help stop` for help").queue(); + } + break; + case "help": // is handled in DiscordCommandListener.onMessageReceived() + return; + default: + channel.sendMessage("Unknown command").queue(); + } + } + + } + /** * 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) { + private void runTimetableChecker(Guild guild) { long guildId = guild.getIdLong(); String guildName = guild.getName(); Timer timer = new Timer(); @@ -99,20 +496,7 @@ public class DiscordCommandListener extends ListenerAdapter { "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(guildName + " 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(guildName + " 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; - } + timetableChecker = new TimetableChecker(allUntisSessions.get(guildId), data.getKlasseId()); timer.scheduleAtFixedRate(new TimerTask() { private long latestImportTime = 0; @@ -175,7 +559,7 @@ public class DiscordCommandListener extends ListenerAdapter { EmbedBuilder embedBuilder = new EmbedBuilder(); embedBuilder.setColor(Color.CYAN); - embedBuilder.setTitle(Utils.advancedFormat(language.getString("title"), new HashMap() {{ + embedBuilder.setTitle(Utils.advancedFormat(language.getString("change-title"), new HashMap() {{ put("weekday", language.getString(localDate.getDayOfWeek().name().toLowerCase())); put("date", localDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); }})); @@ -183,7 +567,7 @@ public class DiscordCommandListener extends ListenerAdapter { for (Timetable.Lesson lesson : cancelledLessons) { TimeUnits.TimeUnitObject timeUnitObject = lesson.getTimeUnitObject(); HashMap formatMap = new HashMap() {{ - put("lesson-name", timeUnitObject.getName()); + put("lesson-number", 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"))); @@ -200,8 +584,8 @@ public class DiscordCommandListener extends ListenerAdapter { Timetable.Lesson to = lesson[0]; Timetable.Lesson from = lesson[1]; HashMap formatMap = new HashMap() {{ - put("from-lesson-name", from.getTimeUnitObject().getName()); - put("to-lesson-name", to.getTimeUnitObject().getName()); + put("from-lesson-number", from.getTimeUnitObject().getName()); + put("to-lesson-number", 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"))); @@ -216,7 +600,7 @@ public class DiscordCommandListener extends ListenerAdapter { for (Timetable.Lesson lesson : notCancelledLessons) { TimeUnits.TimeUnitObject timeUnitObject = lesson.getTimeUnitObject(); HashMap formatMap = new HashMap() {{ - put("lesson-name", timeUnitObject.getName()); + put("lesson-number", 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"))); @@ -233,8 +617,8 @@ public class DiscordCommandListener extends ListenerAdapter { Timetable.Lesson from = lesson[0]; Timetable.Lesson to = lesson[1]; HashMap formatMap = new HashMap() {{ - put("from-lesson-name", from.getTimeUnitObject().getName()); - put("to-lesson-name", to.getTimeUnitObject().getName()); + put("from-lesson-number", from.getTimeUnitObject().getName()); + put("to-lesson-number", 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"))); @@ -325,7 +709,7 @@ public class DiscordCommandListener extends ListenerAdapter { } } } catch (IOException e) { - logger.info("Running main through IOException (" + e.getCause() + ")"); + logger.info("Running main through IOException", e); main(); } } @@ -334,33 +718,6 @@ public class DiscordCommandListener extends ListenerAdapter { logger.info(guildName + " started timetable listening"); } - @Override - public void onReady(ReadyEvent event) { - ArrayList 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) { Thread t = new Thread(() -> { @@ -374,204 +731,14 @@ public class DiscordCommandListener extends ListenerAdapter { // 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 mostMissedTeachers = new ArrayList<>(); - short missedLessons = 0; - for (Map.Entry 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 ` - 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 ` command - if (args.length >= 3 && args.length <= 4) { - if (dataUpdated.getOrDefault(guildId, LocalDateTime.MIN).plusMinutes(1).isAfter(LocalDateTime.now())) { - // this gives the server a little decay time and prevents additional load (because of the untis data encryption) caused by spamming - channel.sendMessage("The data was changed recently, try again in about one minute").queue(); - } else { - dataUpdated.put(guildId, LocalDateTime.now()); - String schoolName; - String className; - 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(); - className = session.getKlassen().findById(klasseId).getName(); - } else { - try { - Klassen.KlasseObject klasse = session.getKlassen().findByName(args[3]); - klasseId = (short) klasse.getId(); - className = klasse.getName(); - } 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); - allTimetableChecker.remove(guildId); - timer.cancel(); - timer.purge(); - runTimetableChecker(guild); - channel.sendMessage("✅ Updated data and restarted timetable listening for class " + className).queue(); - } else { - runTimetableChecker(guild); - channel.sendMessage("✅ Timetable listening has been started for class " + className).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 ` 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 ` 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: - - } - } + runCommand(event.getGuild(), event.getChannel(), event.getMember().getPermissions().contains(Permission.ADMINISTRATOR), command, args); } catch (NullPointerException ignore) { } }); @@ -588,14 +755,13 @@ public class DiscordCommandListener extends ListenerAdapter { 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")) { + System.out.println("sss"); return; } } else if (message.equals("help") || message.startsWith("help ")) { @@ -611,7 +777,7 @@ public class DiscordCommandListener extends ListenerAdapter { String[] args = Arrays.copyOfRange(splitMessage, 1, splitMessage.length); String help = "Use `" + prefix + "help ` to get help / information about a command.\n\n" + "All available commands are:\n" + - "`channel` `clear` `data` `help` `language` `prefix` `stats`"; + "`channel` `clear` `data` `help` `language` `prefix` `stats` `start` `stop` `timetable`"; 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) { @@ -620,7 +786,7 @@ public class DiscordCommandListener extends ListenerAdapter { String title; String description; String example; - String _default = null; + String default_ = null; switch (args[0]) { case "channel": title = "`channel` command"; @@ -639,7 +805,7 @@ public class DiscordCommandListener extends ListenerAdapter { "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`"; + default_ = "`en`"; break; case "help": title = "`help ` command"; @@ -650,35 +816,88 @@ public class DiscordCommandListener extends ListenerAdapter { title = "`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`"; + default_ = "`en`"; break; case "prefix": title = "`prefix ` command"; description = "Changes the prefix with which commands are called"; example = "`prefix $`"; - _default = "`!untis `"; + default_ = "`!untis `"; break; case "stats": title = "`stats` command"; description = "Displays a message with some stats (total cancelled lessons, etc.)"; example = "`stats`"; break; + case "start": + title = "`start` command"; + description = "Starts the stopped timetable listener. Only works if data was set with the `data` command"; + example = "`start`"; + break; + case "stop": + title = "`stop` command"; + description = "Stops timetable listening. Only works if data was set with the `data` command"; + example = "`stop`"; + break; + case "timetable": + title = "`timetable [date] [class name]` command"; + description = "Displays the timetable for a specific date. As `date` you can use 3 formats." + + "1: Only the day (`12`); 2. Day and month (`13.04`); 3. Day, month and year (`31.12.2020`)." + + "Only works if data was set with the `data` command. If no date is given, the timetable for the current date is displayed." + + "As `class name` you can use any class from your school. If class is not given, the class which was assigned in the `data` command is used"; + example = "`timetable 11.11`"; + break; default: channel.sendMessage("Unknown command was given. " + help).queue(); return; } + EmbedBuilder embedBuilder = new EmbedBuilder(); 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); + if (default_ != null) { + embedBuilder.addField("Default", default_, false); } embedBuilder.setFooter("`<>` = required; `[]` = optional"); channel.sendMessage(embedBuilder.build()).queue(); } }); t.setName("Message handler"); + t.start(); + } + + @Override + public void onReady(ReadyEvent event) { + ArrayList 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()) { + Data.Guild data = guildDataConnector.get(guildId); + try { + allUntisSessions.put(guildId, Session.login(data.getUsername(), data.getPassword(), data.getServer(), data.getSchool())); + runTimetableChecker(guild); + } catch (IOException e) { + logger.error("Error for guild " + guild.getName() + " (" + guildId + ") while setting up untis session", e); + } + } + + 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 diff --git a/src/org/bytedream/untisbot/language.json b/src/org/bytedream/untisbot/language.json old mode 100644 new mode 100755 index e859b24..e2f129b --- a/src/org/bytedream/untisbot/language.json +++ b/src/org/bytedream/untisbot/language.json @@ -1,15 +1,20 @@ { "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", + "timetable-title": "Stundenplan für Klasse {class} am {date}", + "timetable-lesson-title": "{lesson-number}. Stunde ({start-time} - {end-time})", + "timetable-teachers": "_Lehrer_: {teachers}", + "timetable-subjects": "_Unterricht_: {subjects}", + "timetable-rooms": "_Raum_: {rooms}", + "change-title": "Stunden Ausfall Information für {weekday}, den {date}", + "cancelled-title": "Ausfall {lesson-number}. Stunde ({start-time} Uhr - {end-time} Uhr)", + "cancelled-body": "Ausfall bei {teachers}, in {subjects}, in der {lesson-number}. Stunde", + "moved-title": "{from-lesson-number}. Stunde wird zur {to-lesson-number}. Stunde umverlegt", + "moved-body": "Die {from-lesson-number}. Stunde bei {teachers} in {subjects} wird zur {to-lesson-number}. Stunde umverlegt", + "not-cancelled-title": "KEIN Ausfall {lesson-number}. Stunde ({start-time} Uhr - {end-time} Uhr)", + "not-cancelled-body": "KEIN Ausfall bei {teachers}, in {subjects}, in der {lesson-number}. Stunde", + "not-moved-title": "{from-lesson-number}. Stunde wird NICHT zur {to-lesson-number}. Stunde umverlegt", + "not-moved-body": "Die {from-lesson-number}. Stunde bei {teachers} wird NICHT zur {to-lesson-number}. Stunde umverlegt", "monday": "Montag", "tuesday": "Dienstag", "wednesday": "Mittwoch", @@ -20,15 +25,20 @@ }, "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", + "timetable-title": "Timetable for class {class} on {date}", + "timetable-lesson-title": "{lesson-number}. lesson ({start-time} - {end-time})", + "timetable-teachers": "_Teacher_: {teachers}", + "timetable-subjects": "_Subject_: {subjects}", + "timetable-rooms": "_Room_: {rooms}", + "change-title": "Irregular lesson information for {weekday}, {date}", + "cancelled-title": "Cancelled {lesson-number}. lesson ({start-time} - {end-time})", + "cancelled-body": "The {lesson-number}. lesson with {teachers} in {subjects} is cancelled", + "moved-title": "The {from-lesson-number}. lesson is moved to {to-lesson-number}. lesson", + "moved-body": "The {from-lesson-number}. lesson with {teachers} in {subjects} is moved to the {to-lesson-number}. lesson", + "not-cancelled-title": "{lesson-number}. lesson ({start-time} - {end-time}) is NO cancelled", + "not-cancelled-body": "The {lesson-number}. lesson with {teachers} in {subjects} is NOT cancelled", + "not-moved-title": "The {from-lesson-number}. lesson is NOT moved to {to-lesson-number}.", + "not-moved-body": "The {from-lesson-number}. lesson with {teachers} in {subjects} is NOT moved to the {to-lesson-number}. lesson", "monday": "monday", "tuesday": "tuesday", "wednesday": "wednesday", diff --git a/src/org/bytedream/untisbot/resources/logback.xml b/src/org/bytedream/untisbot/resources/logback.xml old mode 100644 new mode 100755 diff --git a/src/org/bytedream/untisbot/untis/CheckCallback.java b/src/org/bytedream/untisbot/untis/CheckCallback.java old mode 100644 new mode 100755 diff --git a/src/org/bytedream/untisbot/untis/TimetableChecker.java b/src/org/bytedream/untisbot/untis/TimetableChecker.java old mode 100644 new mode 100755 index aaa48df..c85fff2 --- a/src/org/bytedream/untisbot/untis/TimetableChecker.java +++ b/src/org/bytedream/untisbot/untis/TimetableChecker.java @@ -19,7 +19,7 @@ import java.util.HashSet; public class TimetableChecker { private final Session session; - private final int klasseId; + private final int classId; private final LocalDate[] cancelledLessonsDay = new LocalDate[7]; private final LocalDate[] ignoredLessonsDay = new LocalDate[7]; private final LocalDate[] movedLessonsDay = new LocalDate[7]; @@ -30,16 +30,13 @@ public class TimetableChecker { /** * 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 + * @param session user session + * @param classId id of the class to check the timetable * @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; + public TimetableChecker(Session session, int classId) { + this.session = session; + this.classId = classId; for (LocalDate[] localDates : new HashSet() {{ add(cancelledLessonsDay); @@ -65,7 +62,7 @@ public class TimetableChecker { * @since 1.0 */ public CheckCallback check(LocalDate dateToCheck) throws IOException { - Timetable timetable = session.getTimetableFromKlasseId(dateToCheck, dateToCheck, klasseId); + Timetable timetable = session.getTimetableFromKlasseId(dateToCheck, dateToCheck, classId); timetable.sortByStartTime(); int dayOfWeekInArray = dateToCheck.getDayOfWeek().getValue() - 1; From c34f05e9d449c80a545a25c8da5f791b37f722fe Mon Sep 17 00:00:00 2001 From: ByteDream Date: Fri, 4 Dec 2020 22:01:21 +0100 Subject: [PATCH 02/12] Release preparations --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e807197..651e41a 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ ### 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. +**UntisBot** is a java programmed discord bot, which uses the [WebUntis](https://webuntis.com/) timetable software / api +to receive the timetable for a specific date, automatically send messages when the timetable from a given account or class changes, displays absent teachers, and more! You can invite the bot right [here](https://discord.com/api/oauth2/authorize?client_id=768841979433451520&permissions=268437504&scope=bot) or [host it yourself](#Self-hosting).

@@ -65,7 +66,7 @@ When you run the bot manually you can choose from 2 types of data storage: ### In-memory storage -In memory data storage is pretty simple: Just download the [jar](https://github.com/ByteDream/untisbot-discord/releases/tag/v1.0/UntisBot-1.0.jar) and run it with `java -jar UntisBot-1.0.jar token=`. +In memory data storage is pretty simple: Just download the [jar](https://github.com/ByteDream/untisbot-discord/releases/tag/v1.1/UntisBot-1.0.jar) and run it with `java -jar UntisBot-1.0.jar 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). @@ -82,7 +83,7 @@ To set up the database, you have to execute the following command and replace `< mysql -u -p -e "CREATE DATABASE Untis; USE Untis; $(wget -qO- https://raw.githubusercontent.com/ByteDream/untisbot-discord/master/files/database.sql)" ``` -Now you have set up the database and are ready to go. Download the [jar](https://github.com/ByteDream/untisbot-discord/releases/tag/v1.0/UntisBot-1.0.jar) and run it with `java -jar UntisBot-1.0.jar mariadb`. +Now you have set up the database and are ready to go. Download the [jar](https://github.com/ByteDream/untisbot-discord/releases/tag/v1.1/UntisBot-1.0.jar) and run it with `java -jar UntisBot-1.0.jar mariadb`. ## Run options @@ -169,7 +170,7 @@ If you want to add a language which isn't supported you can - [Database 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.** +**_Note_: The [UntisBot jar file](https://github.com/ByteDream/untisbot-discord/releases/tag/v1.1/UntisBot-1.0.jar) and the [Dockerfile](Dockerfile) are containing all dependencies.** ## License From f19369d338b29d9e9bc9052cd9910e2870a94ec1 Mon Sep 17 00:00:00 2001 From: ByteDream Date: Fri, 4 Dec 2020 23:21:13 +0100 Subject: [PATCH 03/12] Added update rich presence --- src/org/bytedream/untisbot/Main.java | 7 +++ .../bytedream/untisbot/discord/Discord.java | 59 ++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/org/bytedream/untisbot/Main.java b/src/org/bytedream/untisbot/Main.java index 6a870a6..80a416c 100755 --- a/src/org/bytedream/untisbot/Main.java +++ b/src/org/bytedream/untisbot/Main.java @@ -1,3 +1,8 @@ +/** + * @author ByteDream + * @version 1.1 + */ + package org.bytedream.untisbot; import ch.qos.logback.classic.Logger; @@ -21,6 +26,8 @@ import java.util.HashSet; */ public class Main { + public static final String version = "1.1"; + private static Logger logger; private static Connection connection; diff --git a/src/org/bytedream/untisbot/discord/Discord.java b/src/org/bytedream/untisbot/discord/Discord.java index 263c4cf..a087c4e 100755 --- a/src/org/bytedream/untisbot/discord/Discord.java +++ b/src/org/bytedream/untisbot/discord/Discord.java @@ -1,11 +1,27 @@ package org.bytedream.untisbot.discord; +import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.Activity; import org.bytedream.untisbot.Crypt; +import org.bytedream.untisbot.Main; import org.bytedream.untisbot.data.StoreType; import org.json.JSONObject; +import org.json.JSONTokener; import javax.security.auth.login.LoginException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; /** * Base class to start the bot @@ -16,6 +32,7 @@ import javax.security.auth.login.LoginException; public class Discord { private final JDABuilder jdaBuilder; + private JDA jda = null; /** * Configures the bot to make it ready to launch @@ -27,9 +44,49 @@ public class Discord { */ public Discord(String token, StoreType storeType, String encryptPassword, JSONObject languages) { jdaBuilder = JDABuilder.createDefault(token); + updateRichPresence(); jdaBuilder.addEventListeners(new DiscordCommandListener(storeType, new Crypt(encryptPassword), languages)); } + /** + * Show rich presence if a bot update was released within then last 24 hours + * @since 1.1 + */ + private void updateRichPresence() { + try { + HttpURLConnection connection = (HttpURLConnection) new URL("https://api.github.com/repos/ByteDream/untisbot-discord/releases/tags/v" + Main.version).openConnection(); + connection.connect(); + + if (connection.getResponseCode() == 200) { + JSONTokener jsonTokener = new JSONTokener(new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))); + JSONObject releaseInfos = new JSONObject(jsonTokener); + String releaseTime = releaseInfos.getString("published_at"); + LocalDateTime releaseDateTime = LocalDateTime.parse(releaseTime.substring(0, releaseTime.length() - 1)); + LocalDateTime now = LocalDateTime.now(); + + if (ChronoUnit.DAYS.between(now, releaseDateTime) == 0) { + if (jda != null) { + jda.getPresence().setActivity(Activity.playing("update " + Main.version + " („\\(^_^)/“)")); + } else { + jdaBuilder.setActivity(Activity.playing("update " + Main.version + " („\\(^_^)/“)")); + } + new Timer().schedule(new TimerTask() { + @Override + public void run() { + if (jda != null) { + jda.getPresence().setActivity(null); + } else { + jdaBuilder.setActivity(null); + } + } + }, TimeUnit.DAYS.toMillis(1) - ChronoUnit.MILLIS.between(now, releaseDateTime)); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + /** * Starts the bot * @@ -37,7 +94,7 @@ public class Discord { * @since 1.0 */ public void start() throws LoginException { - jdaBuilder.build(); + jda = jdaBuilder.build(); } } From 575058f541df7618ae3e4ce919d4309966e2cd83 Mon Sep 17 00:00:00 2001 From: ByteDream Date: Fri, 4 Dec 2020 23:22:38 +0100 Subject: [PATCH 04/12] Add `info` command --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 651e41a..551022e 100755 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ 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 | `channel` | - | | `clear` | Clears the given untis data, given from the `data` command | `clear` | - | | `data [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`). As `class name` you can use any class from your school. If it isn't specified, the bot tries to get the default class which is assigned to the given account. | `data myname secure https://example.webuntis.com/WebUntis/?school=example#/basic/main 12AB` | - | +| `info` | Displays information about the bot | `info` | - | | `help [command]` | Displays help to a given command | `help data` | - | | `language ` | Changes the language in which the timetable information are displayed. Currently only `de` (german) and `en` (english) are supported | `language de` | `en` | | `prefix ` | Changes the prefix with which commands are called | `prefix $` | `!untis ` | From 5d4e5e634c903450a06ebcaed68dc9663fb89694 Mon Sep 17 00:00:00 2001 From: ByteDream Date: Fri, 4 Dec 2020 23:25:56 +0100 Subject: [PATCH 05/12] Simplified `timetable` command; `stats` and `help` commands rebased --- .../discord/DiscordCommandListener.java | 386 ++++++++++-------- 1 file changed, 214 insertions(+), 172 deletions(-) diff --git a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java index 5af49a7..c48ee1f 100755 --- a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java +++ b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java @@ -13,6 +13,7 @@ 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.Session; +import org.bytedream.untis4j.UntisUtils; import org.bytedream.untis4j.responseObjects.Klassen; import org.bytedream.untis4j.responseObjects.Teachers; import org.bytedream.untis4j.responseObjects.TimeUnits; @@ -97,26 +98,23 @@ public class DiscordCommandListener extends ListenerAdapter { if (args.length < 3) { Session session = allUntisSessions.get(guildId); LocalDate now = LocalDate.now(); - Short classId = null; - LocalDate date = null; + Short classId = data.getKlasseId(); + LocalDate date = now; if (data.getServer() == null && data.getSchool() == null) { channel.sendMessage("Please set your data with the `data` command first, before you use this command. Type `" + data.getPrefix() + "help data` to get information").queue(); return; } - if (args.length == 0) { - classId = data.getKlasseId(); - date = now; - } else { + if (args.length != 0) { for (String arg : args) { - if (date == null) { + if (date.equals(now)) { Integer number = null; try { number = Integer.parseInt(arg); } catch (NumberFormatException ignore) { } - if (number != null && number <= 31 && number >= 1) { + if (number != null && number > 1 && number < 31) { date = LocalDate.of(now.getYear(), now.getMonth(), number); continue; } else if (arg.contains(".")) { @@ -142,14 +140,15 @@ public class DiscordCommandListener extends ListenerAdapter { } } } - System.out.println("sss"); try { classId = (short) session.getKlassen().findByName(arg).getId(); } catch (IOException e) { logger.warn(guildId + " ran into an exception while trying to receive classes for a timetable", e); channel.sendMessage("Couldn't search the class. Try again (later) or contact my author <@650417934073593886>, if the problem won't go away").queue(); + return; } catch (NullPointerException e) { channel.sendMessage("Couldn't find any class with the name '" + arg + "'").queue(); + return; } } } @@ -172,78 +171,67 @@ public class DiscordCommandListener extends ListenerAdapter { put("date", finalDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); }})); + LinkedHashMap> lessons = new LinkedHashMap<>(); + boolean noMultiples = true; LocalTime lastStartTime = LocalTime.MIN; - boolean multipleLessonAtOnce = false; - TreeMap> lessons = new TreeMap<>(); try { for (Timetable.Lesson lesson : Timetable.sortByStartTime(allUntisSessions.get(guildId).getTimetableFromKlasseId(date, date, classId))) { lessons.putIfAbsent(lesson.getStartTime(), new ArrayList<>()); lessons.get(lesson.getStartTime()).add(lesson); - if (lastStartTime.equals(lesson.getStartTime())) { - multipleLessonAtOnce = true; - } + if (lastStartTime.equals(lesson.getStartTime())) noMultiples = false; lastStartTime = lesson.getStartTime(); } - if (multipleLessonAtOnce) { - for (ArrayList listLessons: lessons.values()) { - for (Timetable.Lesson lesson: listLessons) { - embedBuilder.addField(Utils.advancedFormat(language.getString("timetable-lesson-title"), new HashMap() {{ - put("lesson-number", lesson.getTimeUnitObject().getName()); - put("start-time", lesson.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm"))); - put("end-time", lesson.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm"))); - }}), Utils.advancedFormat(language.getString("timetable-teachers"), new HashMap() {{ - put("teachers", String.join(", ", lesson.getTeachers().getFullNames())); - }}) + "\n" + Utils.advancedFormat(language.getString("timetable-subjects"), new HashMap() {{ - put("subjects", String.join(", ", lesson.getSubjects().getLongNames())); - }}) + "\n" + Utils.advancedFormat(language.getString("timetable-rooms"), new HashMap() {{ - put("rooms", String.join(", ", lesson.getRooms().getLongNames())); - }}), listLessons.size() > 1); + for (int i = 0; i < lessons.size(); i++) { + ArrayList listLessons = (ArrayList) lessons.values().toArray()[i]; + for (Timetable.Lesson lesson: (ArrayList) lessons.values().toArray()[i]) { + String additional = ""; + if (lesson.getCode() == UntisUtils.LessonCode.CANCELLED) { + additional = "~~"; } - } - } else { - /*int halfSize = (int) Math.ceil((lessons.values().size() / 2)); - for (int i = 0; i <= halfSize; i++) { - int j = i + 1; - Timetable.Lesson lesson = ((ArrayList) lessons.values().toArray()[i]).get(0); - Timetable.Lesson[] leftRightLessons; - - if (j + halfSize > lessons.values().size() - 1) { - leftRightLessons = new Timetable.Lesson[]{lesson}; - } else { - leftRightLessons = new Timetable.Lesson[]{lesson, ((ArrayList) lessons.values().toArray()[j + halfSize]).get(0)}; - } - - for (Timetable.Lesson lesson1: leftRightLessons) { - embedBuilder.addField(Utils.advancedFormat(language.getString("timetable-lesson-title"), new HashMap() {{ - put("lesson-number", lesson1.getTimeUnitObject().getName()); - put("start-time", lesson1.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm"))); - put("end-time", lesson1.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm"))); - }}), Utils.advancedFormat(language.getString("timetable-teachers"), new HashMap() {{ - put("teachers", String.join(", ", lesson1.getTeachers().getFullNames())); - }}) + "\n" + Utils.advancedFormat(language.getString("timetable-subjects"), new HashMap() {{ - put("subjects", String.join(", ", lesson1.getSubjects().getLongNames())); - }}) + "\n" + Utils.advancedFormat(language.getString("timetable-rooms"), new HashMap() {{ - put("rooms", String.join(", ", lesson1.getRooms().getLongNames())); - }}), true); - } - - embedBuilder.addBlankField(true); - }*/ - for (ArrayList listLesson: lessons.values()) { - Timetable.Lesson lesson = listLesson.get(0); - embedBuilder.addField(Utils.advancedFormat(language.getString("timetable-lesson-title"), new HashMap() {{ + embedBuilder.addField(additional + Utils.advancedFormat(language.getString("timetable-lesson-title"), new HashMap() {{ put("lesson-number", lesson.getTimeUnitObject().getName()); put("start-time", lesson.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm"))); put("end-time", lesson.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm"))); - }}), Utils.advancedFormat(language.getString("timetable-teachers"), new HashMap() {{ + }}) + additional, additional + Utils.advancedFormat(language.getString("timetable-teachers"), new HashMap() {{ put("teachers", String.join(", ", lesson.getTeachers().getFullNames())); }}) + "\n" + Utils.advancedFormat(language.getString("timetable-subjects"), new HashMap() {{ put("subjects", String.join(", ", lesson.getSubjects().getLongNames())); }}) + "\n" + Utils.advancedFormat(language.getString("timetable-rooms"), new HashMap() {{ put("rooms", String.join(", ", lesson.getRooms().getLongNames())); - }}), true); + }}) + additional, noMultiples || listLessons.size() > 1); + } + if (listLessons.size() == 2 && i + 1 != lessons.size()) { + embedBuilder.addBlankField(true); } } + /*int halfSize = (int) Math.ceil((lessons.values().size() / 2)); + for (int i = 0; i <= halfSize; i++) { + int j = i + 1; + Timetable.Lesson lesson = ((ArrayList) lessons.values().toArray()[i]).get(0); + Timetable.Lesson[] leftRightLessons; + + if (j + halfSize > lessons.values().size() - 1) { + leftRightLessons = new Timetable.Lesson[]{lesson}; + } else { + leftRightLessons = new Timetable.Lesson[]{lesson, ((ArrayList) lessons.values().toArray()[j + halfSize]).get(0)}; + } + + for (Timetable.Lesson lesson1: leftRightLessons) { + embedBuilder.addField(Utils.advancedFormat(language.getString("timetable-lesson-title"), new HashMap() {{ + put("lesson-number", lesson1.getTimeUnitObject().getName()); + put("start-time", lesson1.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm"))); + put("end-time", lesson1.getEndTime().format(DateTimeFormatter.ofPattern("HH:mm"))); + }}), Utils.advancedFormat(language.getString("timetable-teachers"), new HashMap() {{ + put("teachers", String.join(", ", lesson1.getTeachers().getFullNames())); + }}) + "\n" + Utils.advancedFormat(language.getString("timetable-subjects"), new HashMap() {{ + put("subjects", String.join(", ", lesson1.getSubjects().getLongNames())); + }}) + "\n" + Utils.advancedFormat(language.getString("timetable-rooms"), new HashMap() {{ + put("rooms", String.join(", ", lesson1.getRooms().getLongNames())); + }}), true); + } + + embedBuilder.addBlankField(true); + }*/ channel.sendMessage(embedBuilder.build()).queue(); } catch (IOException e) { logger.warn(guildId + " ran into an exception while trying to receive a timetable", e); @@ -258,11 +246,8 @@ public class DiscordCommandListener extends ListenerAdapter { 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"); - } + if (guildName.trim().endsWith("s")) embedBuilder.setTitle(guild.getName() + " untis status"); + else embedBuilder.setTitle(guild.getName() + "'s untis status"); ArrayList mostMissedTeachers = new ArrayList<>(); short missedLessons = 0; @@ -283,17 +268,29 @@ public class DiscordCommandListener extends ListenerAdapter { } String timetableChecking; + String dataSet; 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 ` - type `" + data.getPrefix() + "help` for more details"); embedBuilder.setColor(Color.RED); } + if (data.getUsername() != null && data.getServer() != null && data.getSchool() != null) { + dataSet = "✅ Set"; + if (!data.isCheckActive()) { + embedBuilder.setFooter("The timetable checker is deactivated. Type `" + data.getPrefix() + "start` - use `" + data.getPrefix() + "help start` for more details"); + } + } + else { + dataSet = "❌ Not set"; + embedBuilder.setFooter("To set your data, type `" + data.getPrefix() + "set-data ` - use `" + data.getPrefix() + "help data` for more details"); + } + embedBuilder.addField("Timetable checking", timetableChecking, true); + embedBuilder.addField("Data status", dataSet, true); //embedBuilder.addField("Checking interval", data.getSleepTime() / 60000 + " minutes", true); - embedBuilder.addField("Total timetable requests", String.valueOf(stats.getTotalRequests()), 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); @@ -441,6 +438,7 @@ public class DiscordCommandListener extends ListenerAdapter { if (data.isCheckActive()) { channel.sendMessage("Timetable listening already started").queue(); } else { + guildDataConnector.update(guildId, null, null, null, null, null, null, null, null, null, true, null); runTimetableChecker(guild); logger.info(guildName + " started timetable listening"); channel.sendMessage("✅ Timetable listening has been started").queue(); @@ -452,6 +450,7 @@ public class DiscordCommandListener extends ListenerAdapter { case "stop": // `stop` command if (args.length == 0) { if (data.isCheckActive()) { + guildDataConnector.update(guildId, null, null, null, null, null, null, null, null, null, false, null); Timer timer = allTimetableChecker.get(guildId); allTimetableChecker.remove(guildId); timer.cancel(); @@ -465,7 +464,9 @@ public class DiscordCommandListener extends ListenerAdapter { channel.sendMessage("Wrong number of arguments were given (expected 0, got " + args.length + "), type `" + data.getPrefix() + "help stop` for help").queue(); } break; - case "help": // is handled in DiscordCommandListener.onMessageReceived() + case "help": + case "info": + case "infos": // is handled in DiscordCommandListener.onMessageReceived() return; default: channel.sendMessage("Unknown command").queue(); @@ -542,6 +543,8 @@ public class DiscordCommandListener extends ListenerAdapter { } catch (ArrayIndexOutOfBoundsException e) { i++; daysToCheck++; + } catch (NullPointerException e) { + i++; } for (; i <= daysToCheck; i++) { @@ -756,111 +759,150 @@ public class DiscordCommandListener extends ListenerAdapter { String message = event.getMessage().getContentDisplay().trim().toLowerCase(); MessageChannel channel = event.getChannel(); - String prefix; - if (message.contains("help")) { // `help` command - if (event.isFromGuild()) { - prefix = guildDataConnector.get(event.getGuild().getIdLong()).getPrefix(); - if (!event.getMessage().getContentDisplay().startsWith(prefix + "help")) { - System.out.println("sss"); - return; + String[] commands = new String[]{"help", "info"}; + + String prefix = null; + String command = ""; + String[] args = null; + + for (String cmd: commands) { + if (message.contains(cmd)) { + if (event.isFromGuild()) { + prefix = guildDataConnector.get(event.getGuild().getIdLong()).getPrefix(); + if (!event.getMessage().getContentDisplay().startsWith(prefix + cmd)) { + return; + } + } else if (message.equals(cmd) || message.startsWith(cmd + " ")) { + prefix = ""; + } else { + continue; } - } else if (message.equals("help") || message.startsWith("help ")) { - prefix = ""; - } else { - return; + command = cmd; + String[] splitMessage = message.substring(prefix.length()).split(" "); + args = Arrays.copyOfRange(splitMessage, 1, splitMessage.length); + break; } - } else { - return; } - String[] splitMessage = message.substring(prefix.length()).split(" "); - String[] args = Arrays.copyOfRange(splitMessage, 1, splitMessage.length); - String help = "Use `" + prefix + "help ` to get help / information about a command.\n\n" + - "All available commands are:\n" + - "`channel` `clear` `data` `help` `language` `prefix` `stats` `start` `stop` `timetable`"; - 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 ` 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"; - description = "Displays help to a given command"; - example = "`help data`"; - break; - case "language": - title = "`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 ` 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; - case "start": - title = "`start` command"; - description = "Starts the stopped timetable listener. Only works if data was set with the `data` command"; - example = "`start`"; - break; - case "stop": - title = "`stop` command"; - description = "Stops timetable listening. Only works if data was set with the `data` command"; - example = "`stop`"; - break; - case "timetable": - title = "`timetable [date] [class name]` command"; - description = "Displays the timetable for a specific date. As `date` you can use 3 formats." + - "1: Only the day (`12`); 2. Day and month (`13.04`); 3. Day, month and year (`31.12.2020`)." + - "Only works if data was set with the `data` command. If no date is given, the timetable for the current date is displayed." + - "As `class name` you can use any class from your school. If class is not given, the class which was assigned in the `data` command is used"; - example = "`timetable 11.11`"; - break; - default: - channel.sendMessage("Unknown command was given. " + help).queue(); - return; - } - EmbedBuilder embedBuilder = new EmbedBuilder(); - 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(); + switch (command) { + case "help": // `help [command] command` + if (args.length < 2) { + String help = "Use `" + prefix + "help ` to get help / information about a command.\n\n" + + "All available commands are:\n" + + "`channel` • `clear` • `data` • `info` • `help` • `language` • `prefix` • `stats` • `start` • `stop` • `timetable`"; + 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 ` 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 "info": + title = "`info` command"; + description = "Displays information about the bot"; + example = "`info`"; + break; + case "help": + title = "`help ` command"; + description = "Displays help to a given command"; + example = "`help data`"; + break; + case "language": + title = "`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 ` 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; + case "start": + title = "`start` command"; + description = "Starts the stopped timetable listener. Only works if data was set with the `data` command"; + example = "`start`"; + break; + case "stop": + title = "`stop` command"; + description = "Stops timetable listening. Only works if data was set with the `data` command"; + example = "`stop`"; + break; + case "timetable": + title = "`timetable [date] [class name]` command"; + description = "Displays the timetable for a specific date. As `date` you can use 3 formats." + + "1: Only the day (`12`); 2. Day and month (`13.04`); 3. Day, month and year (`31.12.2020`)." + + "Only works if data was set with the `data` command. If no date is given, the timetable for the current date is displayed." + + "As `class name` you can use any class from your school. If class is not given, the class which was assigned in the `data` command is used"; + example = "`timetable 11.11`"; + break; + default: + channel.sendMessage("Unknown command was given. " + help).queue(); + return; + } + EmbedBuilder embedBuilder = new EmbedBuilder(); + 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(); + } + } else { + channel.sendMessage("Wrong number of arguments were given (expected 0 or 1, got " + args.length + "), type `" + prefix + "help help` for help").queue(); + } + break; + case "info": + if (args.length == 0) { + EmbedBuilder infoBuilder = new EmbedBuilder(); + infoBuilder.setTitle("Untis information"); + infoBuilder.setThumbnail("https://cdn.discordapp.com/avatars/768841979433451520/2742868bb029952ee00514a01f84b65b.webp?size=512"); + infoBuilder.setDescription("<@768841979433451520> is a java programmed discord bot, which uses the [WebUntis](https://webuntis.com/) " + + "timetable software to receive the timetable for a specific date, " + + "automatically send messages when the timetable from a given account or class changes, " + + "displays total cancelled lessons etc., and more!"); + infoBuilder.setColor(new Color(255, 165, 0)); + infoBuilder.addField("\uD83D\uDCDDAuthor", "[<@650417934073593886>](https://discordapp.com/users/650417934073593886)", true); + infoBuilder.addField("✨Version", "[v1.1](https://github.com/ByteDream/untisbot-discord/releases/tag/v1.1)", true); + infoBuilder.addField("❓Help", "`" + prefix + "help`", true); + infoBuilder.addField("❤️️Source / GitHub", "[Source code!](https://github.com/ByteDream/untisbot-discord)", true); + //infoBuilder.addField("Total guilds (server)", String.valueOf(guildDataConnector.getAll().size()), true); + infoBuilder.addField("\uD83D\uDCC8Vote", "[Vote!](https://top.gg/bot/768841979433451520/vote)", true); + infoBuilder.addField("\uD83D\uDD10️Data encryption algorithm", "[AES-256](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)", true); + channel.sendMessage(infoBuilder.build()).queue(); + } else { + channel.sendMessage("Wrong number of arguments were given (expected 0, got " + args.length + "), type `" + prefix + "help info` for help").queue(); + } + break; } }); t.setName("Message handler"); From 74274fa2d78cd9ddf35ece27df06c3a6c85e6a4e Mon Sep 17 00:00:00 2001 From: ByteDream Date: Sat, 5 Dec 2020 00:19:51 +0100 Subject: [PATCH 06/12] Created anti sql timeouter --- .../discord/DiscordCommandListener.java | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java index c48ee1f..ca06130 100755 --- a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java +++ b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java @@ -39,6 +39,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.concurrent.TimeUnit; /** * Adapter to handle all events @@ -73,8 +74,16 @@ public class DiscordCommandListener extends ListenerAdapter { statsDataConnector = dataConnector.statsConnector(); this.languages = languages; - EmbedBuilder embedBuilder = new EmbedBuilder(); - embedBuilder.setColor(Color.GREEN); + new Timer().scheduleAtFixedRate(new TimerTask() { // just execute this so that the connect won't have a timeout + @Override + public void run() { + Thread.currentThread().setName("Anti sql timeout"); + try { + Main.getConnection().createStatement().execute("SELECT * FROM Guilds WHERE GUILDID = 0"); + } catch (SQLException ignore) { + } + } + }, 0, TimeUnit.HOURS.toMillis(1)); } /** @@ -161,9 +170,12 @@ public class DiscordCommandListener extends ListenerAdapter { language = languages.getJSONObject(data.getLanguage()); } String className = "-"; + try { - className = session.getKlassen().findById(data.getKlasseId()).getName(); + session.reconnect(); + className = session.getKlassen().findById(classId).getName(); } catch (IOException ignore) {} + String finalClassName = className; // yea java... LocalDate finalDate = date; // yea java part two... embedBuilder.setTitle(Utils.advancedFormat(language.getString("timetable-title"), new HashMap() {{ @@ -279,7 +291,7 @@ public class DiscordCommandListener extends ListenerAdapter { if (data.getUsername() != null && data.getServer() != null && data.getSchool() != null) { dataSet = "✅ Set"; if (!data.isCheckActive()) { - embedBuilder.setFooter("The timetable checker is deactivated. Type `" + data.getPrefix() + "start` - use `" + data.getPrefix() + "help start` for more details"); + embedBuilder.setFooter("The timetable checker is deactivated. Type `" + data.getPrefix() + "start` to re-enable it - use `" + data.getPrefix() + "help start` for more details"); } } else { @@ -430,7 +442,7 @@ public class DiscordCommandListener extends ListenerAdapter { 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(); + channel.sendMessage("Wrong number of arguments were given (expected 1, got " + args.length + "), type `" + data.getPrefix() + "help prefix` for help").queue(); } break; case "start": // `start` command @@ -440,7 +452,6 @@ public class DiscordCommandListener extends ListenerAdapter { } else { guildDataConnector.update(guildId, null, null, null, null, null, null, null, null, null, true, null); runTimetableChecker(guild); - logger.info(guildName + " started timetable listening"); channel.sendMessage("✅ Timetable listening has been started").queue(); } } else { @@ -550,6 +561,7 @@ public class DiscordCommandListener extends ListenerAdapter { for (; i <= daysToCheck; i++) { LocalDate localDate = now.plusDays(i); try { + LocalDate lastChecked = guildDataConnector.get(guildId).getLastChecked(); CheckCallback checkCallback = timetableChecker.check(localDate); ArrayList cancelledLessons = checkCallback.getCancelled(); @@ -636,14 +648,11 @@ public class DiscordCommandListener extends ListenerAdapter { 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()); @@ -671,6 +680,8 @@ public class DiscordCommandListener extends ListenerAdapter { if (error) { error = false; } + } else if (lastChecked == null || lastChecked.isBefore(now.plusDays(i))) { + guildDataConnector.update(guildId, null, null, null, null, null, null, null, null, null, null, now.plusDays(i)); } } catch (Exception e) { logger.warn(guildName + " ran into an exception while trying to check the timetable for the " + localDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")), e); @@ -704,12 +715,6 @@ public class DiscordCommandListener extends ListenerAdapter { if (latestImportTime < sessionLatestImportTime) { latestImportTime = sessionLatestImportTime; main(); - } else { - try { - Main.getConnection().createStatement().execute("SELECT * FROM Guilds WHERE GUILDID = 0"); - // just execute this so that the connect won't have a timeout - } catch (SQLException ignore) { - } } } catch (IOException e) { logger.info("Running main through IOException", e); From 992accf51e521b5ee8d9a721f49c155d3071762a Mon Sep 17 00:00:00 2001 From: ByteDream Date: Sat, 5 Dec 2020 00:38:39 +0100 Subject: [PATCH 07/12] Created unexpected exception catcher in timetable listener --- .../discord/DiscordCommandListener.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java index ca06130..f74276b 100755 --- a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java +++ b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java @@ -719,6 +719,13 @@ public class DiscordCommandListener extends ListenerAdapter { } catch (IOException e) { logger.info("Running main through IOException", e); main(); + } catch (Exception e) { + try { + logger.info("Try to run main through unexpected exception", e); + main(); + } catch (Exception ex) { + logger.warn("Couldn't run main through unexpected exception", ex); + } } } }, 0, data.getSleepTime()); @@ -926,14 +933,15 @@ public class DiscordCommandListener extends ListenerAdapter { statsDataConnector.add(guildId); } + Data.Guild data = guildDataConnector.get(guildId); + try { + allUntisSessions.put(guildId, Session.login(data.getUsername(), data.getPassword(), data.getServer(), data.getSchool())); + } catch (IOException e) { + logger.error("Error for guild " + guild.getName() + " (" + guildId + ") while setting up untis session", e); + continue; + } if (guildDataConnector.get(guildId).isCheckActive()) { - Data.Guild data = guildDataConnector.get(guildId); - try { - allUntisSessions.put(guildId, Session.login(data.getUsername(), data.getPassword(), data.getServer(), data.getSchool())); - runTimetableChecker(guild); - } catch (IOException e) { - logger.error("Error for guild " + guild.getName() + " (" + guildId + ") while setting up untis session", e); - } + runTimetableChecker(guild); } allGuilds.add(guildId); From cc7e1e4999fc192ff752e3c23e0b292ae26ce3ff Mon Sep 17 00:00:00 2001 From: ByteDream Date: Sat, 5 Dec 2020 00:46:54 +0100 Subject: [PATCH 08/12] Code cleanup --- .../bytedream/untisbot/discord/Discord.java | 2 +- .../discord/DiscordCommandListener.java | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/org/bytedream/untisbot/discord/Discord.java b/src/org/bytedream/untisbot/discord/Discord.java index a087c4e..bbd412a 100755 --- a/src/org/bytedream/untisbot/discord/Discord.java +++ b/src/org/bytedream/untisbot/discord/Discord.java @@ -12,7 +12,6 @@ import org.json.JSONTokener; import javax.security.auth.login.LoginException; import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; @@ -50,6 +49,7 @@ public class Discord { /** * Show rich presence if a bot update was released within then last 24 hours + * * @since 1.1 */ private void updateRichPresence() { diff --git a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java index f74276b..0ac4213 100755 --- a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java +++ b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java @@ -89,12 +89,11 @@ public class DiscordCommandListener extends ListenerAdapter { /** * Runs a command * - * @param guild guild from which the command came - * @param channel channel from which the command came + * @param guild guild from which the command came + * @param channel channel from which the command came * @param permission if true, commands which needs (admin) permission to run, can be executed - * @param command command to execute - * @param args extra arguments for the command - * + * @param command command to execute + * @param args extra arguments for the command * @since 1.1 */ private void runCommand(Guild guild, TextChannel channel, boolean permission, String command, String[] args) { @@ -174,7 +173,8 @@ public class DiscordCommandListener extends ListenerAdapter { try { session.reconnect(); className = session.getKlassen().findById(classId).getName(); - } catch (IOException ignore) {} + } catch (IOException ignore) { + } String finalClassName = className; // yea java... LocalDate finalDate = date; // yea java part two... @@ -195,7 +195,7 @@ public class DiscordCommandListener extends ListenerAdapter { } for (int i = 0; i < lessons.size(); i++) { ArrayList listLessons = (ArrayList) lessons.values().toArray()[i]; - for (Timetable.Lesson lesson: (ArrayList) lessons.values().toArray()[i]) { + for (Timetable.Lesson lesson : (ArrayList) lessons.values().toArray()[i]) { String additional = ""; if (lesson.getCode() == UntisUtils.LessonCode.CANCELLED) { additional = "~~"; @@ -293,8 +293,7 @@ public class DiscordCommandListener extends ListenerAdapter { if (!data.isCheckActive()) { embedBuilder.setFooter("The timetable checker is deactivated. Type `" + data.getPrefix() + "start` to re-enable it - use `" + data.getPrefix() + "help start` for more details"); } - } - else { + } else { dataSet = "❌ Not set"; embedBuilder.setFooter("To set your data, type `" + data.getPrefix() + "set-data ` - use `" + data.getPrefix() + "help data` for more details"); } @@ -681,7 +680,7 @@ public class DiscordCommandListener extends ListenerAdapter { error = false; } } else if (lastChecked == null || lastChecked.isBefore(now.plusDays(i))) { - guildDataConnector.update(guildId, null, null, null, null, null, null, null, null, null, null, now.plusDays(i)); + guildDataConnector.update(guildId, null, null, null, null, null, null, null, null, null, null, now.plusDays(i)); } } catch (Exception e) { logger.warn(guildName + " ran into an exception while trying to check the timetable for the " + localDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")), e); @@ -777,7 +776,7 @@ public class DiscordCommandListener extends ListenerAdapter { String command = ""; String[] args = null; - for (String cmd: commands) { + for (String cmd : commands) { if (message.contains(cmd)) { if (event.isFromGuild()) { prefix = guildDataConnector.get(event.getGuild().getIdLong()).getPrefix(); From bc3f6514d70bec66c8d1714254bb4163adda4d77 Mon Sep 17 00:00:00 2001 From: ByteDream Date: Sat, 5 Dec 2020 00:51:20 +0100 Subject: [PATCH 09/12] Bugfix for non sql user --- .../discord/DiscordCommandListener.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java index 0ac4213..bb503a0 100755 --- a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java +++ b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java @@ -74,16 +74,18 @@ public class DiscordCommandListener extends ListenerAdapter { statsDataConnector = dataConnector.statsConnector(); this.languages = languages; - new Timer().scheduleAtFixedRate(new TimerTask() { // just execute this so that the connect won't have a timeout - @Override - public void run() { - Thread.currentThread().setName("Anti sql timeout"); - try { - Main.getConnection().createStatement().execute("SELECT * FROM Guilds WHERE GUILDID = 0"); - } catch (SQLException ignore) { + if (storeType == StoreType.MARIADB) { + new Timer().scheduleAtFixedRate(new TimerTask() { // just execute this so that the connect won't have a timeout + @Override + public void run() { + Thread.currentThread().setName("Anti sql timeout"); + try { + Main.getConnection().createStatement().execute("SELECT * FROM Guilds WHERE GUILDID = 0"); + } catch (SQLException ignore) { + } } - } - }, 0, TimeUnit.HOURS.toMillis(1)); + }, 0, TimeUnit.HOURS.toMillis(1)); + } } /** From f2b0394db3d35160b45a955d383bcebc8592a74e Mon Sep 17 00:00:00 2001 From: ByteDream Date: Sat, 5 Dec 2020 01:01:09 +0100 Subject: [PATCH 10/12] Caught NullPointerException --- src/org/bytedream/untisbot/data/Data.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/bytedream/untisbot/data/Data.java b/src/org/bytedream/untisbot/data/Data.java index 28e574c..808bb33 100755 --- a/src/org/bytedream/untisbot/data/Data.java +++ b/src/org/bytedream/untisbot/data/Data.java @@ -50,7 +50,7 @@ public class Data { public String getUsername() { try { return crypt.decrypt((String) (data[2])); - } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidKeySpecException ignore) { + } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidKeySpecException | NullPointerException ignore) { return null; } } @@ -58,7 +58,7 @@ public class Data { public String getPassword() { try { return crypt.decrypt((String) (data[3])); - } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidKeySpecException ignore) { + } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidKeySpecException | NullPointerException ignore) { return null; } } From c2f0e41e718ae03b8438e212395219afa5296860 Mon Sep 17 00:00:00 2001 From: ByteDream Date: Sat, 5 Dec 2020 01:14:43 +0100 Subject: [PATCH 11/12] Performance optimizations --- .../untisbot/discord/DiscordCommandListener.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java index bb503a0..e7cddae 100755 --- a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java +++ b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java @@ -290,7 +290,7 @@ public class DiscordCommandListener extends ListenerAdapter { timetableChecking = "\uD83D\uDD34 Inactive"; embedBuilder.setColor(Color.RED); } - if (data.getUsername() != null && data.getServer() != null && data.getSchool() != null) { + if (data.getServer() != null && data.getSchool() != null) { dataSet = "✅ Set"; if (!data.isCheckActive()) { embedBuilder.setFooter("The timetable checker is deactivated. Type `" + data.getPrefix() + "start` to re-enable it - use `" + data.getPrefix() + "help start` for more details"); @@ -384,7 +384,7 @@ public class DiscordCommandListener extends ListenerAdapter { guildDataConnector.update(guildId, null, args[0], args[1], server, schoolName, klasseId, null, null, null, true, null); } - if (data.isCheckActive()) { + if (data.isCheckActive() && allTimetableChecker.containsKey(guildId)) { Timer timer = allTimetableChecker.get(guildId); allTimetableChecker.remove(guildId); timer.cancel(); @@ -935,11 +935,13 @@ public class DiscordCommandListener extends ListenerAdapter { } Data.Guild data = guildDataConnector.get(guildId); - try { - allUntisSessions.put(guildId, Session.login(data.getUsername(), data.getPassword(), data.getServer(), data.getSchool())); - } catch (IOException e) { - logger.error("Error for guild " + guild.getName() + " (" + guildId + ") while setting up untis session", e); - continue; + if (data.getUsername() != null && data.getServer() != null && data.getSchool() != null) { + try { + allUntisSessions.put(guildId, Session.login(data.getUsername(), data.getPassword(), data.getServer(), data.getSchool())); + } catch (IOException e) { + logger.error("Error for guild " + guild.getName() + " (" + guildId + ") while setting up untis session", e); + continue; + } } if (guildDataConnector.get(guildId).isCheckActive()) { runTimetableChecker(guild); From ebc9ede60e9a153c2002676ecce111c02dda00c5 Mon Sep 17 00:00:00 2001 From: ByteDream Date: Sat, 5 Dec 2020 01:40:15 +0100 Subject: [PATCH 12/12] Database bugfix --- src/org/bytedream/untisbot/data/DataConnector.java | 10 +++++----- .../untisbot/discord/DiscordCommandListener.java | 8 +++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/org/bytedream/untisbot/data/DataConnector.java b/src/org/bytedream/untisbot/data/DataConnector.java index 60449b8..8ff8d2b 100755 --- a/src/org/bytedream/untisbot/data/DataConnector.java +++ b/src/org/bytedream/untisbot/data/DataConnector.java @@ -192,7 +192,7 @@ public class DataConnector { args.put("LANGUAGE", language); if (username != null) { if (username.isEmpty()) { - args.put("USERNAME", "NULL"); + args.put("USERNAME", ""); } else { try { args.put("USERNAME", crypt.encrypt(username)); @@ -205,12 +205,12 @@ public class DataConnector { } if (password != null) { if (password.isEmpty()) { - args.put("PASSWORD", "NULL"); + args.put("PASSWORD", ""); } else { try { args.put("PASSWORD", crypt.encrypt(password)); } catch (GeneralSecurityException ignore) { - args.put("PASSWORD", null); + args.put("PASSWORD", ""); } } } else { @@ -218,7 +218,7 @@ public class DataConnector { } if (server != null) { if (server.isEmpty()) { - args.put("SERVER", "NULL"); + args.put("SERVER", ""); } else { args.put("SERVER", server); } @@ -227,7 +227,7 @@ public class DataConnector { } if (school != null) { if (school.isEmpty()) { - args.put("SCHOOL", "NULL"); + args.put("SCHOOL", ""); } else { args.put("SCHOOL", school); } diff --git a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java index e7cddae..0afb8f4 100755 --- a/src/org/bytedream/untisbot/discord/DiscordCommandListener.java +++ b/src/org/bytedream/untisbot/discord/DiscordCommandListener.java @@ -290,7 +290,7 @@ public class DiscordCommandListener extends ListenerAdapter { timetableChecking = "\uD83D\uDD34 Inactive"; embedBuilder.setColor(Color.RED); } - if (data.getServer() != null && data.getSchool() != null) { + if (data.getServer() != null && !data.getServer().equals("") && data.getSchool() != null && !data.getSchool().equals("")) { dataSet = "✅ Set"; if (!data.isCheckActive()) { embedBuilder.setFooter("The timetable checker is deactivated. Type `" + data.getPrefix() + "start` to re-enable it - use `" + data.getPrefix() + "help start` for more details"); @@ -333,7 +333,9 @@ public class DiscordCommandListener extends ListenerAdapter { 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(); + allUntisSessions.remove(guildId); + allTimetableChecker.remove(guildId); + channel.sendMessage("Cleared untis data and stopped timetable listening if active").queue(); } else { channel.sendMessage("Wrong number of arguments were given (expected 0, got " + args.length + "), type `" + data.getPrefix() + "help clear` for help").queue(); } @@ -392,7 +394,7 @@ public class DiscordCommandListener extends ListenerAdapter { runTimetableChecker(guild); channel.sendMessage("✅ Updated data and restarted timetable listening for class " + className).queue(); } else if (data.getLastChecked() != null) { - channel.sendMessage("✅ Updated data. Timetable listening were manually stopped a while ago. To re-enable it, type `" + data.getPrefix() + "start`").queue(); + channel.sendMessage("✅ Updated data. Timetable listening were stopped a while ago. To re-enable it, type `" + data.getPrefix() + "start`").queue(); } else { runTimetableChecker(guild); channel.sendMessage("✅ Timetable listening has been started for class " + className).queue();