Moved commands to extra function; added timetable, start, stop commands; general changes

This commit is contained in:
2020-12-04 18:19:59 +01:00
parent cecaf526d4
commit 88ad913bca
16 changed files with 516 additions and 287 deletions

0
src/org/bytedream/untisbot/Crypt.java Normal file → Executable file
View File

0
src/org/bytedream/untisbot/Main.java Normal file → Executable file
View File

0
src/org/bytedream/untisbot/Utils.java Normal file → Executable file
View File

2
src/org/bytedream/untisbot/data/Data.java Normal file → Executable file
View File

@ -72,7 +72,7 @@ public class Data {
}
public Short getKlasseId() {
return (short) data[6];
return (Short) data[6];
}
public Long getChannelId() {

0
src/org/bytedream/untisbot/data/DataConnector.java Normal file → Executable file
View File

0
src/org/bytedream/untisbot/data/StoreType.java Normal file → Executable file
View File

0
src/org/bytedream/untisbot/discord/Discord.java Normal file → Executable file
View File

View File

@ -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<Long, Session> allUntisSessions = new HashMap<>();
private final HashMap<Long, Timer> 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<String, Object>() {{
put("class", finalClassName);
put("date", finalDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")));
}}));
LocalTime lastStartTime = LocalTime.MIN;
boolean multipleLessonAtOnce = false;
TreeMap<LocalTime, ArrayList<Timetable.Lesson>> 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<Timetable.Lesson> listLessons: lessons.values()) {
for (Timetable.Lesson lesson: listLessons) {
embedBuilder.addField(Utils.advancedFormat(language.getString("timetable-lesson-title"), new HashMap<String, Object>() {{
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<String, Object>() {{
put("teachers", String.join(", ", lesson.getTeachers().getFullNames()));
}}) + "\n" + Utils.advancedFormat(language.getString("timetable-subjects"), new HashMap<String, Object>() {{
put("subjects", String.join(", ", lesson.getSubjects().getLongNames()));
}}) + "\n" + Utils.advancedFormat(language.getString("timetable-rooms"), new HashMap<String, Object>() {{
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<Timetable.Lesson>) 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<Timetable.Lesson>) lessons.values().toArray()[j + halfSize]).get(0)};
}
for (Timetable.Lesson lesson1: leftRightLessons) {
embedBuilder.addField(Utils.advancedFormat(language.getString("timetable-lesson-title"), new HashMap<String, Object>() {{
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<String, Object>() {{
put("teachers", String.join(", ", lesson1.getTeachers().getFullNames()));
}}) + "\n" + Utils.advancedFormat(language.getString("timetable-subjects"), new HashMap<String, Object>() {{
put("subjects", String.join(", ", lesson1.getSubjects().getLongNames()));
}}) + "\n" + Utils.advancedFormat(language.getString("timetable-rooms"), new HashMap<String, Object>() {{
put("rooms", String.join(", ", lesson1.getRooms().getLongNames()));
}}), true);
}
embedBuilder.addBlankField(true);
}*/
for (ArrayList<Timetable.Lesson> listLesson: lessons.values()) {
Timetable.Lesson lesson = listLesson.get(0);
embedBuilder.addField(Utils.advancedFormat(language.getString("timetable-lesson-title"), new HashMap<String, Object>() {{
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<String, Object>() {{
put("teachers", String.join(", ", lesson.getTeachers().getFullNames()));
}}) + "\n" + Utils.advancedFormat(language.getString("timetable-subjects"), new HashMap<String, Object>() {{
put("subjects", String.join(", ", lesson.getSubjects().getLongNames()));
}}) + "\n" + Utils.advancedFormat(language.getString("timetable-rooms"), new HashMap<String, Object>() {{
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<String> mostMissedTeachers = new ArrayList<>();
short missedLessons = 0;
for (Map.Entry<String, Short> entry : stats.getAbsentTeachers().entrySet()) {
if (entry.getValue() > missedLessons) {
mostMissedTeachers.clear();
mostMissedTeachers.add(entry.getKey());
missedLessons = entry.getValue();
} else if (entry.getValue() == missedLessons) {
mostMissedTeachers.add(entry.getKey());
}
}
String mostMissedTeachersText;
if (missedLessons == 0) {
mostMissedTeachersText = "n/a";
} else {
mostMissedTeachersText = String.join(", ", mostMissedTeachers) + " - " + missedLessons + " missed lessons";
}
String timetableChecking;
if (data.isCheckActive()) {
timetableChecking = "\uD83D\uDFE2 Active";
embedBuilder.setColor(Color.GREEN);
} else {
timetableChecking = "\uD83D\uDD34 Inactive";
embedBuilder.setFooter("To start timetable checking, type `" + data.getPrefix() + "set-data <username> <password> <loginpage url>` - type `" + data.getPrefix() + "help` for more details");
embedBuilder.setColor(Color.RED);
}
embedBuilder.addField("Timetable checking", timetableChecking, true);
//embedBuilder.addField("Checking interval", data.getSleepTime() / 60000 + " minutes", true);
embedBuilder.addField("Total timetable requests", String.valueOf(stats.getTotalRequests()), true);
embedBuilder.addField("Total lessons checked", String.valueOf(stats.getTotalLessons()), true);
embedBuilder.addField("Total weeks checked", String.valueOf((int) (Math.floor((float) stats.getTotalDays() / 7))), true);
embedBuilder.addField("Total cancelled lessons", String.valueOf(stats.getTotalCancelledLessons()), true);
embedBuilder.addField("Total moved lessons", String.valueOf(stats.getTotalMovedLessons()), true);
embedBuilder.addField("Average cancelled lessons per week", String.valueOf(stats.getAverageCancelledLessonsPerWeek()), true);
embedBuilder.addField("Average moved lessons per week", String.valueOf(stats.getAverageMovedLessonsPerWeek()), true);
embedBuilder.addField("Most missed teacher", mostMissedTeachersText, false);
channel.sendMessage(embedBuilder.build()).queue();
} else {
channel.sendMessage("Wrong number of arguments were given (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 <username> <password> <server> <school name>` 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 <language>` command
if (args.length == 1) {
String language = args[0];
if (!languages.has(language)) {
channel.sendMessage("The language `" + language + "` is not supported. Type `" + data.getPrefix() + "help` to see all available languages").queue();
} else {
guildDataConnector.update(guildId, language, null, null, null, null, null, null, null, null, null, null);
logger.info(guildName + " set their language to " + language);
channel.sendMessage("Updated language to `" + language + "`").queue();
}
} else {
channel.sendMessage("Wrong number of arguments were given (expected 1, got " + args.length + "), type `" + data.getPrefix() + "help language` for help").queue();
}
break;
case "prefix": // `prefix <new prefix>` command
if (args.length == 1) {
String prefix = args[0];
if (prefix.length() == 0 || prefix.length() > 6) {
channel.sendMessage("The prefix must be between 1 and 6 characters long").queue();
} else {
String note = "";
if (prefix.contains("'") || prefix.contains("\"")) {
channel.sendMessage("Cannot use `'` or `\"` in prefix").queue();
return;
}
if (prefix.length() == 1) {
if ("!?$¥§%&@€#|/\\=.:-_+,;*+~<>^°".indexOf(prefix.charAt(0)) == -1) {
note += "\n_Note_: Because the prefix is not in `!?$¥§%&@€#|/\\=.:-_+,;*+~<>^°` you have to call commands with a blank space between it and the prefix";
}
} else {
prefix += " ";
note += "\n_Note_: Because the prefix is longer than 1 character you have to call commands with a blank space between it and the prefix";
}
guildDataConnector.update(guildId, null, null, null, null, null, null, null, prefix, null, null, null);
logger.info(guildName + " set their prefix to " + prefix);
channel.sendMessage("Updated prefix to `" + prefix + "`" + note).queue();
}
} else {
channel.sendMessage("Wrong number of arguments were given (expected 3, got " + args.length + "), type `" + data.getPrefix() + "help prefix` for help").queue();
}
break;
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<String, Object>() {{
embedBuilder.setTitle(Utils.advancedFormat(language.getString("change-title"), new HashMap<String, Object>() {{
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<String, Object> formatMap = new HashMap<String, Object>() {{
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<String, Object> formatMap = new HashMap<String, Object>() {{
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<String, Object> formatMap = new HashMap<String, Object>() {{
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<String, Object> formatMap = new HashMap<String, Object>() {{
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<Long> allGuilds = new ArrayList<>();
for (Guild guild : event.getJDA().getGuilds()) {
long guildId = guild.getIdLong();
if (!guildDataConnector.has(guildId)) {
guildDataConnector.add(guildId);
}
if (!statsDataConnector.has(guildId)) {
statsDataConnector.add(guildId);
}
if (guildDataConnector.get(guildId).isCheckActive()) {
runTimetableChecker(guild);
}
allGuilds.add(guildId);
}
for (Data.Guild data : guildDataConnector.getAll()) {
if (!allGuilds.contains(data.getGuildId())) {
guildDataConnector.remove(data.getGuildId());
statsDataConnector.remove(data.getGuildId());
}
}
logger.info("Bot is ready | Total guilds: " + guildDataConnector.getAll().size());
}
@Override
public void onGuildMessageReceived(GuildMessageReceivedEvent event) {
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<String> mostMissedTeachers = new ArrayList<>();
short missedLessons = 0;
for (Map.Entry<String, Short> entry : stats.getAbsentTeachers().entrySet()) {
if (entry.getValue() > missedLessons) {
mostMissedTeachers.clear();
mostMissedTeachers.add(entry.getKey());
missedLessons = entry.getValue();
} else if (entry.getValue() == missedLessons) {
mostMissedTeachers.add(entry.getKey());
}
}
String mostMissedTeachersText;
if (missedLessons == 0) {
mostMissedTeachersText = "n/a";
} else {
mostMissedTeachersText = String.join(", ", mostMissedTeachers) + " - " + missedLessons + " missed lessons";
}
String timetableChecking;
if (data.isCheckActive()) {
timetableChecking = "\uD83D\uDFE2 Active";
embedBuilder.setColor(Color.GREEN);
} else {
timetableChecking = "\uD83D\uDD34 Inactive";
embedBuilder.setFooter("To start timetable checking, type `" + data.getPrefix() + "set-data <username> <password> <loginpage url>` - type `" + data.getPrefix() + "help` for more details");
embedBuilder.setColor(Color.RED);
}
embedBuilder.addField("Timetable checking", timetableChecking, true);
//embedBuilder.addField("Checking interval", data.getSleepTime() / 60000 + " minutes", true);
embedBuilder.addField("Total timetable requests", String.valueOf(stats.getTotalRequests()), true);
embedBuilder.addField("Total lessons checked", String.valueOf(stats.getTotalLessons()), true);
embedBuilder.addField("Total weeks checked", String.valueOf((int) (Math.floor((float) stats.getTotalDays() / 7))), true);
embedBuilder.addField("Total cancelled lessons", String.valueOf(stats.getTotalCancelledLessons()), true);
embedBuilder.addField("Total moved lessons", String.valueOf(stats.getTotalMovedLessons()), true);
embedBuilder.addField("Average cancelled lessons per week", String.valueOf(stats.getAverageCancelledLessonsPerWeek()), true);
embedBuilder.addField("Average moved lessons per week", String.valueOf(stats.getAverageMovedLessonsPerWeek()), true);
embedBuilder.addField("Most missed teacher", mostMissedTeachersText, false);
channel.sendMessage(embedBuilder.build()).queue();
} else {
channel.sendMessage("Wrong number of arguments were given, type `" + data.getPrefix() + "help` for help").queue();
}
} else if (event.getMember().getPermissions().contains(Permission.ADMINISTRATOR)) {
switch (command) {
case "channel": // `channel` command
if (args.length == 0) {
guildDataConnector.update(guild.getIdLong(), null, null, null, null, null, null, channel.getIdLong(), null, null, null, null);
logger.info(guildName + " set a new channel to send the timetable changes to");
channel.sendMessage("This channel is now set as the channel where I send the timetable changes in").queue();
} else {
channel.sendMessage("Wrong number of arguments were given (expected 0, got " + args.length + "), type `" + data.getPrefix() + "help channel` for help").queue();
}
break;
case "clear": // `clear` command
if (args.length == 0) {
guildDataConnector.update(guild.getIdLong(), null, "", "", "", "", (short) 0, null, null, null, false, null);
logger.info(guildName + " cleared their data");
channel.sendMessage("Cleared untis data and stopped timetable listening").queue();
} else {
channel.sendMessage("Wrong number of arguments were given (expected 0, got " + args.length + "), type `" + data.getPrefix() + "help clear` for help").queue();
}
break;
case "data": // `data <username> <password> <server> <school name>` command
if (args.length >= 3 && args.length <= 4) {
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 <language>` command
if (args.length == 1) {
String language = args[0];
if (!languages.has(language)) {
channel.sendMessage("The language `" + language + "` is not supported. Type `" + data.getPrefix() + "help` to see all available languages").queue();
} else {
guildDataConnector.update(guildId, language, null, null, null, null, null, null, null, null, null, null);
logger.info(guildName + " set their language to " + language);
channel.sendMessage("Updated language to `" + language + "`").queue();
}
} else {
channel.sendMessage("Wrong number of arguments were given (expected 1, got " + args.length + "), type `" + data.getPrefix() + "help language` for help").queue();
}
break;
case "prefix": // `prefix <new prefix>` command
if (args.length == 1) {
String prefix = args[0];
if (prefix.length() == 0 || prefix.length() > 6) {
channel.sendMessage("The prefix must be between 1 and 6 characters long").queue();
} else {
String note = "";
if (prefix.contains("'") || prefix.contains("\"")) {
channel.sendMessage("Cannot use `'` or `\"` in prefix").queue();
return;
}
if (prefix.length() == 1) {
if ("!?$¥§%&@€#|/\\=.:-_+,;*+~<>^°".indexOf(prefix.charAt(0)) == -1) {
note += "\n_Note_: Because the prefix is not in `!?$¥§%&@€#|/\\=.:-_+,;*+~<>^°` you have to call commands with a blank space between it and the prefix";
}
} else {
prefix += " ";
note += "\n_Note_: Because the prefix is longer than 1 character you have to call commands with a blank space between it and the prefix";
}
guildDataConnector.update(guildId, null, null, null, null, null, null, null, prefix, null, null, null);
logger.info(guildName + " set their prefix to " + prefix);
channel.sendMessage("Updated prefix to `" + prefix + "`" + note).queue();
}
} else {
channel.sendMessage("Wrong number of arguments were given (expected 3, got " + args.length + "), type `" + data.getPrefix() + "help prefix` for help").queue();
}
break;
default:
}
}
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 <command>` 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>` command";
@ -650,35 +816,88 @@ public class DiscordCommandListener extends ListenerAdapter {
title = "`language <language>` command";
description = "Changes the language in which the timetable information are displayed. Currently only 'de' (german) and 'en' (english) are supported";
example = "`language de`";
_default = "`en`";
default_ = "`en`";
break;
case "prefix":
title = "`prefix <new 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<Long> allGuilds = new ArrayList<>();
for (Guild guild : event.getJDA().getGuilds()) {
long guildId = guild.getIdLong();
if (!guildDataConnector.has(guildId)) {
guildDataConnector.add(guildId);
}
if (!statsDataConnector.has(guildId)) {
statsDataConnector.add(guildId);
}
if (guildDataConnector.get(guildId).isCheckActive()) {
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

46
src/org/bytedream/untisbot/language.json Normal file → Executable file
View File

@ -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",

0
src/org/bytedream/untisbot/resources/logback.xml Normal file → Executable file
View File

0
src/org/bytedream/untisbot/untis/CheckCallback.java Normal file → Executable file
View File

17
src/org/bytedream/untisbot/untis/TimetableChecker.java Normal file → Executable file
View File

@ -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<LocalDate[]>() {{
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;