From f3184a2a07230d79f446c76e26a7e9505af76e6f Mon Sep 17 00:00:00 2001 From: MaxWai <44324946+maxwai@users.noreply.github.com> Date: Sun, 24 Jan 2021 22:55:25 +0100 Subject: [PATCH] changed program layout so that each command (or command Group) is it's own class and added a lot of comment and log outputs --- README.md | 35 +-- src/main/META-INF/MANIFEST.MF | 2 +- src/main/java/Bot/BotEvents.java | 199 +++++++++++++ src/main/java/Bot/BotMain.java | 130 +++++++++ src/main/java/{ => Bot}/Config.java | 3 + src/main/java/BotEvents.java | 312 --------------------- src/main/java/BotMain.java | 121 -------- src/main/java/Commands/BotStatus.java | 51 ++++ src/main/java/Commands/Countdowns.java | 337 +++++++++++++++++++++++ src/main/java/Commands/Help.java | 149 ++++++++++ src/main/java/Commands/Ping.java | 26 ++ src/main/java/Commands/Purge.java | 35 +++ src/main/java/Commands/Reload.java | 87 ++++++ src/main/java/Commands/Timezones.java | 280 +++++++++++++++++++ src/main/java/Commands/Trained.java | 73 +++++ src/main/java/Commands/package-info.java | 6 + src/main/java/Countdowns.java | 295 -------------------- src/main/java/EmbedMessages.java | 110 -------- src/main/java/Timezones.java | 168 ----------- 19 files changed, 1396 insertions(+), 1023 deletions(-) create mode 100644 src/main/java/Bot/BotEvents.java create mode 100644 src/main/java/Bot/BotMain.java rename src/main/java/{ => Bot}/Config.java (99%) delete mode 100644 src/main/java/BotEvents.java delete mode 100644 src/main/java/BotMain.java create mode 100644 src/main/java/Commands/BotStatus.java create mode 100644 src/main/java/Commands/Countdowns.java create mode 100644 src/main/java/Commands/Help.java create mode 100644 src/main/java/Commands/Ping.java create mode 100644 src/main/java/Commands/Purge.java create mode 100644 src/main/java/Commands/Reload.java create mode 100644 src/main/java/Commands/Timezones.java create mode 100644 src/main/java/Commands/Trained.java create mode 100644 src/main/java/Commands/package-info.java delete mode 100644 src/main/java/Countdowns.java delete mode 100644 src/main/java/EmbedMessages.java delete mode 100644 src/main/java/Timezones.java diff --git a/README.md b/README.md index 4d5f4a4..8f4a368 100644 --- a/README.md +++ b/README.md @@ -2,38 +2,40 @@ [![GitHub license](https://badgen.net/github/license/maxwai/ATConnect_Bot)](LICENSE) [![release](https://badgen.net/github/release/maxwai/ATConnect_Bot)](https://github.com/maxwai/ATConnect_Bot/releases) - # ATConnect Bot + The source code for the ATConnect Bot ## Getting Started ### Prerequisites -You will need Java Version 15 or later to make it work. -It may work with lower Java versions, but it was programmed using the Java 15 JDK. +You will need Java Version 15 or later to make it work. It may work with lower Java versions, but it +was programmed using the Java 15 JDK. -This Bot isa supposed to be running on only one Server at a Time. +**This Bot is supposed to be running on only one Server at a Time.** ### Installing -Download the jar file from the latest release and save it in a folder. -You will need to create at 2 files (These files will be created by the program if not present, -and you will be asked to fill them): +Download the jar file from the latest release and save it in a folder. You will need to create at 2 +files (These files will be created by the program if not present, and you will be asked to fill +them): + * Token.cfg (contains only the Bot Token) * Roles.cfg
Layout: - * Guild=\ - * Owner=\ - * Admin=\ - * Event_Organizer=\ - * Instructor=\ - * Trained=\ + * Guild=\ + * Owner=\ + * Admin=\ + * Event_Organizer=\ + * Instructor=\ + * Trained=\ ### How to Use -* start the jar file in a terminal with the command `java -jar ATConnect_Bot.jar`
+* Start the jar file in a terminal with the command `java -jar ATConnect_Bot.jar`
(do not just double click it to open it) -* For now, all commands are only in Discord, command line commands are in the works +* All commands are only in Discord, command line commands are not necessary since the Bot is + supposed to be run as a daemon and a service for easiness ## TODO @@ -45,4 +47,5 @@ and you will be asked to fill them): ## License [![GitHub license](https://badgen.net/github/license/maxwai/ATConnect_Bot)](LICENSE) -This project is licensed under the GNU General Public License - see the [LICENSE](LICENSE) file for details \ No newline at end of file +This project is licensed under the GNU General Public License - see the [LICENSE](LICENSE) file for +details \ No newline at end of file diff --git a/src/main/META-INF/MANIFEST.MF b/src/main/META-INF/MANIFEST.MF index 8559068..eca0b2b 100644 --- a/src/main/META-INF/MANIFEST.MF +++ b/src/main/META-INF/MANIFEST.MF @@ -1,3 +1,3 @@ Manifest-Version: 1.0 -Main-Class: BotMain +Main-Class: Bot.BotMain diff --git a/src/main/java/Bot/BotEvents.java b/src/main/java/Bot/BotEvents.java new file mode 100644 index 0000000..94e8b13 --- /dev/null +++ b/src/main/java/Bot/BotEvents.java @@ -0,0 +1,199 @@ +package Bot; + +import Commands.BotStatus; +import Commands.Countdowns; +import Commands.Help; +import Commands.Ping; +import Commands.Purge; +import Commands.Reload; +import Commands.Timezones; +import Commands.Trained; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent; +import net.dv8tion.jda.api.hooks.SubscribeEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BotEvents { + + /** + * Adds a Trashcan so that the Message can be easily deleted by users + * + * @param message The message where the Trashcan should be added + */ + public static void addTrashcan(Message message) { + message.addReaction("\uD83D\uDDD1") + .queue(); // add a reaction to make it easy to delete the post + } + + /** + * Deletes the message after the given amount of time in Seconds + * + * @param message The message to delete + * @param seconds Time until the message should be deleted + */ + public static void deleteMessageAfterXTime(Message message, long seconds) { + try { + Thread.sleep(seconds * 1000); + } catch (InterruptedException ignored) { + } + message.delete().queue(); // delete the Message after X sec + } + + /** + * Returns the Name of the User on the Server + * + * @param member User where we want the name + * + * @return Return the Nickname, or username if nickname is not available + */ + public static String getServerName(Member member) { + String nickname = member.getNickname(); + if (nickname == null) + nickname = member.getEffectiveName(); + return nickname; + } + + /** + * Is triggered when an Emote is added / count is changed + * + * @param event Reaction add Event + */ + @SubscribeEvent + public void onEmoteAdded(GuildMessageReactionAddEvent event) { + if (event.getUser().isBot()) return; // don't react if the bot is adding this emote + Member authorMember = event.getGuild().getMember(event.getUser()); + List rolesOfUser = authorMember != null ? authorMember.getRoles() : new ArrayList<>(); + boolean isAdmin = + rolesOfUser.contains(event.getGuild().getRoleById(BotMain.ROLES.get("Admin"))) || + event.getUser().getIdLong() == BotMain.ROLES.get("Owner"); + event.retrieveMessage() + .queue(message -> { // only do something if he is admin or the wastebasket was already here from the bot + if (!message.getAuthor().isBot()) return; // delete only bot messages + if ((isAdmin || message.getReactions().get(0).isSelf()) + && event.getReactionEmote().isEmoji()) { + String emoji = event.getReactionEmote().getEmoji(); + if (emoji.substring(0, emoji.length() - 1).equals("\uD83D\uDDD1") + || emoji.equals("\uD83D\uDDD1")) { // :wastebasket: + LoggerFactory.getLogger("ReactionAdded") + .info("deleting message because of :wastebasket: reaction"); + // check if this message was part of a Countdown + if (Countdowns.messageIds.contains(message.getId())) + // close that countdown since the message will be deleted + Countdowns.closeSpecificThread(message.getId()); + message.delete().queue(); // delete the message + } + } + }); + } + + /** + * Is triggered when the Nickname of a User is changed + * + * @param event Nickname Update Event + */ + @SubscribeEvent + public void onNicknameChanged(GuildMemberUpdateNicknameEvent event) { + String newNickOG = event.getNewNickname(); + User user = event.getUser(); + if (!user.isBot() && newNickOG != null) { // User should not be a Bot + String newNick = newNickOG.toLowerCase(Locale.ROOT); + // User should not be an Alt account + if (newNick.contains("[z") && !newNick.contains("[alt")) { + String newOffset = newNick.substring(newNick.indexOf("[z")); + String oldNick = event.getOldNickname(); + Logger logger = LoggerFactory.getLogger("NicknameChanged"); + if (oldNick != null) { + oldNick = oldNick.toLowerCase(Locale.ROOT); + String oldOffset = oldNick.substring(oldNick.indexOf("[z")); + // don't change the timezone if the offset is the same + if (!newOffset.equals(oldOffset)) { + if (Timezones.updateSpecificTimezone(user.getIdLong(), newOffset)) + logger.info("Updated Timezone of User: " + newNickOG); + else + logger.error("Could not save Timezone of User: " + newNickOG); + } + } else { // old nick could not be loaded but new nick is still there so load timezone anyway + if (Timezones.updateSpecificTimezone(user.getIdLong(), newOffset)) + logger.info("Updated Timezone of User: " + event.getNewNickname()); + else + logger.error("Could not save Timezone of User: " + event.getNewNickname()); + } + } + } + } + + /** + * Is triggered when a Message is written + * + * @param event Message Receive Event + */ + @SubscribeEvent + public void onReceiveMessage(MessageReceivedEvent event) { + if (event.getAuthor().isBot()) return; + Logger logger = LoggerFactory.getLogger("ReceivedMessage"); + String content = event.getMessage().getContentRaw().toLowerCase(Locale.ROOT); + MessageChannel channel = event.getChannel(); + + // These boolean are always false if written in a personal message + boolean isAdmin = false; + boolean isEventOrganizer = false; + boolean isInstructor = false; + + // if this message is from a Guild/Server then check the roles of the User + if (event.isFromGuild()) { + Member authorMember = event.getGuild().getMember(event.getAuthor()); + List rolesOfUser = + authorMember != null ? authorMember.getRoles() : new ArrayList<>(); + isAdmin = rolesOfUser + .contains(event.getGuild().getRoleById(BotMain.ROLES.get("Admin"))); + isEventOrganizer = rolesOfUser + .contains(event.getGuild().getRoleById(BotMain.ROLES.get("Event_Organizer"))); + isInstructor = rolesOfUser + .contains(event.getGuild().getRoleById(BotMain.ROLES.get("Instructor"))); + } + boolean isOwner = event.getAuthor().getIdLong() == BotMain.ROLES.get("Owner"); + isAdmin = isAdmin || isOwner; // Owner is also admin + + if (content.length() != 0 && content.charAt(0) == '!') { + logger.info("Received Message from " + event.getAuthor().getName() + ": " + content); + String command; + content = content.substring(1); + if (content.indexOf(' ') != -1) + command = content.substring(0, content.indexOf(' ')); + else + command = content; + switch (command) { + case "help" -> Help.showHelp(isInstructor, isEventOrganizer, isAdmin, + channel); // show Help Page + case "ping" -> Ping.makePing(channel); // make a ping test + case "time" -> Timezones + .getTimezoneOfUserCommand(event, content); // get the Timezone of a User + case "timezones" -> Timezones.getTimezoneOfAllUsersCommand(event.getGuild(), + channel); // print the Timezone of all Users + case "trained" -> Trained.makeUserTrained(isInstructor, event, + channel); // give a User the Trained Role + case "countdown" -> Countdowns.countdownCommand(isEventOrganizer, isOwner, event, + channel); // create a live countdown + case "restart" -> BotStatus + .restartBot(isAdmin, channel); // restarts the Bot connection + case "reload" -> Reload.reloadMain(isAdmin, event, channel, + content); // reload the Config files or Timezones + case "purge" -> Purge.purgeMessages(isOwner, channel, + content); // purges X Messages from the channel + case "stop" -> BotStatus + .stopBot(isOwner, channel); // stops the Bot, this takes a while + } + } + } + +} diff --git a/src/main/java/Bot/BotMain.java b/src/main/java/Bot/BotMain.java new file mode 100644 index 0000000..b94a8dd --- /dev/null +++ b/src/main/java/Bot/BotMain.java @@ -0,0 +1,130 @@ +package Bot; + +import Commands.Countdowns; +import Commands.Timezones; +import java.util.Map; +import javax.security.auth.login.LoginException; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.hooks.AnnotatedEventManager; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.MemberCachePolicy; + +public class BotMain { + + /** + * The IDs for when the Bot is restarted to know which Message to edit + */ + public static final long[] restartIDs = new long[2]; + /** + * Lock for the {@link #restartIDs} + */ + public static final Object lock = new Object(); + /** + * Token of the Bot, Fetch with the Token.cfg file + */ + public static String TOKEN = Config.getToken(); + /** + * Map of special Role IDs: + * Admin, Owner, Event Organizer + */ + public static Map ROLES = Config.getRoles(); + /** + * {@link JDA} Instance of the Bot + */ + private static JDA jda; + /** + * {@link JDABuilder} for the Bot + */ + private static JDABuilder jdaBuilder; + + public static void main(String[] args) throws LoginException { + initializeJDABuilder(); + connectBot(); + } + + /** + * Will setup the JDA Builder with the necessary settings + */ + private static void initializeJDABuilder() { + jdaBuilder = JDABuilder.createDefault(TOKEN) + // set to Event Manager to use @Annotated Methods + .setEventManager(new AnnotatedEventManager()) + // add the Event Listener Class + .addEventListeners(new BotEvents()) + // set that the Bot is "listening to !help" + .setActivity(Activity.listening("!help")) + // disable the Presences and typing Intents since not used + .disableIntents(GatewayIntent.GUILD_PRESENCES, GatewayIntent.GUILD_MESSAGE_TYPING) + // enable the Message reaction and guild members intents + .enableIntents(GatewayIntent.GUILD_MESSAGE_REACTIONS, GatewayIntent.GUILD_MEMBERS) + // cache all members, this is used for member fetching + .setMemberCachePolicy(MemberCachePolicy.ALL); + } + + /** + * Will reload the Configs (without the Countdowns and Timezones) + */ + public static void reloadConfig() { + TOKEN = Config.getToken(); + ROLES = Config.getRoles(); + } + + /** + * Will restart the Bot + */ + public static void restartBot() { + disconnectBot(); // disconnect the Bot + try { + connectBot(); // connect the Bot + } catch (LoginException e) { + e.printStackTrace(); + } + synchronized (lock) { + while (restartIDs[1] == 0L) { + try { + lock.wait(); + } catch (InterruptedException ignored) { + } + } + TextChannel channel = jda.getTextChannelById(restartIDs[0]); + if (channel != null) + channel.editMessageById(restartIDs[1], "Bot successfully restarted").queue(); + restartIDs[0] = 0; + restartIDs[1] = 0; + } + } + + /** + * Connect to the Bot and load the Countdowns + * + * @throws LoginException if the TOKEN of the Bot is wrong + */ + private static void connectBot() throws LoginException { + jda = jdaBuilder.build(); + try { + jda.awaitReady(); // wait that the Bot is fully connected + Countdowns.restartCountdowns(jda); + Guild guild = jda + .getGuildById(ROLES.get("Guild")); // get the Guild where the Bot is active + if (guild != null) + guild.loadMembers().onSuccess(members -> {}); // load all Members into cache + Timezones.loadTimezones(); // load the all timezones of all the Users + } catch (InterruptedException ignored) { + } + } + + /** + * Will disconnect the Bot and save the Countdowns + */ + public static void disconnectBot() { + Countdowns.closeAllThreads(); // finish and save the Countdowns + Timezones.saveTimezones(); // save all USer Timezones + jda.getRegisteredListeners().forEach(jda::removeEventListener); + jda.shutdown(); + } + +} diff --git a/src/main/java/Config.java b/src/main/java/Bot/Config.java similarity index 99% rename from src/main/java/Config.java rename to src/main/java/Bot/Config.java index 4cc8fbf..12e80e1 100644 --- a/src/main/java/Config.java +++ b/src/main/java/Bot/Config.java @@ -1,3 +1,6 @@ +package Bot; + +import Commands.Countdowns; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/BotEvents.java b/src/main/java/BotEvents.java deleted file mode 100644 index 5cde17f..0000000 --- a/src/main/java/BotEvents.java +++ /dev/null @@ -1,312 +0,0 @@ -import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.MessageBuilder; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.MessageChannel; -import net.dv8tion.jda.api.entities.Role; -import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent; -import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent; -import net.dv8tion.jda.api.exceptions.HierarchyException; -import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; -import net.dv8tion.jda.api.hooks.SubscribeEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -public class BotEvents { - - /** - * Is triggered when an Emote is added / count is changed - * @param event Reaction add Event - */ - @SubscribeEvent - public void onEmoteAdded(GuildMessageReactionAddEvent event) { - if(event.getUser().isBot()) return; // don't react if the bot is adding this emote - Member authorMember = event.getGuild().getMember(event.getUser()); - List rolesOfUser = authorMember != null ? authorMember.getRoles() : new ArrayList<>(); - boolean isAdmin = rolesOfUser.contains(event.getGuild().getRoleById(BotMain.ROLES.get("Admin"))) || - event.getUser().getIdLong() == BotMain.ROLES.get("Owner"); - event.retrieveMessage().queue(message -> { // only do something if he is admin or the wastebasket was already here from the bot - if(isAdmin || message.getReactions().get(0).isSelf()) { - if (!message.getAuthor().isBot()) return; // delete only bot messages - if (event.getReactionEmote().isEmoji()) { - String emoji = event.getReactionEmote().getEmoji(); - if (emoji.substring(0, emoji.length() - 1).equals("\uD83D\uDDD1") || emoji.equals("\uD83D\uDDD1")) { // :wastebasket: - LoggerFactory.getLogger("ReactionAdded").info("deleting message because of :wastebasket: reaction"); - if (Countdowns.messageIds.contains(message.getId())) { // check if this message was part of a Countdown - Countdowns.closeSpecificThread(message.getId()); // close that countdown since the message will be deleted - } - message.delete().queue(); - } - } - } - }); - } - - /** - * Is triggered when the Nickname of a User is changed - * @param event Nickname Update Event - */ - @SubscribeEvent - public void onNicknameChanged(GuildMemberUpdateNicknameEvent event) { - String newNick = event.getNewNickname(); - if(!event.getUser().isBot() && newNick != null) { // User should not be a Bot - newNick = newNick.toLowerCase(Locale.ROOT); - if(newNick.contains("[z") && !newNick.contains("[alt")){ // User should not be an Alt account - String newOffset = newNick.substring(newNick.indexOf("[z")); - String oldNick = event.getOldNickname(); - Logger logger = LoggerFactory.getLogger("NicknameChanged"); - if (oldNick != null) { - oldNick = oldNick.toLowerCase(Locale.ROOT); - String oldOffset = oldNick.substring(oldNick.indexOf("[z")); - if (!newOffset.equals(oldOffset)) { // don't change the timezone if the offset is the same - if (Timezones.updateSpecificTimezone(event.getUser().getIdLong(), newOffset)) { - logger.info("Updated Timezone of User: " + event.getNewNickname()); - } else { - logger.error("Could not save Timezone of User: " + event.getNewNickname()); - } - } - } else { // old nick could not be loaded but new nick is still there so load timezone anyway - if (Timezones.updateSpecificTimezone(event.getUser().getIdLong(), newOffset)) { - logger.info("Updated Timezone of User: " + event.getNewNickname()); - } else { - logger.error("Could not save Timezone of User: " + event.getNewNickname()); - } - } - } - } - } - - /** - * Is triggered when a Message is written - * @param event Message Receive Event - */ - @SubscribeEvent - public void onReceiveMessage(MessageReceivedEvent event) { - if (event.getAuthor().isBot()) return; - Logger logger = LoggerFactory.getLogger("ReceivedMessage"); - String content = event.getMessage().getContentRaw().toLowerCase(Locale.ROOT); - MessageChannel channel = event.getChannel(); - - // These boolean are always false if written in a personal message - boolean isAdmin = false; - boolean isEventOrganizer = false; - boolean isInstructor = false; - - // if this message is from a Guild/Server then check if they have the Admin and/or event organizer role - if(event.isFromGuild()) { - Member authorMember = event.getGuild().getMember(event.getAuthor()); - List rolesOfUser = authorMember != null ? authorMember.getRoles() : new ArrayList<>(); - isAdmin = rolesOfUser.contains(event.getGuild().getRoleById(BotMain.ROLES.get("Admin"))); - isEventOrganizer = rolesOfUser.contains(event.getGuild().getRoleById(BotMain.ROLES.get("Event_Organizer"))); - isInstructor = rolesOfUser.contains(event.getGuild().getRoleById(BotMain.ROLES.get("Instructor"))); - } - boolean isOwner = event.getAuthor().getIdLong() == BotMain.ROLES.get("Owner"); - isAdmin = isAdmin || isOwner; // Owner is also admin - - if (content.length() != 0 && content.charAt(0) == '!') { - logger.info("Received Message from " + event.getAuthor().getName() + ": " + content); - String command; - content = content.substring(1); - if(content.indexOf(' ') != -1) - command = content.substring(0, content.indexOf(' ')); - else - command = content; - switch (command) { - case "help" -> { // show Help Page - EmbedBuilder eb = EmbedMessages.getHelpPage(); - if(isInstructor) - EmbedMessages.getInstructor(eb); // attach Instructor only commands - if(isEventOrganizer) - EmbedMessages.getEventOrganizer(eb); // attach Event Organizer only commands - if(isAdmin) - EmbedMessages.getAdminHelpPage(eb); // attach Admin only commands - channel.sendMessage(eb.build()).queue(BotEvents::addTrashcan); - } - case "ping" -> { // make a ping test - long time = System.currentTimeMillis(); - channel.sendMessage("Pong!").queue(message -> - message.editMessageFormat("Pong: %d ms", System.currentTimeMillis() - time).queue()); - } - case "time" -> { - if(content.length() != 4) { - content = content.substring(5); - ArrayList members = new ArrayList<>(); - StringBuilder output = new StringBuilder("```\n"); - while(!content.equals("")) { - if(content.charAt(0) == '<') { - members.add(Long.valueOf(content.substring(3, content.indexOf(">")))); - content = content.substring(content.indexOf(">") + 1); - } else { - final String name; - if(content.contains(",")) { - name = content.substring(0, content.indexOf(',')).toLowerCase(Locale.ROOT); - content = content.substring(content.indexOf(',') + 1); - } else { - name = content.toLowerCase(Locale.ROOT); - content = ""; - } - final int size = members.size(); - event.getGuild().getMembers().forEach(member -> { - if(size == members.size()) { - String serverName = getServerName(member).toLowerCase(Locale.ROOT); - if (!serverName.contains("[alt")) { - if (serverName.contains(name)) { - members.add(member.getIdLong()); - } - } - } - }); - if(size == members.size()) { - String error = "Could not find User: " + name; - logger.warn(error); - output.append(error).append("\n"); - } - } - if(content.length() != 0 && content.charAt(0) == ' ') - content = content.substring(1); - } - members.forEach(memberID -> output.append(Timezones.printUserLocalTime(memberID, event.getGuild())).append("\n")); - event.getChannel().sendMessage(output.append("```").toString()).queue(); - } - } - case "timezones" -> { - channel.sendMessage(Timezones.printAllUsers(event.getGuild())).queue(); - } - case "trained" -> { - if(isInstructor) { - String errorMessage = null; - List member = event.getMessage().getMentionedMembers(); - if(member.size() == 1) { - Role trainedRole = event.getGuild().getRoleById(BotMain.ROLES.get("Trained")); - if(trainedRole != null) { - try { - event.getGuild().addRoleToMember(member.get(0), trainedRole).queue(); - MessageBuilder messageBuilder = new MessageBuilder(); - messageBuilder.append(member.get(0)).append(" has the Role `Trained`"); - channel.sendMessage(messageBuilder.build()).queue(); - } catch (HierarchyException e) { - errorMessage = "Could not add the Role to User because Bot has his Role under the Trained role"; - channel.sendMessage(errorMessage).queue(BotEvents::addTrashcan); - } catch (InsufficientPermissionException e) { - errorMessage = "Bot doesn't have the Permission Manage Roles"; - channel.sendMessage(errorMessage).queue(BotEvents::addTrashcan); - } - } else { - errorMessage = "Can't find the Trained Role. Please update the role ID's"; - channel.sendMessage(errorMessage).queue(BotEvents::addTrashcan); - } - } else if(member.size() == 0) { - channel.sendMessage("You have to mention a User that is Trained").queue(); - } else { - channel.sendMessage("Can't mention multiple members at once, please mention one member at a time"). - queue(message -> deleteMessageAfterXTime(message, 10)); - } - if(errorMessage != null) { - logger.error(errorMessage); - } - } else - channel.sendMessage("You don't have permission for this command").queue(); - } - case "countdown" -> { // create a live countdown - if (isEventOrganizer || isOwner) { - Countdowns.startNewCountdown(event); - } else - channel.sendMessage("You don't have permission for this command").queue(); - } - case "restart" -> { // restarts the Bot connection - if (isAdmin) { - BotMain.restartIDs[0] = channel.getIdLong(); - channel.sendMessage("restarting Bot").queue(message -> { - synchronized (BotMain.lock) { - BotMain.restartIDs[1] = message.getIdLong(); - BotMain.lock.notify(); - } - }); - BotMain.restartBot(); - } else - channel.sendMessage("You don't have permission for this command").queue(); - } - case "reload" -> { // reload the Config files or Timezones - if (isAdmin) { - if (content.equals("reload")) { - channel.sendMessage("Please specify what to reload. Possible is: config, timezones") - .queue(message -> deleteMessageAfterXTime(message, 10)); - } else { - content = content.substring(7); - switch (content) { - case "config" -> // Reload Config files - channel.sendMessage("reloading Config").queue(message -> { - BotMain.reloadConfig(); - message.editMessage("Config reloaded successfully").queue(); - }); - case "timezones", "timezone" -> // Reload Timezones - channel.sendMessage("reloading all user Timezones").queue(message -> { - Timezones.updateTimezones(event.getJDA()); - message.editMessage("Timezones reloaded").queue(); - }); - default -> channel.sendMessage("Please specify what to reload. Possible is: config, timezones") - .queue(message -> deleteMessageAfterXTime(message, 10)); - } - } - } else - channel.sendMessage("You don't have permission for this command").queue(); - } - case "purge" -> { - if(isOwner) { - int size = 1 + Integer.parseInt(content.substring(6)); // add 1 since the command should not count - event.getChannel().getIterableHistory().takeAsync(size).thenAccept(channel::purgeMessages) - .thenAccept(unused -> event.getChannel().sendMessage((size - 1) + " Messages Purged") - .queue(BotEvents::addTrashcan)); - - } else - channel.sendMessage("You don't have permission for this command").queue(); - } - case "stop" -> { // stops the Bot, this takes a while - if(isOwner) { - channel.sendMessage("stopping the Bot. Bye...").queue(); - BotMain.disconnectBot(); - } else - channel.sendMessage("You don't have permission for this command").queue(); - } - } - } - } - - /** - * Adds a Trashcan so that the Message can be easily deleted by users - * @param message The message where the Trashcan should be added - */ - public static void addTrashcan(Message message) { - message.addReaction("\uD83D\uDDD1").queue(); // add a reaction to make it easy to delete the post - } - - /** - * Deletes the message after the given amount of time in Seconds - * @param message The message to delete - * @param seconds Time until the message should be deleted - */ - public static void deleteMessageAfterXTime(Message message, long seconds) { - try { - Thread.sleep(seconds * 1000); - } catch (InterruptedException ignored) {} - message.delete().queue(); // delete the Message after X sec - } - - /** - * Returns the Name of the User on the Server - * @param member User where we want the name - * @return Return the Nickname, or username if nickname is not available - */ - public static String getServerName(Member member) { - String nickname = member.getNickname(); - if(nickname == null) - nickname = member.getEffectiveName(); - return nickname; - } - -} diff --git a/src/main/java/BotMain.java b/src/main/java/BotMain.java deleted file mode 100644 index c8c1fb6..0000000 --- a/src/main/java/BotMain.java +++ /dev/null @@ -1,121 +0,0 @@ -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.JDABuilder; -import net.dv8tion.jda.api.entities.Activity; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.TextChannel; -import net.dv8tion.jda.api.hooks.AnnotatedEventManager; -import net.dv8tion.jda.api.requests.GatewayIntent; -import net.dv8tion.jda.api.utils.MemberCachePolicy; - -import javax.security.auth.login.LoginException; -import java.util.Map; - -public class BotMain { - - /** - * Token of the Bot, Fetch with the Token.cfg file - */ - public static String TOKEN = Config.getToken(); - /** - * Map of special Role IDs: - * Admin, Owner, Event Organizer - */ - public static Map ROLES = Config.getRoles(); - - /** - * The IDs for when the Bot is restarted to know which Message to edit - */ - public static final long[] restartIDs = new long[2]; - /** - * Lock for the {@link #restartIDs} - */ - public static final Object lock = new Object(); - - /** - * {@link JDA} Instance of the Bot - */ - private static JDA jda; - /** - * {@link JDABuilder} for the Bot - */ - private static JDABuilder jdaBuilder; - - public static void main(String[] args) throws LoginException { - initializeJDABuilder(); - connectBot(); - } - - /** - * Will setup the JDA Builder with the necessary settings - */ - private static void initializeJDABuilder() { - jdaBuilder = JDABuilder.createDefault(TOKEN) - .setEventManager(new AnnotatedEventManager()) // set to Event Manager to use @Annotated Methods - .addEventListeners(new BotEvents()) // add the Event Listener Class - .setActivity(Activity.listening("!help")) // set that the Bot is "listening to !help" - .disableIntents(GatewayIntent.GUILD_PRESENCES, GatewayIntent.GUILD_MESSAGE_TYPING) // disable the Presences and typing Intents since not used - .enableIntents(GatewayIntent.GUILD_MESSAGE_REACTIONS, GatewayIntent.GUILD_MEMBERS) // enable the Message reaction and guild members intents - .setMemberCachePolicy(MemberCachePolicy.ALL); // cache all members, this is used for member fetching - } - - /** - * Will reload the Configs (without the Countdowns and Timezones) - */ - public static void reloadConfig() { - TOKEN = Config.getToken(); - ROLES = Config.getRoles(); - } - - /** - * Will restart the Bot - */ - public static void restartBot(){ - disconnectBot(); // disconnect the Bot - try { - connectBot(); // connect the Bot - } catch (LoginException e) { - e.printStackTrace(); - } - synchronized (lock) { - while (restartIDs[1] == 0L) { - try { - lock.wait(); - } catch (InterruptedException ignored) { - } - } - TextChannel channel = jda.getTextChannelById(restartIDs[0]); - if (channel != null) - channel.editMessageById(restartIDs[1], "Bot successfully restarted").queue(); - restartIDs[0] = 0; - restartIDs[1] = 0; - } - } - - /** - * Connect to the Bot and load the Countdowns - * @throws LoginException if the TOKEN of the Bot is wrong - */ - private static void connectBot() throws LoginException { - jda = jdaBuilder.build(); - try { - jda.awaitReady(); // wait that the Bot is fully connected - Countdowns.restartCountdowns(jda); - Guild guild = jda.getGuildById(ROLES.get("Guild")); // get the Guild where the Bot is active - if(guild != null) - guild.loadMembers().onSuccess(members -> {}); // load all Members into cache - Timezones.loadTimezones(); // load the all timezones of all the Users - } catch (InterruptedException ignored) { - } - } - - /** - * Will disconnect the Bot and save the Countdowns - */ - public static void disconnectBot() { - Countdowns.closeAllThreads(); // finish and save the Countdowns - Timezones.saveTimezones(); // save all USer Timezones - jda.getRegisteredListeners().forEach(jda::removeEventListener); - jda.shutdown(); - } - -} diff --git a/src/main/java/Commands/BotStatus.java b/src/main/java/Commands/BotStatus.java new file mode 100644 index 0000000..27b1d68 --- /dev/null +++ b/src/main/java/Commands/BotStatus.java @@ -0,0 +1,51 @@ +package Commands; + +import Bot.BotMain; +import net.dv8tion.jda.api.entities.MessageChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BotStatus { + + /** + * The Logger for Log Messages + */ + private static final Logger logger = LoggerFactory.getLogger("Bot Status"); + + /** + * Will restart the bot Connection + * + * @param isAdmin If the User is an Admin + * @param channel The Channel where the Message was send. + */ + public static void restartBot(boolean isAdmin, MessageChannel channel) { + if (isAdmin) { // only Admin is allowed to restart the Bot + BotMain.restartIDs[0] = channel.getIdLong(); // save the channel ID for later + logger.info("Restarting Bot"); + channel.sendMessage("restarting Bot").queue(message -> { + synchronized (BotMain.lock) { + BotMain.restartIDs[1] = message.getIdLong(); // save the message ID for later + BotMain.lock + .notify(); // notify another Thread that the Bot can be restarted now + } + }); + BotMain.restartBot(); // restart Bot + } else // User isn't an Admin + channel.sendMessage("You don't have permission for this command").queue(); + } + + /** + * Will Stop the Bot + * + * @param isOwner If the User is the Owner of the Bot + * @param channel The Channel where the Message was send. + */ + public static void stopBot(boolean isOwner, MessageChannel channel) { + if (isOwner) { // only the Owner is allowed to stop the Bot + logger.info("Stopping Bot"); + channel.sendMessage("stopping the Bot. Bye...").queue(); + BotMain.disconnectBot(); // stop the Bot + } else // User isn't the Owner + channel.sendMessage("You don't have permission for this command").queue(); + } +} diff --git a/src/main/java/Commands/Countdowns.java b/src/main/java/Commands/Countdowns.java new file mode 100644 index 0000000..372de7f --- /dev/null +++ b/src/main/java/Commands/Countdowns.java @@ -0,0 +1,337 @@ +package Commands; + +import Bot.BotEvents; +import Bot.Config; +import java.time.Instant; +import java.util.Stack; +import java.util.concurrent.TimeUnit; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Countdowns { + + /** + * Stack of all the Ids of the Countdown messages + */ + public static final Stack messageIds = new Stack<>(); + /** + * Stack of all the Countdown Threads + */ + private static final Stack countdowns = new Stack<>(); + /** + * The Logger for Log Messages + */ + private static final Logger logger = LoggerFactory.getLogger("Countdown"); + + /** + * Will add a new countdown to the List + * + * @param isEventOrganizer If this User is an Event Organizer + * @param isOwner If the User is the Owner of the Bot + * @param event Event to get more information + * @param channel The Channel where the Message was send. + * This may be removed in a further release since this can also be fetched with the event + * Instance + */ + public static void countdownCommand(boolean isEventOrganizer, boolean isOwner, + MessageReceivedEvent event, MessageChannel channel) { + if (isEventOrganizer || isOwner) // Countdown can only be done by Event Organizer or Owner + startNewCountdown(event, channel); + else // isn't Event Organizer or Owner + channel.sendMessage("You don't have permission for this command").queue(); + } + + /** + * Start a new Countdown with the given Information in the Event + * + * @param event The Event with the Countdown Information + */ + private static void startNewCountdown(MessageReceivedEvent event, MessageChannel channel) { + logger.debug("detected countdown command"); + String content = event.getMessage().getContentRaw(); + try { + channel.deleteMessageById(event.getMessageId()).queue(); + content = content.substring(11); + + // take the time information of the countdown + int day = Integer.parseInt(content.substring(0, content.indexOf('.'))); + content = content.substring(content.indexOf('.') + 1); + int month = Integer.parseInt(content.substring(0, content.indexOf('.'))); + content = content.substring(content.indexOf('.') + 1); + int year = Integer.parseInt(content.substring(0, content.indexOf(' '))); + content = content.substring(content.indexOf(' ') + 1); + int hour = Integer.parseInt(content.substring(0, content.indexOf(':'))); + content = content.substring(content.indexOf(':') + 1); + int minutes; + + // take the extra text that needs to be displayed after the countdown + if (content.length() <= 2) { + minutes = Integer.parseInt(content); + content = ""; + } else { + minutes = Integer.parseInt(content.substring(0, content.indexOf(' '))); + content = content.substring(content.indexOf(' ')); + } + + // Parse the date to a Instant Instance + Instant date = Instant.parse(year + "-" + String.format("%02d", month) + "-" + + String.format("%02d", day) + "T" + String.format("%02d", hour) + ":" + + String.format("%02d", minutes) + ":00Z"); + + // Check if the time is not in th past + if (date.getEpochSecond() < Instant.now().getEpochSecond()) { + channel.sendMessage( + "You tried making a countdown in the past. (Message will delete after 10 sec)") + .queue(message -> BotEvents.deleteMessageAfterXTime(message, 10)); + logger.warn("User tried making a countdown in the past"); + return; + } + logger.info("Starting countdown thread"); + // start the Thread that changes the Message to the current Countdown + CountdownsThread countdownsThread = new CountdownsThread(channel, content, date); + countdownsThread.start(); + countdowns.push(countdownsThread); + saveAllCountdowns(); // save the countdown in the event of unexpected failure + } catch (NumberFormatException | StringIndexOutOfBoundsException ignored) { // The command had some parsing error + channel.sendMessage("Something went wrong with your command try again\n" + + "Format is: `!countdown DD.MM.YYYY HH:mm `").queue(); + } + } + + /** + * Restarts the Countdowns that are in the Countdowns.cfg file + * + * @param jda The JDA to get the Channels and Messages + */ + public static void restartCountdowns(JDA jda) { + Config.getCountdowns().forEach(countdownInfos -> { + // Pick up the channel where the Message is + TextChannel channel = jda.getTextChannelById(countdownInfos[0]); + if (channel + != null) { // check if the Channel is still there, if not ignore this Countdown + channel.retrieveMessageById(countdownInfos[1]) + .queue(message -> { // message still exists + Instant date = Instant.parse(countdownInfos[3]); + if (date.getEpochSecond() < Instant.now() + .getEpochSecond()) { // check if the Countdown is in the past + message.editMessage("Countdown finished").queue(); + BotEvents.addTrashcan( + message); // add a reaction to make it easy to delete the post + } else { + logger.info("Added Countdown Thread from existing Countdown"); + CountdownsThread countdownsThread = + new CountdownsThread(channel, countdownInfos[1], + countdownInfos[2], date); + countdownsThread.start(); + countdowns.push(countdownsThread); + } + }, throwable -> logger + .warn("Removing one Countdown where Message is deleted")); // If the message is deleted + } else { + logger.warn("Removing one Countdown where Channel is deleted"); + } + }); + } + + /** + * Save all active Countdowns in the Countdowns.cfg + */ + private static void saveAllCountdowns() { + Config.saveCountdowns(countdowns); + } + + /** + * Close a specific Countdown with its Message ID + * + * @param messageId The Message ID of the Countdown that needs to be closed + */ + public static void closeSpecificThread(String messageId) { + int index = messageIds.indexOf(messageId); + if (index == -1) return; + CountdownsThread thread = countdowns.get(index); + thread.interrupt(); + countdowns.remove(index); + messageIds.remove(index); + } + + /** + * Close all Thread and save them in Countdowns.cfg + */ + public static void closeAllThreads() { + saveAllCountdowns(); + countdowns.forEach(Thread::interrupt); + } + + /** + * Thread Class for the Countdowns + */ + public static class CountdownsThread extends Thread { + + /** + * Lock for {@link #stop} + */ + private final Object lock = new Object(); + /** + * The Channel where the Message of the Countdown is + */ + private final MessageChannel channel; + /** + * The Text that is added at the end of the Countdown + */ + private final String text; + /** + * The Date of the End of the Countdown + */ + private final Instant date; + /** + * boolean to know if the Thread should be closed + */ + private boolean stop = false; + /** + * The Message ID of the Countdown + */ + private String messageId; + + /** + * Constructor when the Countdown is restored after a restart + * + * @param channel The Channel where the Message of the Countdown is + * @param messageId The Message ID of the Countdown + * @param text The Text that is added at the end of the Countdown + * @param date The Date of the End of the Countdown + */ + private CountdownsThread(MessageChannel channel, String messageId, String text, + Instant date) { + this.text = text; + this.date = date; + this.channel = channel; + this.messageId = messageId; + messageIds.push(this.messageId); + } + + /** + * Constructor for a new Countdown + * + * @param channel The Channel where the Message of the Countdown will be + * @param text The Text that is added at the end of the Countdown + * @param date The Date of the End of the Countdown + */ + private CountdownsThread(MessageChannel channel, String text, Instant date) { + this.text = text; + this.date = date; + this.channel = channel; + + logger.info("sending message"); + channel.sendMessage(computeLeftTime()[0] + text).queue(message -> { + synchronized (lock) { + this.messageId = message.getId(); + messageIds.push(this.messageId); + lock.notify(); // notify the Thread that the Message Id is available + } + }); + } + + /** + * Compute the String Array with the information for the Countdowns.cfg File + * + * @return A String Array with following layout: {@code {channelId, messageId, text, date}} + */ + public String[] getInfos() { + return new String[]{channel.getId(), messageId, text, date.toString()}; + } + + /** + * The main Function of this Thread + */ + @Override + public void run() { + synchronized (lock) { + if (messageId == null) { // If don't yet have a message ID wait for it + try { + lock.wait(); + } catch (InterruptedException ignored) { + } + } + } + while (!stop) { // don't stop until wanted + Object[] info = computeLeftTime(); + if (info[0] instanceof Boolean) { // if this countdown is at it's end + logger.info("Countdown finished removing it from the Threads List"); + channel.editMessageById(messageId, "Countdown finished") + .queue(BotEvents::addTrashcan, + // add a reaction to make it easy to delete the post + throwable -> logger + .warn("Removing one Countdown where Message is deleted")); // Message was deleted, don't do anything here + countdowns.remove(this); + return; + } + logger.info("editing message: " + info[0]); + channel.editMessageById(messageId, info[0] + text) + .queue(message -> {}, throwable -> { + // Message was deleted, delete this Thread + stop = true; + countdowns.remove(this); + logger.warn("Removing one Countdown where Message is deleted"); + }); + try { + long sleepTime = (Long) info[1]; // sleep until the next change + //noinspection BusyWait + sleep(sleepTime < 5000 ? 60000 : sleepTime); + } catch (InterruptedException ignored) { + } + } + } + + @Override + public void interrupt() { + stop = true; + super.interrupt(); + } + + /** + * Compute the time until the next change + * + * @return An Array with: {@code {String countdownMessage, Long timeUntilNextChange}} + */ + private Object[] computeLeftTime() { + long differenceOG = date.getEpochSecond() - Instant.now().getEpochSecond(); + long dayDiff = TimeUnit.DAYS.convert(differenceOG, TimeUnit.SECONDS); + long differenceHour = differenceOG - TimeUnit.SECONDS.convert(dayDiff, TimeUnit.DAYS); + long hourDiff = TimeUnit.HOURS.convert(differenceHour, TimeUnit.SECONDS); + long differenceMinutes = + differenceHour - TimeUnit.SECONDS.convert(hourDiff, TimeUnit.HOURS); + long minutesDiff = TimeUnit.MINUTES.convert(differenceMinutes, TimeUnit.SECONDS); + long differenceSeconds = + differenceMinutes - TimeUnit.SECONDS.convert(minutesDiff, TimeUnit.MINUTES); + + if (dayDiff > 7) { + if (dayDiff % 7 == 0) + return new Object[]{(dayDiff / 7) + " weeks left", + TimeUnit.MILLISECONDS.convert(differenceHour, TimeUnit.SECONDS)}; + return new Object[]{(dayDiff / 7) + " weeks and " + (dayDiff % 7) + " days left", + TimeUnit.MILLISECONDS.convert(differenceHour, TimeUnit.SECONDS)}; + } + if (dayDiff > 3 || dayDiff > 0 && hourDiff == 0) + return new Object[]{dayDiff + " days left", + TimeUnit.MILLISECONDS.convert(differenceHour, TimeUnit.SECONDS)}; + if (dayDiff > 0) + return new Object[]{dayDiff + " days and " + hourDiff + " left", + TimeUnit.MILLISECONDS.convert(differenceMinutes, TimeUnit.SECONDS)}; + if (hourDiff > 6 || hourDiff > 0 && minutesDiff == 0) + return new Object[]{hourDiff + " hours left", + TimeUnit.MILLISECONDS.convert(differenceMinutes, TimeUnit.SECONDS)}; + if (hourDiff > 0) + return new Object[]{hourDiff + " hours and " + minutesDiff + " minutes left", + TimeUnit.MILLISECONDS.convert(differenceSeconds, TimeUnit.SECONDS)}; + if (minutesDiff > 0) + return new Object[]{minutesDiff + " minutes left", + TimeUnit.MILLISECONDS.convert(differenceSeconds, TimeUnit.SECONDS)}; + return new Object[]{true}; + } + } + +} diff --git a/src/main/java/Commands/Help.java b/src/main/java/Commands/Help.java new file mode 100644 index 0000000..8319589 --- /dev/null +++ b/src/main/java/Commands/Help.java @@ -0,0 +1,149 @@ +package Commands; + +import Bot.BotEvents; +import java.awt.Color; +import java.time.Instant; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Help { + + /** + * The Logger for Log Messages + */ + private static final Logger logger = LoggerFactory.getLogger("Help Command"); + + /** + * will show the Help Embed to the User + * + * @param isInstructor If this User is an Instructor + * @param isEventOrganizer If this User is an Event Organizer + * @param isAdmin If this User is an Admin + * @param channel The Channel where the Command was send + */ + public static void showHelp(boolean isInstructor, boolean isEventOrganizer, boolean isAdmin, + MessageChannel channel) { + EmbedBuilder eb = getHelpPage(); // get the Basic Help Page + if (isInstructor) + getInstructorHelpPage(eb); // attach Instructor only commands + if (isEventOrganizer) + getEventOrganizerHelpPage(eb); // attach Event Organizer only commands + if (isAdmin) + getAdminHelpPage(eb); // attach Admin only commands + logger.info("Sending Help page"); + channel.sendMessage(eb.build()).queue(BotEvents::addTrashcan); + } + + /** + * Build the basic Help Page + * + * @return The Basic Help Page, still needs to be Build + */ + private static EmbedBuilder getHelpPage() { + EmbedBuilder eb = new EmbedBuilder(); + + eb.setColor(Color.YELLOW); + + eb.setTitle("Commands:"); + + eb.setDescription("List off all known Commands"); + + eb.addField("`!help`", "shows this page", true); + + eb.addField("`!timezones`", "Lists all Timezones with their Users", true); + + eb.addField("`!time`", """ + Will show the local time of the given Users. + The Time is calculated using the Zulu offset of the Nickname. + Users can be given as Tags or as a `,` separated List of Names. + When Names are given, only a partial match is necessary. + Example layouts: + `!time User, User1` + `!time @User @User2`""", false); + + eb.setTimestamp(Instant.now()); + + return eb; + } + + /** + * Adds the Instructor Portion to the Help Page + * + * @param eb the already pre-build Help Page + */ + private static void getInstructorHelpPage(EmbedBuilder eb) { + eb.appendDescription(" with commands for Instructors"); + + eb.addBlankField(false); + + eb.addField("Instructor Commands", "Commands that are only for Instructors", + false); + + eb.addField("`!trained`", """ + Will add the Role `Trained` the mentioned Member. + The mentioned Member must be mentioned with a Tag. + Only one Member at a time can be mentioned with the Command. + Syntax: + `!trained @User`""", false); + } + + /** + * Adds the Event Organizer Portion to the Help Page + * + * @param eb the already pre-build Help Page + */ + private static void getEventOrganizerHelpPage(EmbedBuilder eb) { + if (eb.getDescriptionBuilder().toString().contains("with commands for")) + eb.appendDescription(" and Event Organizers"); + else + eb.appendDescription(" with commands for Event Organizers"); + + eb.addBlankField(false); + + eb.addField("Event Organizer Commands", + "Commands that are only for event organizers", false); + + eb.addField("`!countdown`", """ + adds a countdown to the next event in the welcome channel + Syntax: + `!countdown DD.MM.YYYY HH:mm ` + The time is always in UTC""", false); + } + + /** + * Adds the Admin Portion to the Help page + * + * @param eb the already pre-build Help Page + */ + private static void getAdminHelpPage(EmbedBuilder eb) { + if (eb.getDescriptionBuilder().toString().contains("with commands for")) + eb.appendDescription(" and Admins"); + else + eb.appendDescription(" with commands for Admins"); + + eb.addBlankField(false); + + eb.addField("Admin Commands", "Commands that are only for admins:", + false); + + eb.addField("`!restart`", "restarts the bot", true); + + // This Command should not be shown since only the Owner can do it. + // eb.addField("!stop", "stops the bot", true); + + eb.addField("`!reload XY`", """ + reloads all config files + Following arguments are available: + `config`, `timezones`""", true); + + // This Command should not be shown since only the Owner can do it. + +// eb.addField("`!purge`", """ +// purges the given amount of Messages from the channel not including the command. +// Layout: +// `!purge 10` +// `!purge all`""", true); + } +} diff --git a/src/main/java/Commands/Ping.java b/src/main/java/Commands/Ping.java new file mode 100644 index 0000000..e316c23 --- /dev/null +++ b/src/main/java/Commands/Ping.java @@ -0,0 +1,26 @@ +package Commands; + +import net.dv8tion.jda.api.entities.MessageChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Ping { + + /** + * The Logger for Log Messages + */ + private static final Logger logger = LoggerFactory.getLogger("Ping Command"); + + /** + * Will send a "Pong!" Message and then edit this message to know the current ping of the Bot + * + * @param channel The Channel where the ping was send from + */ + public static void makePing(MessageChannel channel) { + long time = System.currentTimeMillis(); + logger.info("Sending Ping Message"); + channel.sendMessage("Pong!").queue(message -> + message.editMessageFormat("Pong: %d ms", System.currentTimeMillis() - time) + .queue()); + } +} diff --git a/src/main/java/Commands/Purge.java b/src/main/java/Commands/Purge.java new file mode 100644 index 0000000..2da77b5 --- /dev/null +++ b/src/main/java/Commands/Purge.java @@ -0,0 +1,35 @@ +package Commands; + +import Bot.BotEvents; +import net.dv8tion.jda.api.entities.MessageChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Purge { + + /** + * The Logger for Log Messages + */ + private static final Logger logger = LoggerFactory.getLogger("Purge Command"); + + /** + * Will purge the given amount of Messages + * + * @param isOwner If the User is the Owner of the Bot + * @param channel The Channel where the Message was send. + * @param content The Content of the Message that was send. + */ + public static void purgeMessages(boolean isOwner, MessageChannel channel, String content) { + if (isOwner) { // only the Owner is allowed to purge Messages + // add 1 since the command should not count + int size = 1 + Integer.parseInt(content.substring(6)); + logger.info("Purging " + size + " Messages"); + channel.getIterableHistory() // Get the whole History + .takeAsync(size) // Take a list of the last X Messages + .thenAccept(channel::purgeMessages) // purge all Messages in that list + .thenAccept(unused -> channel.sendMessage((size - 1) + " Messages Purged") + .queue(BotEvents::addTrashcan)); + } else // User isn't the Owner + channel.sendMessage("You don't have permission for this command").queue(); + } +} diff --git a/src/main/java/Commands/Reload.java b/src/main/java/Commands/Reload.java new file mode 100644 index 0000000..cfc4224 --- /dev/null +++ b/src/main/java/Commands/Reload.java @@ -0,0 +1,87 @@ +package Commands; + +import Bot.BotEvents; +import Bot.BotMain; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Reload { + + /** + * The Logger for Log Messages + */ + private static final Logger logger = LoggerFactory.getLogger("Reload Command"); + + /** + * Will reload the given Information (can be Config or Timezones) + * + * @param isAdmin the User is an Admin + * @param event Event to get more information + * @param channel The Channel where the Message was send. + * This may be removed in a further release since this can also be fetched with the event + * Instance + * @param content The Content of the Message that was send. + * This may be removed in a further release since this can also be fetched with the event + * Instance + */ + public static void reloadMain(boolean isAdmin, MessageReceivedEvent event, + MessageChannel channel, String content) { + if (isAdmin) { // only Admin is allowed to Reload the Config or Timezones + if (content.equals("reload")) { // The User did not specify what to reload + reloadNotSpecified(channel); + } else { + content = content.substring(7); + switch (content) { + case "config" -> reloadConfig(channel); // Reload Config files + case "timezones", "timezone" -> reloadTimezones(event, + channel); // Reload Timezones + default -> reloadNotSpecified(channel); + } + } + } else // User isn't an Admin + channel.sendMessage("You don't have permission for this command").queue(); + } + + /** + * Reloads the Config + * + * @param channel The Channel where the Message was send. + * + * @see BotMain#reloadConfig + */ + private static void reloadConfig(MessageChannel channel) { + logger.info("Reloading the Config"); + channel.sendMessage("reloading Config").queue(message -> { + BotMain.reloadConfig(); // reload all Config files + message.editMessage("Config reloaded successfully").queue(); + }); + } + + /** + * Reloads the Timezones of all Users + * + * @param event Event to get more information + * @param channel The Channel where the Message was send. + * This may be removed in a further release since this can also be fetched with the event + * Instance + */ + private static void reloadTimezones(MessageReceivedEvent event, MessageChannel channel) { + logger.info("Reloading the Timezones"); + channel.sendMessage("reloading all user Timezones").queue(message -> { + Timezones.updateTimezones(event.getJDA()); // reload all Timezones + message.editMessage("Timezones reloaded").queue(); + }); + } + + /** + * The User tried to reload something without specifying or something not known + * + * @param channel The Channel where the Message was send. + */ + private static void reloadNotSpecified(MessageChannel channel) { + channel.sendMessage("Please specify what to reload. Possible is: config, timezones") + .queue(message -> BotEvents.deleteMessageAfterXTime(message, 10)); + } +} diff --git a/src/main/java/Commands/Timezones.java b/src/main/java/Commands/Timezones.java new file mode 100644 index 0000000..d9e511d --- /dev/null +++ b/src/main/java/Commands/Timezones.java @@ -0,0 +1,280 @@ +package Commands; + +import Bot.BotEvents; +import Bot.BotMain; +import Bot.Config; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Timezones { + + /** + * The Logger for Log Messages + */ + private static final Logger logger = LoggerFactory.getLogger("Timezones"); + /** + * Date Format for how to output the local Time of a User + */ + private static final DateTimeFormatter sdf = DateTimeFormatter.ofPattern("EEE HH:mm") + .withLocale(Locale.ENGLISH); + /** + * Map with all the Timezones of the Users + */ + private static Map timezones = new HashMap<>(); + + /** + * Will get the local times of the Users mentioned + * + * @param event Event to get more information + * @param content The Content of the Message that was send. + * This may be removed in a further release since this can also be fetched with the event + * Instance + */ + public static void getTimezoneOfUserCommand(MessageReceivedEvent event, String content) { + if (content.length() != 4) { // check if Users are mentioned + content = content.substring(5)/*.toLowerCase(Locale.ROOT)*/; // is already LowerCase + ArrayList members = new ArrayList<>(); + StringBuilder output = new StringBuilder("```\n"); // begin building the output message + while (!content.equals("")) { // loop until no member mentioned is left + if (content.charAt(0) == '<') { // member was mentioned with a tag + members.add(Long.valueOf(content.substring(3, content.indexOf(">")))); + content = content.substring(content.indexOf(">") + 1); + } else { // Member was mentioned with (part of) his Guild name + final String name; + if (content.contains(",")) { // there are more names after this one + name = content.substring(0, content.indexOf(',')); + content = content.substring(content.indexOf(',') + 1); + } else { // there are no more names after this one + name = content; + content = ""; // empty content for exit + } + final int size = members.size(); // save how many members are in the list now + event.getGuild().getMembers().forEach(member -> { + if (size == members.size()) { // if no member was added continue + String serverName = BotEvents.getServerName(member) + .toLowerCase(Locale.ROOT); + // ignore Bots and doesn't need to be a complete match + if (!serverName.contains("[alt") && (serverName.contains(name))) + members.add(member.getIdLong()); + } + }); + if (size == members.size()) { // User was not found + String error = "Could not find User: " + name; + logger.warn(error); + output.append(error).append("\n"); + } + } + // if there is a space, skip it + if (content.length() != 0 && content.charAt(0) == ' ') + content = content.substring(1); + } + members.forEach(memberID -> output + .append(Timezones.printUserLocalTime(memberID, event.getGuild())).append("\n")); + logger.info("Sending Timezones of some Users"); + event.getChannel().sendMessage(output.append("```").toString()).queue(); + } + } + + /** + * Will get a full list of all Users with Timezones and reply with all local Times + * + * @param guild The Guild where the command was written + * @param channel The Channel where the Message was send. + */ + public static void getTimezoneOfAllUsersCommand(Guild guild, MessageChannel channel) { + Map> timezoneGroups = new HashMap<>(); // Hashmap with all different timezones + timezones.forEach((memberID, offset) -> { + ArrayList members; // Get the correct Arraylist + if (timezoneGroups.containsKey(offset)) { + members = timezoneGroups.get(offset); + } else { + members = new ArrayList<>(); + timezoneGroups.put(offset, members); + } + Member member = guild.getMemberById(memberID); // Get the Member with the given ID + if (member != null) { + members.add(BotEvents.getServerName(member)); + } + }); + + Map sortedTimezones = new TreeMap<>(); // sorted Map with all different timezones + timezoneGroups.forEach((offset, list) -> { + float offsetNumber; // true numeric Value of the offset for sorting purposes + if (offset.contains(":")) { + offsetNumber = Integer.parseInt(offset.substring(0, offset.indexOf(':'))) + + Integer.parseInt(offset.substring(offset.indexOf(':') + 1)) / 60f; + } else { + offsetNumber = Integer.parseInt(offset); + } + sortedTimezones.put(offsetNumber, offset); + }); + + StringBuilder output = new StringBuilder("```\n"); + for (Map.Entry entry : sortedTimezones.entrySet()) { + ZonedDateTime localTime = ZonedDateTime.now(ZoneOffset.of(entry.getValue())); + output.append(localTime.format(sdf)) + .append(" (Z") + .append(entry.getValue()) + .append("):\n"); // print the Timezone + timezoneGroups.get(entry.getValue()).forEach(member -> + output.append("\t") + .append(member).append("\n")); // print every user in this Timezone + output.append("\n"); + } + logger.info("Sending Timezones of all Users"); + channel.sendMessage(output.append("```").toString()).queue(); + } + + /** + * Update the Timezone of every User of the Guild + * + * @param jda The JDA Instance of the Bot + */ + static void updateTimezones(JDA jda) { // package-private to be used in Reload.java + Guild guild = jda.getGuildById( + BotMain.ROLES.get("Guild")); // get the Guild where the Bot is deployed + if (guild != null) { + Map timezonesTemp = new HashMap<>(); // make a new Hashmap that will replace the old one later on + List members = guild.getMembers(); // get all Members on the Guild + members.forEach(member -> { + User user = member.getUser(); + String nickname = BotEvents.getServerName(member); // get Nickname of User + nickname = nickname.toLowerCase(Locale.ROOT); + // ignore Alt account and when the name does not contain a timezone + if (!user.isBot() && !nickname.contains("[alt") && nickname.contains("[z")) { + try { + String offset = getTimezone(nickname.substring(nickname.indexOf("[z"))); + timezonesTemp.put(member.getIdLong(), offset); + logger.info("Updated Timezone of User: " + member.getNickname()); + } catch (NumberFormatException ignored) { // The timezone in the nickname can't be parsed as a timezone + // if the User was in the old Map then take his last value as his current + if (timezones.containsKey(member.getIdLong())) + timezonesTemp + .put(member.getIdLong(), timezones.get(member.getIdLong())); + logger.error("Could not save Timezone of User: " + member.getNickname()); + } + } + }); + timezones = timezonesTemp; // replace the old Map by the new one + } else { // The Guild ID given is not a Guild where the Bot is + logger.error("Bot is not on the specified Server"); + } + } + + /** + * Will update the Timezone of a User + * + * @param userId The ID of the User + * @param timezone The new Timezone String of the User in following format: [Z+/-0] + * + * @return {@code true} if the update was successful or {@code false} if Timezone was not + * correctly parsed + */ + public static boolean updateSpecificTimezone(long userId, String timezone) { + try { + timezones.put(userId, getTimezone(timezone)); // try to update the timezone + } catch (NumberFormatException ignored) { + return false; + } + return true; + } + + /** + * Save the Timezones in a File for use after restart + */ + public static void saveTimezones() { + Config.saveTimezones(timezones); + } + + /** + * Load the Timezones from the File + */ + public static void loadTimezones() { + timezones = Config.getTimezones(); + } + + /** + * Return the timezone offset from the String + * + * @param timezone String with the following format: [Z+/-0] + * + * @return The offset + * + * @throws NumberFormatException when the Timezone couldn't be parsed + */ + private static String getTimezone(String timezone) throws NumberFormatException { + try { + String offset = timezone.substring(timezone.indexOf('z') + 1, + timezone.indexOf(']')); // extract the offset without extras + // try to catch as many "troll" offsets as possible + if (offset.contains("--") || offset.contains("++")) { + offset = offset.substring(2); + } else if (offset.equals("+-0") || offset.equals("-+0") || + offset.equals("+/-0") || offset.equals("-/+0") || + offset.equals("±0")) { + offset = "0"; + } else if (offset.contains(":")) { + if (offset.indexOf(':') == 2) { + offset = offset.charAt(0) + "0" + offset.substring(1); + } + } + float offsetNumber; + if (offset.contains(":")) { // offset is with minutes + // this is done to confirm that the offset is a valid number + offsetNumber = Integer.parseInt(offset.substring(0, offset.indexOf(':'))) + + Integer.parseInt(offset.substring(offset.indexOf(':') + 1)) / 60f; + } else { + // this is done to confirm that the offset is a valid number + offsetNumber = Integer.parseInt(offset); + } + if (offsetNumber > 18 || offsetNumber < -18) { // offset is not in range of a timezone + throw new NumberFormatException(); + } + return offset; + } catch (IndexOutOfBoundsException ignored) { + throw new NumberFormatException(); + } + } + + /** + * Returns the String for one User with it's local Time + * + * @param memberID The Member ID of the User where we want the local time + * @param guild The Guild where the Member is + * + * @return The String that can be send + */ + private static String printUserLocalTime(long memberID, Guild guild) { + Member member = guild.getMemberById(memberID); + if (member != null) { + String name = BotEvents.getServerName(member); + if (timezones.containsKey(memberID)) { // check if we know the timezone of the user + String timezone = timezones.get(memberID); + ZonedDateTime localTime = ZonedDateTime.now(ZoneOffset.of(timezone)); + return "It is currently " + localTime.format(sdf) + " (Z" + timezone + ") for " + + name; + } else { + return name + " does not have a Timezone set."; + } + } else { + logger.error("Could not find user with ID: " + memberID); + return ""; + } + } + +} diff --git a/src/main/java/Commands/Trained.java b/src/main/java/Commands/Trained.java new file mode 100644 index 0000000..8cd2189 --- /dev/null +++ b/src/main/java/Commands/Trained.java @@ -0,0 +1,73 @@ +package Commands; + +import Bot.BotEvents; +import Bot.BotMain; +import java.util.List; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.exceptions.HierarchyException; +import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Trained { + + /** + * The Logger for Log Messages + */ + private static final Logger logger = LoggerFactory.getLogger("Trained Command"); + + /** + * Will add the Role "Trained" to a User + * + * @param isInstructor If the User sending the Command is an Instructor + * @param event Event to get more information + * @param channel The Channel where the Message was send. + * This may be removed in a further release since this can also be fetched with the event + * Instance + */ + public static void makeUserTrained(boolean isInstructor, MessageReceivedEvent event, + MessageChannel channel) { + if (isInstructor) { // only the Instructor can mark Users as trained + String errorMessage = null; // If there was an error message send to the User, this will then send a Log as well + List member = event.getMessage().getMentionedMembers(); + if (member.size() == 1) { + Role trainedRole = event.getGuild().getRoleById(BotMain.ROLES.get("Trained")); + if (trainedRole != null) { + try { + event.getGuild().addRoleToMember(member.get(0), trainedRole) + .queue(); // add the role to the User + MessageBuilder messageBuilder = new MessageBuilder(); + messageBuilder.append(member.get(0)).append(" has the Role `Trained`"); + channel.sendMessage(messageBuilder.build()) + .queue(); // confirm to the User that the role was added + logger.info( + "Added Trained Role to " + BotEvents.getServerName(member.get(0))); + } catch (HierarchyException e) { // The Trained Role is above the Bot role + errorMessage = "Could not add the Role to User because Bot has his Role under the Trained role"; + channel.sendMessage(errorMessage).queue(BotEvents::addTrashcan); + } catch (InsufficientPermissionException e) { // The Bot doesn't have permissions to add Roles + errorMessage = "Bot doesn't have the Permission Manage Roles"; + channel.sendMessage(errorMessage).queue(BotEvents::addTrashcan); + } + } else { // The Trained Role ID is wrong in the config + errorMessage = "Can't find the Trained Role. Please update the role ID's"; + channel.sendMessage(errorMessage).queue(BotEvents::addTrashcan); + } + } else if (member.size() == 0) { // No members mentioned + channel.sendMessage("You have to mention a User that is Trained").queue(); + } else { // Too many members mentioned + channel.sendMessage( + "Can't mention multiple members at once, please mention one member at a time") + .queue(message -> BotEvents.deleteMessageAfterXTime(message, 10)); + } + if (errorMessage != null) { + logger.error(errorMessage); // There was an Error so send it as well in the Log + } + } else // User isn't an Instructor + channel.sendMessage("You don't have permission for this command").queue(); + } +} diff --git a/src/main/java/Commands/package-info.java b/src/main/java/Commands/package-info.java new file mode 100644 index 0000000..ea227e8 --- /dev/null +++ b/src/main/java/Commands/package-info.java @@ -0,0 +1,6 @@ +/** + * This Package contains all Commands available. + *

+ * Each Command (or command group) is an own Class + */ +package Commands; \ No newline at end of file diff --git a/src/main/java/Countdowns.java b/src/main/java/Countdowns.java deleted file mode 100644 index 13622b6..0000000 --- a/src/main/java/Countdowns.java +++ /dev/null @@ -1,295 +0,0 @@ -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.MessageChannel; -import net.dv8tion.jda.api.entities.TextChannel; -import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.Instant; -import java.util.Stack; -import java.util.concurrent.TimeUnit; - -public class Countdowns { - - /** - * Stack of all the Countdown Threads - */ - private static final Stack countdowns = new Stack<>(); - /** - * Stack of all the Ids of the Countdown messages - */ - public static final Stack messageIds = new Stack<>(); - /** - * The Logger for Log Messages - */ - private static final Logger logger = LoggerFactory.getLogger("Countdown"); - - /** - * Start a new Countdown with the given Information in the Event - * @param event The Event with the Countdown Information - */ - public static void startNewCountdown(MessageReceivedEvent event) { - logger.debug("detected countdown command"); - String content = event.getMessage().getContentRaw(); - MessageChannel channel = event.getChannel(); - try { - channel.deleteMessageById(event.getMessageId()).queue(); - content = content.substring(11); - - // take the time information of the countdown - int day = Integer.parseInt(content.substring(0, content.indexOf('.'))); - content = content.substring(content.indexOf('.') + 1); - int month = Integer.parseInt(content.substring(0, content.indexOf('.'))); - content = content.substring(content.indexOf('.') + 1); - int year = Integer.parseInt(content.substring(0, content.indexOf(' '))); - content = content.substring(content.indexOf(' ') + 1); - int hour = Integer.parseInt(content.substring(0, content.indexOf(':'))); - content = content.substring(content.indexOf(':') + 1); - int minutes; - - // take the extra text that needs to be displayed after the countdown - if(content.length() <= 2) { - minutes = Integer.parseInt(content); - content = ""; - } else { - minutes = Integer.parseInt(content.substring(0, content.indexOf(' '))); - content = content.substring(content.indexOf(' ')); - } - - // Parse the date to a Instant Instance - Instant date = Instant.parse(year + "-" + String.format("%02d", month) + "-" + - String.format("%02d", day) + "T" + String.format("%02d", hour) + ":" + - String.format("%02d", minutes) + ":00Z"); - - // Check if the time is not in th past - if(date.getEpochSecond() < Instant.now().getEpochSecond()) { - channel.sendMessage("You tried making a countdown in the past. (Message will delete after 10 sec)") - .queue(message -> BotEvents.deleteMessageAfterXTime(message, 10)); - logger.warn("User tried making a countdown in the past"); - return; - } - logger.info("Starting countdown thread"); - // start the Thread that changes the Message to the current Countdown - CountdownsThread countdownsThread = new CountdownsThread(channel, content, date); - countdownsThread.start(); - countdowns.push(countdownsThread); - } catch (NumberFormatException | StringIndexOutOfBoundsException ignored) { // The command had some parsing error - channel.sendMessage("Something went wrong with your command try again\n" + - "Format is: `!countdown DD.MM.YYYY HH:mm `").queue(); - } - } - - /** - * Restarts the Countdowns that are in the Countdowns.cfg file - * @param jda The JDA to get the Channels and Messages - */ - public static void restartCountdowns(JDA jda) { - Config.getCountdowns().forEach(countdownInfos -> { - // Pick up the channel where the Message is - TextChannel channel = jda.getTextChannelById(countdownInfos[0]); - if(channel != null) { // check if the Channel is still there, if not ignore this Countdown - channel.retrieveMessageById(countdownInfos[1]).queue(message -> { // message still exists - Instant date = Instant.parse(countdownInfos[3]); - if(date.getEpochSecond() < Instant.now().getEpochSecond()) { // check if the Countdown is in the past - message.editMessage("Countdown finished").queue(); - BotEvents.addTrashcan(message); // add a reaction to make it easy to delete the post - } else { - logger.info("Added Countdown Thread from existing Countdown"); - CountdownsThread countdownsThread = - new CountdownsThread(channel, countdownInfos[1], countdownInfos[2], date); - countdownsThread.start(); - countdowns.push(countdownsThread); - }}, throwable -> logger.warn("Removing one Countdown where Message is deleted")); // If the message is deleted - } else { - logger.warn("Removing one Countdown where Channel is deleted"); - } - }); - } - - /** - * Save all active Countdowns in the Countdowns.cfg - */ - public static void saveAllCountdowns() { - Config.saveCountdowns(countdowns); - } - - /** - * Close a specific Countdown with its Message ID - * @param messageId The Message ID of the Countdown that needs to be closed - */ - public static void closeSpecificThread(String messageId) { - int index = messageIds.indexOf(messageId); - if(index == -1) return; - CountdownsThread thread = countdowns.get(index); - thread.interrupt(); - countdowns.remove(index); - messageIds.remove(index); - } - - /** - * Close all Thread and save them in Countdowns.cfg - */ - public static void closeAllThreads() { - saveAllCountdowns(); - countdowns.forEach(Thread::interrupt); - } - - /** - * Thread Class for the Countdowns - */ - static class CountdownsThread extends Thread { - - /** - * Lock for {@link #stop} - */ - private final Object lock = new Object(); - /** - * boolean to know if the Thread should be closed - */ - private boolean stop = false; - - /** - * The Channel where the Message of the Countdown is - */ - private final MessageChannel channel; - /** - * The Message ID of the Countdown - */ - private String messageId; - /** - * The Text that is added at the end of the Countdown - */ - private final String text; - /** - * The Date of the End of the Countdown - */ - private final Instant date; - - /** - * Constructor when the Countdown is restored after a restart - * @param channel The Channel where the Message of the Countdown is - * @param messageId The Message ID of the Countdown - * @param text The Text that is added at the end of the Countdown - * @param date The Date of the End of the Countdown - */ - private CountdownsThread(MessageChannel channel, String messageId, String text, Instant date) { - this.text = text; - this.date = date; - this.channel = channel; - this.messageId = messageId; - messageIds.push(this.messageId); - } - - /** - * Constructor for a new Countdown - * @param channel The Channel where the Message of the Countdown will be - * @param text The Text that is added at the end of the Countdown - * @param date The Date of the End of the Countdown - */ - private CountdownsThread(MessageChannel channel, String text, Instant date) { - this.text = text; - this.date = date; - this.channel = channel; - - logger.info("sending message"); - channel.sendMessage(computeLeftTime()[0] + text).queue(message -> { - synchronized (lock) { - this.messageId = message.getId(); - messageIds.push(this.messageId); - lock.notify(); // notify the Thread that the Message Id is available - } - }); - } - - /** - * Compute the String Array with the information for the Countdowns.cfg File - * @return A String Array with following layout: {@code {channelId, messageId, text, date}} - */ - public String[] getInfos() { - return new String[]{channel.getId(), messageId, text, date.toString()}; - } - - /** - * The main Function of this Thread - */ - @Override - public void run() { - synchronized (lock) { - if (messageId == null) { // If don't yet have a message ID wait for it - try { - lock.wait(); - } catch (InterruptedException ignored) { - } - } - } - while(!stop) { // don't stop until wanted - Object[] info = computeLeftTime(); - if(info[0] instanceof Boolean) { // if this countdown is at it's end - logger.info("Countdown finished removing it from the Threads List"); - channel.editMessageById(messageId, "Countdown finished") - .queue(BotEvents::addTrashcan, // add a reaction to make it easy to delete the post - throwable -> logger.warn("Removing one Countdown where Message is deleted")); // Message was deleted, don't do anything here - countdowns.remove(this); - return; - } - logger.info("editing message: " + info[0]); - channel.editMessageById(messageId, info[0] + text).queue(message -> {}, throwable -> { - // Message was deleted, delete this Thread - stop = true; - countdowns.remove(this); - logger.warn("Removing one Countdown where Message is deleted"); - }); - try{ - long sleepTime = (Long) info[1]; // sleep until the next change - //noinspection BusyWait - sleep(sleepTime < 5000?60000:sleepTime); - } catch (InterruptedException ignored) {} - } - } - - @Override - public void interrupt() { - stop = true; - super.interrupt(); - } - - /** - * Compute the time until the next change - * @return An Array with: {@code {String countdownMessage, Long timeUntilNextChange}} - */ - private Object[] computeLeftTime() { - long differenceOG = date.getEpochSecond() - Instant.now().getEpochSecond() ; - long dayDiff = TimeUnit.DAYS.convert(differenceOG, TimeUnit.SECONDS); - long differenceHour = differenceOG - TimeUnit.SECONDS.convert(dayDiff, TimeUnit.DAYS); - long hourDiff = TimeUnit.HOURS.convert(differenceHour, TimeUnit.SECONDS); - long differenceMinutes = differenceHour - TimeUnit.SECONDS.convert(hourDiff, TimeUnit.HOURS); - long minutesDiff = TimeUnit.MINUTES.convert(differenceMinutes, TimeUnit.SECONDS); - long differenceSeconds = differenceMinutes - TimeUnit.SECONDS.convert(minutesDiff, TimeUnit.MINUTES); - - if(dayDiff > 7) { - if(dayDiff % 7 == 0) - return new Object[]{(dayDiff / 7) + " weeks left", - TimeUnit.MILLISECONDS.convert(differenceHour, TimeUnit.SECONDS)}; - return new Object[]{(dayDiff / 7) + " weeks and " + (dayDiff % 7) + " days left", - TimeUnit.MILLISECONDS.convert(differenceHour, TimeUnit.SECONDS)}; - } - if(dayDiff > 3 || dayDiff > 0 && hourDiff == 0) - return new Object[]{dayDiff + " days left", - TimeUnit.MILLISECONDS.convert(differenceHour, TimeUnit.SECONDS)}; - if(dayDiff > 0) - return new Object[]{dayDiff + " days and " + hourDiff + " left", - TimeUnit.MILLISECONDS.convert(differenceMinutes, TimeUnit.SECONDS)}; - if(hourDiff > 6 || hourDiff > 0 && minutesDiff == 0) - return new Object[]{hourDiff + " hours left", - TimeUnit.MILLISECONDS.convert(differenceMinutes, TimeUnit.SECONDS)}; - if(hourDiff > 0) - return new Object[]{hourDiff + " hours and " + minutesDiff + " minutes left", - TimeUnit.MILLISECONDS.convert(differenceSeconds, TimeUnit.SECONDS)}; - if(minutesDiff > 0) - return new Object[]{minutesDiff + " minutes left", - TimeUnit.MILLISECONDS.convert(differenceSeconds, TimeUnit.SECONDS)}; - return new Object[]{true}; - } - } - -} diff --git a/src/main/java/EmbedMessages.java b/src/main/java/EmbedMessages.java deleted file mode 100644 index c6b3f29..0000000 --- a/src/main/java/EmbedMessages.java +++ /dev/null @@ -1,110 +0,0 @@ -import net.dv8tion.jda.api.EmbedBuilder; - -import java.awt.*; -import java.time.Instant; - -public class EmbedMessages { - - /** - * Build the basic Help Page - * @return The Basic Help Page, still needs to be Build - */ - public static EmbedBuilder getHelpPage() { - EmbedBuilder eb = new EmbedBuilder(); - - eb.setColor(Color.YELLOW); - - eb.setTitle("Commands:"); - - eb.setDescription("List off all known Commands"); - - eb.addField("`!help`", "shows this page", true); - - eb.addField("`!timezones`", "Lists all Timezones with their Users", true); - - eb.addField("`!time`", """ - Will show the local time of the given Users. - The Time is calculated using the Zulu offset of the Nickname. - Users can be given as Tags or as a `,` separated List of Names. - When Names are given, only a partial match is necessary. - Example layouts: - `!time User, User1` - `!time @User @User2`""", false); - - eb.setTimestamp(Instant.now()); - - return eb; - } - - /** - * Adds the Instructor Portion to the Help Page - * @param eb the already pre-build Help Page - */ - public static void getInstructor(EmbedBuilder eb) { - eb.setDescription(eb.getDescriptionBuilder().append(" with commands for Instructors")); - - eb.addBlankField(false); - - eb.addField("Instructor Commands", "Commands that are only for Instructors", false); - - eb.addField("`!trained`", """ - Will add the Role `Trained` the mentioned Member. - The mentioned Member must be mentioned with a Tag. - Only one Member at a time can be mentioned with the Command. - Syntax: - `!trained @User`""", false); - } - - /** - * Adds the Event Organizer Portion to the Help Page - * @param eb the already pre-build Help Page - */ - public static void getEventOrganizer(EmbedBuilder eb) { - StringBuilder desc = eb.getDescriptionBuilder(); - if(desc.toString().contains("with commands for")) - eb.setDescription(desc.append(" and Event Organizers")); - else - eb.setDescription(desc.append(" with commands for Event Organizers")); - - eb.addBlankField(false); - - eb.addField("Event Organizer Commands", "Commands that are only for event organizers", false); - - eb.addField("`!countdown`", """ - adds a countdown to the next event in the welcome channel - Syntax: - `!countdown DD.MM.YYYY HH:mm ` - The time is always in UTC""", false); - } - - /** - * Adds the Admin Portion to the Help page - * @param eb the already pre-build Help Page - */ - public static void getAdminHelpPage(EmbedBuilder eb) { - StringBuilder desc = eb.getDescriptionBuilder(); - if(desc.toString().contains("with commands for")) - eb.setDescription(desc.append(" and Admins")); - else - eb.setDescription(desc.append(" with commands for Admins")); - - eb.addBlankField(false); - - eb.addField("Admin Commands", "Commands that are only for admins:", false); - - eb.addField("`!restart`", "restarts the bot", true); - - //eb.addField("!stop", "stops the bot", true); - - eb.addField("`!reload XY`", """ - reloads all config files - Following arguments are available: - `config`, `timezones`""", true); - -// eb.addField("`!purge`", """ -// purges the given amount of Messages from the channel not including the command. -// Layout: -// `!purge 10` -// `!purge all`""", true); - } -} diff --git a/src/main/java/Timezones.java b/src/main/java/Timezones.java deleted file mode 100644 index 053f872..0000000 --- a/src/main/java/Timezones.java +++ /dev/null @@ -1,168 +0,0 @@ -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.User; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; - -public class Timezones { - - /** - * Map with all the Timezones of the Users - */ - private static Map timezones = new HashMap<>(); - /** - * The Logger for Log Messages - */ - private static final Logger logger = LoggerFactory.getLogger("Timezones"); - - /** - * Update the Timezone of every User of the Guild - * @param jda The JDA Instance of the Bot - */ - public static void updateTimezones(JDA jda) { - Guild guild = jda.getGuildById(BotMain.ROLES.get("Guild")); - if(guild != null) { - Map timezonesTemp = new HashMap<>(); - List members = guild.getMembers(); - members.forEach(member -> { - User user = member.getUser(); - String nickname = BotEvents.getServerName(member); - nickname = nickname.toLowerCase(Locale.ROOT); - if(!user.isBot() && !nickname.contains("[alt") && nickname.contains("[z")) { - try { - String offset = getTimezone(nickname.substring(nickname.indexOf("[z"))); - timezonesTemp.put(member.getIdLong(), offset); - logger.info("Updated Timezone of User: " + member.getNickname()); - } catch (NumberFormatException ignored) { - if(timezones.containsKey(member.getIdLong())) - timezonesTemp.put(member.getIdLong(), timezones.get(member.getIdLong())); - logger.error("Could not save Timezone of User: " + member.getNickname()); - } - } - }); - timezones = timezonesTemp; - } else { - logger.error("Bot is not on the specified Server"); - } - } - - /** - * Will update the Timezone of a User - * @param userId The ID of the User - * @param timezone The new Timezone String of the User in following format: [Z+/-0] - * @return {@code true} if the update was successful or {@code false} if Timezone was not correctly parsed - */ - public static boolean updateSpecificTimezone(long userId, String timezone) { - try { - timezones.put(userId, getTimezone(timezone)); - } catch (NumberFormatException ignored) { - timezones.remove(userId); - return false; - } - return true; - } - - public static void saveTimezones() { - Config.saveTimezones(timezones); - } - - public static void loadTimezones() { - timezones = Config.getTimezones(); - } - - /** - * Return the timezone offset from the String - * @param timezone String with the following format: [Z+/-0] - * @return The offset - * @throws NumberFormatException when the Timezone couldn't be parsed - */ - private static String getTimezone(String timezone) throws NumberFormatException { - try { - String offset = timezone.substring(timezone.indexOf('z') + 1, timezone.indexOf(']')); - if(offset.contains("--") || offset.contains("++")) - offset = offset.substring(2); - else if(offset.equals("+-0") || offset.equals("-+0") || - offset.equals("+/-0") || offset.equals("-/+0") || - offset.equals("±0")) - offset = "0"; - else if(offset.contains(":")) { - if(offset.indexOf(':') == 2) - offset = offset.charAt(0) + "0" + offset.substring(1); - } - float offsetNumber; - if(offset.contains(":")) { - offsetNumber = Integer.parseInt(offset.substring(0, offset.indexOf(':'))) + Integer.parseInt(offset.substring(offset.indexOf(':') + 1)) / 60f; - } else { - offsetNumber = Integer.parseInt(offset); - } - if(offsetNumber > 18 || offsetNumber < -18) - throw new NumberFormatException(); - return offset; // this is done to confirm that the offset is a valid number - } catch (IndexOutOfBoundsException ignored) { - throw new NumberFormatException(); - } - } - - private static final DateTimeFormatter sdf = DateTimeFormatter.ofPattern("EEE HH:mm").withLocale(Locale.ENGLISH); - - public static String printUserLocalTime(long memberID, Guild guild) { - Member member = guild.getMemberById(memberID); - if(member != null) { - String name = BotEvents.getServerName(member); - if (timezones.containsKey(memberID)) { - String timezone = timezones.get(memberID); - ZonedDateTime localTime = ZonedDateTime.now(ZoneOffset.of(timezone)); - return "It is currently " + localTime.format(sdf) + " (Z" + timezone + ") for " + name; - } else { - return name + " does not have a Timezone set."; - } - } else { - logger.error("Could not find user with ID: " + memberID); - return ""; - } - } - - public static String printAllUsers(Guild guild) { - Map> timezoneGroups = new HashMap<>(); - timezones.forEach((memberID, offset) -> { - ArrayList members; - if(timezoneGroups.containsKey(offset)) { - members = timezoneGroups.get(offset); - } else { - members = new ArrayList<>(); - timezoneGroups.put(offset, members); - } - Member member = guild.getMemberById(memberID); - if(member != null) { - members.add(BotEvents.getServerName(member)); - } - }); - - Map sortedTimezones = new TreeMap<>(); - timezoneGroups.forEach((offset, list) -> { - float offsetNumber; - if(offset.contains(":")) { - offsetNumber = Integer.parseInt(offset.substring(0, offset.indexOf(':'))) + Integer.parseInt(offset.substring(offset.indexOf(':') + 1)) / 60f; - } else { - offsetNumber = Integer.parseInt(offset); - } - sortedTimezones.put(offsetNumber, offset); - }); - - StringBuilder output = new StringBuilder("```\n"); - for(Map.Entry entry: sortedTimezones.entrySet()) { - ZonedDateTime localTime = ZonedDateTime.now(ZoneOffset.of(entry.getValue())); - output.append(localTime.format(sdf)).append(" (Z").append(entry.getValue()).append("):\n"); - timezoneGroups.get(entry.getValue()).forEach(member -> output.append("\t").append(member).append("\n")); - output.append("\n"); - } - return output.append("```").toString(); - } - -}