diff --git a/src/me/despawningbone/discordbot/EventListener.java b/src/me/despawningbone/discordbot/EventListener.java index 96b5091..657e613 100644 --- a/src/me/despawningbone/discordbot/EventListener.java +++ b/src/me/despawningbone/discordbot/EventListener.java @@ -1,416 +1,419 @@ package me.despawningbone.discordbot; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Element; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import me.despawningbone.discordbot.command.Command; import me.despawningbone.discordbot.command.CommandResult; import me.despawningbone.discordbot.command.CommandResult.CommandResultType; import me.despawningbone.discordbot.command.music.AudioTrackHandler; import me.despawningbone.discordbot.command.music.GuildMusicManager; import me.despawningbone.discordbot.command.music.Music; import me.despawningbone.discordbot.utils.MiscUtils; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.MessageBuilder; import net.dv8tion.jda.api.entities.Activity; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.GuildVoiceState; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.VoiceChannel; import net.dv8tion.jda.api.entities.Message.Attachment; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.events.guild.voice.GuildVoiceJoinEvent; import net.dv8tion.jda.api.events.guild.voice.GuildVoiceLeaveEvent; import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMoveEvent; import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; import net.dv8tion.jda.api.events.message.guild.GuildMessageUpdateEvent; import net.dv8tion.jda.api.events.user.UserActivityStartEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; public class EventListener extends ListenerAdapter { public static Multimap testAnimePicCache = ArrayListMultimap.create(); @Override public void onGuildMessageReceived(GuildMessageReceivedEvent event) { DiscordBot.mainJDA = event.getJDA(); // refresh it? //no need, everything's getJDA() is the same jda basis but audioplayer's still using it; need to migrate to store in guildmusicmanager //String[] words = event.getMessage().getContentDisplay().split(" "); User author = event.getAuthor(); TextChannel channel = event.getChannel(); Message msg = event.getMessage(); //checkAnimePics(event); //DONE use log4j //logging if (!DiscordBot.logExcemptID.contains(author.getId())) { String guildinfo = "[" + event.getGuild().getName() + " #" + channel.getName() + "]"; String payload = "[INFO] " + guildinfo + System.lineSeparator() + " " + msg + System.lineSeparator() + " Full msg: " + msg.getContentDisplay(); List att = event.getMessage().getAttachments(); if (!att.isEmpty()) { payload += System.lineSeparator() + " Attachments:"; for (int i = 0; i < att.size(); i++) { payload += System.lineSeparator() + " " + att.get(i).getUrl(); } } DiscordBot.logger.trace(payload); //TODO log embeds and add user id too? } /*if (String.join("", words).equalsIgnoreCase("!despacito")) { channel.sendMessage("https://www.youtube.com/watch?v=W3GrSMYbkBE").queue(); return; }*/ CommandResult result = null; try (Connection con = DiscordBot.db.getConnection(); Statement s = con.createStatement()){ String prefix = MiscUtils.getPrefix(s, event.getGuild().getId()); if(msg.getContentDisplay().toLowerCase().startsWith(prefix.toLowerCase())) { String msgStripped = msg.getContentDisplay().substring(prefix.length()).replaceAll("\\s\\s+", " "); //merges space String[] args = msgStripped.split(" "); // base on command length? Command cmd = DiscordBot.commands.get(args[0].toLowerCase()); cmd = cmd == null ? DiscordBot.aliases.get(args[0].toLowerCase()) : cmd; if (cmd != null) { ResultSet uRs = s.executeQuery("SELECT reports FROM users WHERE id = " + author.getId() + ";"); if(uRs.next()) { //checks null at the same time if(uRs.getString(1).split("\n").length >= 5) { channel.sendMessage("You are banned from using the bot.").queue(); DiscordBot.logger.info("[WARN] " + author.getName() + " (" + author.getId() + ") tried to execute " + msg.getContentDisplay() + " but was banned."); return; } } uRs.close(); long perm = MiscUtils.getActivePerms(s, channel, cmd); //result = cmd.execute(channel, author, msg, args); String perms = cmd.hasSubCommand() ? null : MiscUtils.getMissingPerms(perm, cmd.getRequiredBotUserLevel(), event.getMember(), channel); //pass it to the subcommand handler to handle instead + if(cmd.isDisabled()) perms = "DISABLED"; //override if disabled by code if(perms == null || event.getAuthor().getId().equals(DiscordBot.OwnerID)) { //owner overrides perms for convenience /*if(cmd.isDisabled(channel.getGuild().getId())) { channel.sendMessage("This command is disabled!").queue(); result = new CommandResult(CommandResultType.FAILURE, "Disabled command"); } else {*/ cmd.executeAsync(channel, author, msg, Arrays.copyOfRange(args, 1, args.length), r -> { //catch all exceptions? //should have actually DiscordBot.logger.info("[" + r.getResultType() + "] " + author.getName() + " (" + author.getId() + ") executed " + msg.getContentDisplay() + (r.getRemarks() == null ? "." : ". (" + r.getRemarks() + ")")); //logging has to be before sendMessage, or else if no permission it will just quit if(r.getMessage() != null) channel.sendMessage(r.getMessage()).queue(); }); //dont know if async will screw anything up //wont, TODO log date and which server executed the command also? return; //} - } else if(perms.equals("DISABLED") || cmd.isDisabled()) { + } else if(perms.equals("DISABLED")) { + author.openPrivateChannel().queue(c -> //notify user whats wrong if possible + c.sendMessage("Sorry, but the command `" + msg.getContentDisplay() + "` is disabled in the channel `#" + channel.getName() + "`.").queue()); msg.addReaction("❎").queue(); result = new CommandResult(CommandResultType.DISABLED); } else { result = new CommandResult(CommandResultType.NOPERMS, perms); channel.sendMessage(result.getMessage()).queue(); } } else { result = new CommandResult(CommandResultType.FAILURE, "Invalid command"); //do more stuff? } } else if(msg.getContentRaw().matches("<@!?" + DiscordBot.BotID + ">")) result = greet(channel, author, prefix); } catch (SQLException e) { result = new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } if(result != null) DiscordBot.logger.info("[" + result.getResultType() + "] " + author.getName() + " (" + author.getId() + ") executed " + msg.getContentDisplay() + (result.getRemarks() == null ? "." : ". (" + result.getRemarks() + ")")); } @SuppressWarnings("unused") private void checkAnimePics(GuildMessageReceivedEvent event) { //TESTING CompletableFuture.runAsync(() -> { User author = event.getAuthor(); TextChannel channel = event.getChannel(); Message msg = event.getMessage(); List pics = new ArrayList<>(); if(channel.getId().equals("615396635383562272") && !DiscordBot.BotID.equals(author.getId())) { for(Attachment att : msg.getAttachments()) { if(att.isImage()) pics.add(att.getUrl()); } /*if(msg.getContentDisplay().contains("http")) { for(String s : msg.getContentDisplay().split("http")) { try { new URL("http" + s); pics.add("http" + s); } catch(MalformedURLException e) { ; } } }*/ try { Thread.sleep(1500); msg = channel.retrieveMessageById(msg.getId()).complete(); } catch (InterruptedException e2) { e2.printStackTrace(); } for(MessageEmbed em : msg.getEmbeds()) { //System.out.println(em.toJSONObject()); if(em.getThumbnail() != null) { System.out.println("thumb"); if(em.getSiteProvider() != null && em.getSiteProvider().getName().equals("pixiv")) { //pixiv doesnt show whole pic in embed pics.add(em.getUrl()); } else { pics.add(em.getThumbnail().getUrl()); } } else if(em.getImage() != null && !author.isBot()) { System.out.println("img"); pics.add(em.getImage().getUrl()); } else { System.out.println("url"); pics.add(em.getUrl()); } } //System.out.println(pics); //System.out.println(testAnimePicCache); for(String pic : pics) { //handle first since these mustnt be the same urls, therefore cache saucenao sauce instead String origPic = pic; if(!testAnimePicCache.containsValue(pic)) { Element saucenao; try { saucenao = Jsoup.connect("https://saucenao.com/search.php?url=" + pic).get().body(); Element result = saucenao.selectFirst(".resulttable"); try { //normal pixiv/deviantart handling if(result.parent().attr("class").equals("result hidden")) throw new NullPointerException(); Element source = result.selectFirst("strong:contains(ID:)").nextElementSibling(); pic = source.attr("href"); //will not run if the cache has the value || if the link specified aint a pic } catch (NullPointerException e1) { //weird saucenao card formatting (eg episode info), or no result; we dont handle these ; } } catch (IOException e) { e.printStackTrace(); } } System.out.println("next" + testAnimePicCache); if(testAnimePicCache.containsValue(pic)) { try { Multimap temp = ArrayListMultimap.create(); System.out.println("temp"); Message m = channel.retrieveMessageById(Multimaps.invertFrom(testAnimePicCache, temp).get(pic).toArray(new String[1])[0]).complete(); EmbedBuilder eb = new EmbedBuilder(); eb.setTitle("Repost detected!"); eb.setDescription("[This image](" + origPic + ")" + (origPic.equals(pic) ? "" : "([source](" + pic + "))") + " is a repost of [this message](" + m.getJumpUrl() + ") by `" + m.getAuthor().getName() + "#" + m.getAuthor().getDiscriminator() + "` at **" + m.getTimeCreated().format(DateTimeFormatter.RFC_1123_DATE_TIME) + "**.\n"); eb.setThumbnail(origPic); channel.sendMessage(eb.build()).queue(); continue; } catch(NullPointerException e) { //if the message is deleted e.printStackTrace(); } } else { System.out.println("put"); testAnimePicCache.put(msg.getId(), pic); } } } }).whenComplete((r, t) -> {if(t != null) t.printStackTrace();}); } private Properties greetEasterEggs = new Properties(); { try(FileInputStream in = new FileInputStream(new File(System.getProperty("user.dir") + File.separator + "eastereggs.properties"))){ greetEasterEggs.load(in); } catch (IOException e) { e.printStackTrace(); } } private CommandResult greet(TextChannel channel, User author, String prefix) { MessageBuilder smsg = new MessageBuilder(); String nick = channel.getGuild().getMemberById(author.getId()).getNickname(); if (nick != null) { smsg.append("Yo " + nick + "!\n"); } else { smsg.append("Yo " + author.getName() + "!\n"); } String easterEgg = greetEasterEggs.getProperty(author.getId()); if(easterEgg != null) { smsg.append(easterEgg.replace("\\n", "\n") + "\n"); } EmbedBuilder eb = new EmbedBuilder(); eb.setColor(0x051153); //TODO version info based on git commits eb.appendDescription("This bot is running **despbot v1.5.0**, Shard `" + DiscordBot.mainJDA.getShardInfo().getShardString() + "`. ([invite me!](https://discordapp.com/oauth2/authorize?&client_id=" + DiscordBot.BotID + "&scope=bot&permissions=0))\n"); eb.appendDescription("Connected guilds: `" + DiscordBot.mainJDA.getGuildCache().size() + (DiscordBot.mainJDA.getShardManager() == null ? "" : "/" + DiscordBot.mainJDA.getShardManager().getShards().stream().mapToLong(jda -> jda.getGuildCache().size()).sum()) + "`; "); eb.appendDescription("Total members (cached): `" + DiscordBot.mainJDA.getUserCache().size() + (DiscordBot.mainJDA.getShardManager() == null ? "" : "/" + DiscordBot.mainJDA.getShardManager().getShards().stream().mapToLong(jda -> jda.getUserCache().size()).sum()) + "`\n"); eb.appendDescription("DM `" + DiscordBot.mainJDA.getUserById(DiscordBot.OwnerID).getAsTag() + "` if you have any questions!\n"); eb.appendDescription("To get a list of commands, do `" + prefix + "help`."); smsg.setEmbed(eb.build()); Message fmsg = smsg.build(); channel.sendMessage(fmsg).queue(); return new CommandResult(CommandResultType.SUCCESS, null); } @Override public void onGuildMessageUpdate(GuildMessageUpdateEvent event) { //System.out.println("edit"); //debug Message msg = event.getMessage(); User author = event.getAuthor(); TextChannel channel = event.getChannel(); if (!DiscordBot.logExcemptID.contains(author.getId())) { String guildinfo = "[" + event.getGuild().getName() + " #" + channel.getName() + "]"; DiscordBot.logger.trace("[EDIT] " + guildinfo + System.lineSeparator() + " " + msg + System.lineSeparator() + " Full edited msg: " + msg.getContentDisplay()); } } @Override public void onUserActivityStart(UserActivityStartEvent event) { //store presence for checking osu pp Activity osu = event.getNewActivity(); if (osu != null && osu.getName().equals("osu!") && osu.isRich() //if need to include other games and details, just remove this if clause and change sGame below && osu.asRichPresence().getDetails() != null) { String toolTip = osu.asRichPresence().getLargeImage().getText(); //DiscordBot.guildMemberPresence.put(event.getUser().getId(), game); //so that game update to nothing wont be logged /*System.out.println(event.getGuild().getName()); System.out.println(event.getUser().getName()); System.out.println(game.getName()); System.out.println(game.getUrl()); System.out.println(game.getType()); System.out.println(game.isRich() ? game.asRichPresence().getDetails() : "no rich"); System.out.println(game.isRich() ? game.asRichPresence().getState() : "no rich");*/ //OffsetDateTime timesent = OffsetDateTime.now(); //System.out.println(toolTip); String sGame = (toolTip.lastIndexOf(" (") != -1 ? toolTip.substring(0, toolTip.lastIndexOf(" (")) : toolTip) + "||" + osu.asRichPresence().getDetails(); //sGame = sGame.replaceAll("'", "''"); /*OffsetDateTime timeReceived = OffsetDateTime.now(); long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS)); System.out.println("Time taken: " + ms + "ms");*/ try (Connection con = DiscordBot.db.getConnection()) { //DONE do i need to close the statement? PreparedStatement s = con.prepareStatement("INSERT INTO users(id, game) VALUES (" + event.getUser().getId() + ", ?) ON CONFLICT(id) DO UPDATE SET game = ? WHERE game <> ?;" ); //prevent blank updates s.setString(1, sGame); s.setString(2, sGame); s.setString(3, sGame); s.execute(); //con.createStatement().execute("INSERT INTO users(id, game) VALUES (" + event.getUser().getId() + ", '" + sGame + "') ON CONFLICT(id) DO UPDATE SET game = '" + sGame + "' WHERE game <> '" + sGame + "';" ); //prevent blank updates } catch (SQLException e) { //FIXED if i make this not osu only, be aware of SQL injections through sGame (probably being paranoid tho) e.printStackTrace(); } /*timeReceived = OffsetDateTime.now(); ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS)); System.out.println("Time taken: " + ms + "ms");*/ } } //TODO only update when not paused? //TODO update on member deafen? private void waitActivity(Guild guild) { AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler(); ap.getGuildMusicManager(guild).player.setPaused(true); ap.getGuildMusicManager(guild).clearQueueCleanup = ap.ex.schedule(() -> { ap.stopAndClearQueue(guild); DiscordBot.lastMusicCmd.get(guild.getId()).sendMessage("The queue has been cleared.").queue(); ap.getGuildMusicManager(guild).clearQueueCleanup = null; }, 1, TimeUnit.MINUTES); } private String updateActivity(Guild guild, VoiceChannel vc) { AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler(); GuildMusicManager mm = ap.getGuildMusicManager(guild); if(mm.clearQueueCleanup != null) { mm.player.setPaused(false); mm.clearQueueCleanup.cancel(true); mm.clearQueueCleanup = null; } String type = mm.scheduler.loop; //mm cannot be null if (type != null && vc.getMembers().size() > 2 && vc.getMembers().contains(guild.getMemberById(DiscordBot.OwnerID))) { ap.toggleLoopQueue(guild, type); return type; } return null; } @Override public void onGuildVoiceLeave(GuildVoiceLeaveEvent event) { // theoretically i dont need to check if lastMusicCmd has the entry or not, as it must have one to trigger this GuildVoiceState vs = event.getGuild().getMemberById(DiscordBot.BotID).getVoiceState(); if(!event.getMember().getUser().getId().equals(DiscordBot.BotID)) { if (vs.inVoiceChannel()) { if (vs.getChannel().equals(event.getChannelLeft()) && event.getChannelLeft().getMembers().size() < 2) { TextChannel channel = DiscordBot.lastMusicCmd.get(event.getGuild().getId()); channel.sendMessage( "All users have left the music channel, the player is now paused.\nThe queue will be cleared in 1 minute if there is no activity.") .queue(); waitActivity(event.getGuild()); } } } else { //got kicked AudioTrackHandler ap = ((Music) DiscordBot.commands.get("music")).getAudioTrackHandler(); if(ap != null) { //if not destroyed ap.stopAndClearQueue(event.getGuild()); //can double fire on normal end; shouldnt be too big of a problem } } } @Override public void onGuildVoiceMove(GuildVoiceMoveEvent event) { // theoretically i dont need to check if lastMusicCmd has the entry or not, as it must have one to trigger this GuildVoiceState vs = event.getGuild().getMemberById(DiscordBot.BotID).getVoiceState(); if (vs.inVoiceChannel()) { String id = event.getGuild().getId(); TextChannel channel = DiscordBot.lastMusicCmd.get(id); if (!event.getMember().getUser().getId().equals(DiscordBot.BotID)) { if (vs.getChannel().equals(event.getChannelLeft())) { if (!event.getMember().getUser().getId().equals(DiscordBot.BotID) && event.getChannelLeft().getMembers().size() < 2) { channel.sendMessage( "All users have left the music channel, the player is now paused.\nThe queue will be cleared in 1 minute if there is no activity.") .queue(); waitActivity(event.getGuild()); } } else if (vs.getChannel().equals(event.getChannelJoined())) { String type = updateActivity(event.getGuild(), event.getChannelJoined()); if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue(); } } else { //moved bot to empty channel if(event.getChannelJoined().getMembers().size() < 2) { channel.sendMessage( "The bot has been moved to an empty channel, the player is now paused.\nThe queue will be cleared in 1 minute if there is no activity.") .queue(); waitActivity(event.getGuild()); } else { //moved bot to channel with ppl event.getGuild().getAudioManager().openAudioConnection(event.getChannelJoined()); //seems to need explicit reconnect on bot move, it gets stuck on attempting to reconnect otherwise; probably would be fixed soon but hot patch for now String type = updateActivity(event.getGuild(), event.getChannelJoined()); if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue(); } } } } @Override public void onGuildVoiceJoin(GuildVoiceJoinEvent event) { GuildVoiceState vs = event.getGuild().getMemberById(DiscordBot.BotID).getVoiceState(); String id = event.getGuild().getId(); TextChannel channel = DiscordBot.lastMusicCmd.get(id); if(channel != null) { if (vs.inVoiceChannel() && !event.getMember().getUser().getId().equals(DiscordBot.BotID)) { if (vs.getChannel().equals(event.getChannelJoined())) { String type = updateActivity(event.getGuild(), event.getChannelJoined()); if(type != null) channel.sendMessage((type.equals("loop") ? "Looping" : "Autoplay") + " mode disabled due to new users joining the music channel.").queue(); } } } } } diff --git a/src/me/despawningbone/discordbot/command/Command.java b/src/me/despawningbone/discordbot/command/Command.java index e2ba553..ec581fd 100644 --- a/src/me/despawningbone/discordbot/command/Command.java +++ b/src/me/despawningbone/discordbot/command/Command.java @@ -1,208 +1,212 @@ package me.despawningbone.discordbot.command; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; //import java.time.OffsetDateTime; //import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import org.apache.commons.lang3.exception.ExceptionUtils; import me.despawningbone.discordbot.DiscordBot; import me.despawningbone.discordbot.command.CommandResult.CommandResultType; import me.despawningbone.discordbot.utils.MiscUtils; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; public class Command { //no need to be abstract anymore //make it final to ensure thread safety? protected String name; protected String desc; protected String usage; protected List alias; //fixed-size in this case is negligible, as it will not be edited later on anyways protected List remarks = null; protected boolean isDisabled; protected EnumSet perms = EnumSet.noneOf(Permission.class); //so that not configured commands would have a default of no perms needed, instead of null //WILL NOT ALLOW DENY OVERRIDE FOR NOW protected int botUserLevel; //0 = everyone, 1 = BotMod, 2 = BotOwner; negative value = either BotUser or required perm protected List examples; protected ExecuteImpl impl = null; private Command parent = null; private String cat; //not used; remove? //actually gets used now private LinkedHashMap subCmds = new LinkedHashMap<>(); //preserve order public HashMap subCmdAliases = new HashMap(); //TODO temporary solution //DONE add examples; what about subcmds? //for overriding protected Command() {} //only used by nested commands, since all explicitly declared commands override execute(); hot creation of subcommands are possible, but permission setting via !desp settings is not supported public Command(String name, List aliases, Command.ExecuteImpl exeFunc, String usage, List examples, String desc, List remarks, EnumSet defaultPerms, int botUserLevel) { this.name = name; this.alias = aliases; this.impl = exeFunc; this.usage = usage; this.examples = examples; this.desc = desc; this.remarks = remarks; this.perms = defaultPerms; this.botUserLevel = botUserLevel; } public interface ExecuteImpl { //DONE make this a seperate class to store the ever increasing params? if so, protected constructor since it doesnt need to be instantiated elsewhere other than registerSubCommand here //implemented nested commands instead public CommandResult execute(TextChannel channel, User author, Message msg, String[] args); } private void setParent(Command parent) { //private so only register function can access to avoid accidental use this.parent = parent; } //for anonymous inner classes if needed public void registerSubCommand(Command subCmd) { subCmd.setParent(this); //make parent only set on register since subcmds with wrong/no parents can result in finnicky bugs subCmds.put(subCmd.getName(), subCmd); if(subCmd.getAliases() != null) subCmd.getAliases().forEach(a -> subCmdAliases.put(a, subCmd)); } public void registerSubCommand(String name, List aliases, Command.ExecuteImpl exe, String usage, List examples, String desc, List remarks) { //subcmd has its own examples? //DONE subcmd permissions and botuserlevel //probably not needed, as its gonna be binded to guild instead of cmds soon enough registerSubCommand(name, aliases, exe, usage, examples, desc, remarks, EnumSet.noneOf(Permission.class), 0); } public void registerSubCommand(String name, List aliases, Command.ExecuteImpl exe, String usage, List examples, String desc, List remarks, EnumSet defaultPerms, int botUserLevel) { Command subCmd = new Command(name, aliases, exe, usage, examples, desc, remarks, defaultPerms, botUserLevel); registerSubCommand(subCmd); } public boolean hasSubCommand() { return !subCmds.isEmpty(); } public Set getSubCommandNames() { return subCmds.keySet(); } public Command getSubCommand(String name) { Command subCmd = subCmds.get(name); return subCmd == null ? subCmdAliases.get(name) : subCmd; } public Command getParent() { return parent; } public enum BotUserLevel { //DONE? implement this DONE test this DEFAULT, BOT_MOD, BOT_OWNER } public String getCategory() { //is null if it is a subcommand return cat; } public String getName() { return name; } public String getDesc() { return desc; } public String getUsage() { return usage; } public List getAliases() { return alias; } public List getRemarks() { return remarks; } public EnumSet getDefaultPerms() { //DONE make this channel dependent? return perms; } public int getRequiredBotUserLevel() { return botUserLevel; } public List getExamples() { return examples; } public boolean isDisabled() { //FIXED think of what to do with disabledGuild; it makes the command class not thread safe; store in DB instead, and be channel based? return isDisabled; } //even when multiple thread references to one specific command, it should be thread safe because it doesnt modify anything in the execute stage; and even the hashmap is somewhat immutable as it will never be changed after finished loading //i dont think i need to even volatile that hashmap //the only thing i might need to do to make it thread safe is to make the hashmap a concurrenthashmap - public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) { //if you want a main command to work with sub commands, just super() this and then write the main command stuff + public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) { //if you want a main command to work with sub commands, just define impl along with subcmds instead of overriding this method OffsetDateTime timesent = OffsetDateTime.now(); try(Connection con = DiscordBot.db.getConnection(); Statement s = con.createStatement()) { //not the best, but i cant share connection through threads String prefix = MiscUtils.getPrefix(s, channel.getGuild().getId()); if(hasSubCommand()) { if(args.length > 0) { Command subCmd = getSubCommand(args[0].toLowerCase()); if(subCmd != null) { int botUserLevel = subCmd.getRequiredBotUserLevel(); Command temp = subCmd; while(temp.getParent() != null && botUserLevel == 0) { temp = temp.getParent(); botUserLevel = temp.getRequiredBotUserLevel(); //if not set get parent's } - + String perms = subCmd.hasSubCommand() ? null : MiscUtils.getMissingPerms(MiscUtils.getActivePerms(s, channel, subCmd), botUserLevel, channel.getGuild().getMember(author), channel); //yet again pass to handler - if(perms == null) { + if(subCmd.isDisabled()) perms = "DISABLED"; //override if disabled by code + + if(perms == null || author.getId().equals(DiscordBot.OwnerID)) { //owner overrides perms for convenience OffsetDateTime timeReceived = OffsetDateTime.now(); long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS)); System.out.println("subcmd parse Time taken: " + ms + "ms"); return subCmd.execute(channel, author, msg, Arrays.copyOfRange(args, 1, args.length)); - } else if(perms.equals("DISABLED") || this.isDisabled) { + } else if(perms.equals("DISABLED")) { + author.openPrivateChannel().queue(c -> //notify user whats wrong if possible + c.sendMessage("Sorry, but the command `" + msg.getContentDisplay() + "` is disabled in the channel `#" + channel.getName() + "`.").queue()); msg.addReaction("❎").queue(); return new CommandResult(CommandResultType.DISABLED); } else { return new CommandResult(CommandResultType.NOPERMS, perms); } - + } else { if(impl == null) return new CommandResult(CommandResultType.INVALIDARGS, "Unknown sub command! Check `" + prefix + "help " + this.getName() + "` for more info."); } } else { //only run if no execute definition, if not let it override default messages if(impl == null) return new CommandResult(CommandResultType.INVALIDARGS, "Please specify a subcommand. You can view them with `" + prefix + "help " + this.name + "`."); } } return impl.execute(channel, author, msg, args); //fallback if no subcommand found; useful for defining custom messages or default command for a set of subcommands } catch (SQLException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } } public void executeAsync(TextChannel channel, User author, Message msg, String[] args, Consumer success) { OffsetDateTime timesent = OffsetDateTime.now(); CompletableFuture.supplyAsync(() -> execute(channel, author, msg, args)) .exceptionally(ex -> new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex.getCause()))) //getCause should always work as ex is a CompletionException .thenAccept(success).thenAccept(r -> { //DONE thenAccept parses the CommandResult OffsetDateTime timeReceived = OffsetDateTime.now(); long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS)); System.out.println("Time taken: " + ms + "ms"); }); } }