diff --git a/src/main/java/buttondevteam/discordplugin/DPUtils.java b/src/main/java/buttondevteam/discordplugin/DPUtils.java index abc1f8f..25adff8 100755 --- a/src/main/java/buttondevteam/discordplugin/DPUtils.java +++ b/src/main/java/buttondevteam/discordplugin/DPUtils.java @@ -1,6 +1,5 @@ package buttondevteam.discordplugin; -import buttondevteam.lib.TBMCCoreAPI; import org.bukkit.Bukkit; import sx.blah.discord.util.EmbedBuilder; import sx.blah.discord.util.RequestBuffer; @@ -9,6 +8,7 @@ import sx.blah.discord.util.RequestBuffer.IVoidRequest; import javax.annotation.Nullable; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; public final class DPUtils { @@ -39,18 +39,13 @@ public final class DPUtils { * Performs Discord actions, retrying when ratelimited. May return null if action fails too many times or in safe mode. */ @Nullable - public static T perform(IRequest action, long timeout, TimeUnit unit) { + public static T perform(IRequest action, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { if (DiscordPlugin.SafeMode) return null; if (Thread.currentThread() == DiscordPlugin.mainThread) // TODO: Ignore shutdown message <-- // throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag."); Bukkit.getLogger().warning("Waiting for a Discord request on the main thread!"); - try { - return RequestBuffer.request(action).get(timeout, unit); // Let the pros handle this - } catch (Exception e) { - TBMCCoreAPI.SendException("Couldn't perform Discord action!", e); - return null; - } + return RequestBuffer.request(action).get(timeout, unit); // Let the pros handle this } /** diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java index 170c789..c5e6ded 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java @@ -15,6 +15,7 @@ import com.google.gson.JsonParser; import lombok.val; import net.milkbowl.vault.permission.Permission; import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.java.JavaPlugin; @@ -35,6 +36,7 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; public class DiscordPlugin extends JavaPlugin implements IListener { @@ -210,16 +212,8 @@ public class DiscordPlugin extends JavaPlugin implements IListener { TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this); TBMCChatAPI.AddCommands(this, DiscordMCCommandBase.class); TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class); - new Thread(this::AnnouncementGetterThreadMethod).start(); + new Thread(this::AnnouncementGetterThreadMethod).start(); //TODO: Handle relogging (test) setupProviders(); - /* - * IDiscordOAuth doa = new DiscordOAuthBuilder(dc).withClientID("226443037893591041") .withClientSecret(getConfig().getString("appsecret")) .withRedirectUrl("https://" + - * (TBMCCoreAPI.IsTestServer() ? "localhost" : "server.figytuna.com") + ":8081/callback") .withScopes(Scope.IDENTIFY).withHttpServerOptions(new HttpServerOptions().setPort(8081)) - * .withSuccessHandler((rc, user) -> { rc.response().headers().add("Location", "https://" + (TBMCCoreAPI.IsTestServer() ? "localhost" : "server.figytuna.com") + ":8080/login?type=discord&" - * + rc.request().query()); rc.response().setStatusCode(303); rc.response().end("Redirecting"); rc.response().close(); }).withFailureHandler(rc -> { rc.response().headers().add("Location", - * "https://" + (TBMCCoreAPI.IsTestServer() ? "localhost" : "server.figytuna.com") + ":8080/login?type=discord&" + rc.request().query()); rc.response().setStatusCode(303); - * rc.response().end("Redirecting"); rc.response().close(); }).build(); getLogger().info("Auth URL: " + doa.buildAuthUrl()); - */ } catch (Exception e) { TBMCCoreAPI.SendException("An error occured while enabling DiscordPlugin!", e); } @@ -260,22 +254,28 @@ public class DiscordPlugin extends JavaPlugin implements IListener { } saveConfig(); - MCChatListener.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannelWait(ch, "", - new EmbedBuilder().withColor(Restart ? Color.ORANGE : Color.RED) - .withTitle(Restart ? "Server restarting" : "Server stopping") - .withDescription( - Bukkit.getOnlinePlayers().size() > 0 - ? (DPUtils - .sanitizeString(Bukkit.getOnlinePlayers().stream() - .map(p -> p.getDisplayName()).collect(Collectors.joining(", "))) - + (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ") - + "asked *politely* to leave the server for a bit.") - : "") - .build(), 5, TimeUnit.SECONDS)); + MCChatListener.forAllMCChat(ch -> { + try { + DiscordPlugin.sendMessageToChannelWait(ch, "", + new EmbedBuilder().withColor(Restart ? Color.ORANGE : Color.RED) + .withTitle(Restart ? "Server restarting" : "Server stopping") + .withDescription( + Bukkit.getOnlinePlayers().size() > 0 + ? (DPUtils + .sanitizeString(Bukkit.getOnlinePlayers().stream() + .map(Player::getDisplayName).collect(Collectors.joining(", "))) + + (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ") + + "asked *politely* to leave the server for a bit.") + : "") + .build(), 5, TimeUnit.SECONDS); + } catch (TimeoutException | InterruptedException e) { + e.printStackTrace(); + } + }); ChromaBot.getInstance().updatePlayerList(); try { SafeMode = true; // Stop interacting with Discord - MCChatListener.stop(); + MCChatListener.stop(true); ChromaBot.delete(); dc.changePresence(StatusType.IDLE, ActivityType.PLAYING, "Chromacraft"); //No longer using the same account for testing dc.logout(); @@ -359,26 +359,30 @@ public class DiscordPlugin extends JavaPlugin implements IListener { } public static void sendMessageToChannel(IChannel channel, String message, EmbedObject embed) { - sendMessageToChannel(channel, message, embed, false); + try { + sendMessageToChannel(channel, message, embed, false); + } catch (TimeoutException | InterruptedException e) { + e.printStackTrace(); //Shouldn't happen, as we're not waiting on the result + } } - public static IMessage sendMessageToChannelWait(IChannel channel, String message) { + public static IMessage sendMessageToChannelWait(IChannel channel, String message) throws TimeoutException, InterruptedException { return sendMessageToChannelWait(channel, message, null); } - public static IMessage sendMessageToChannelWait(IChannel channel, String message, EmbedObject embed) { + public static IMessage sendMessageToChannelWait(IChannel channel, String message, EmbedObject embed) throws TimeoutException, InterruptedException { return sendMessageToChannel(channel, message, embed, true); } - public static IMessage sendMessageToChannelWait(IChannel channel, String message, EmbedObject embed, long timeout, TimeUnit unit) { + public static IMessage sendMessageToChannelWait(IChannel channel, String message, EmbedObject embed, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { return sendMessageToChannel(channel, message, embed, true, timeout, unit); } - private static IMessage sendMessageToChannel(IChannel channel, String message, EmbedObject embed, boolean wait) { + private static IMessage sendMessageToChannel(IChannel channel, String message, EmbedObject embed, boolean wait) throws TimeoutException, InterruptedException { return sendMessageToChannel(channel, message, embed, wait, -1, null); } - private static IMessage sendMessageToChannel(IChannel channel, String message, EmbedObject embed, boolean wait, long timeout, TimeUnit unit) { + private static IMessage sendMessageToChannel(IChannel channel, String message, EmbedObject embed, boolean wait, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { if (message.length() > 1980) { message = message.substring(0, 1980); Bukkit.getLogger() @@ -404,6 +408,8 @@ public class DiscordPlugin extends JavaPlugin implements IListener { DPUtils.performNoWait(r); return null; } + } catch (TimeoutException | InterruptedException e) { + throw e; } catch (Exception e) { Bukkit.getLogger().warning( "Failed to deliver message to Discord! Channel: " + channel.getName() + " Message: " + message); diff --git a/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java b/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java index 670b522..a339c4d 100755 --- a/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java +++ b/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java @@ -27,7 +27,7 @@ public class ConnectCommand extends DiscordCommandBase { @Override public boolean run(IMessage message, String args) { if (args.length() == 0) - return true; + return false; if (args.contains(" ")) { DiscordPlugin.sendMessageToChannel(message.getChannel(), "Too many arguments.\nUsage: connect "); diff --git a/src/main/java/buttondevteam/discordplugin/listeners/MCChatListener.java b/src/main/java/buttondevteam/discordplugin/listeners/MCChatListener.java index b5ff177..12d706b 100755 --- a/src/main/java/buttondevteam/discordplugin/listeners/MCChatListener.java +++ b/src/main/java/buttondevteam/discordplugin/listeners/MCChatListener.java @@ -12,6 +12,7 @@ import buttondevteam.lib.chat.ChatRoom; import buttondevteam.lib.chat.TBMCChatAPI; import buttondevteam.lib.player.TBMCPlayer; import com.vdurmont.emoji.EmojiParser; +import io.netty.util.collection.LongObjectHashMap; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.val; @@ -38,6 +39,7 @@ import java.time.Instant; import java.util.*; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -53,7 +55,7 @@ public class MCChatListener implements Listener, IListener @EventHandler // Minecraft public void onMCChat(TBMCChatEvent ev) { - if (ev.isCancelled()) + if (DiscordPlugin.SafeMode || ev.isCancelled()) //SafeMode: Needed so it doesn't restart after server shutdown return; sendevents.add(new AbstractMap.SimpleEntry<>(ev, Instant.now())); if (sendtask != null) @@ -71,15 +73,10 @@ public class MCChatListener implements Listener, IListener try { TBMCChatEvent e; Instant time; - try { - val se = sendevents.take(); // Wait until an element is available - e = se.getKey(); - time = se.getValue(); - } catch (InterruptedException ex) { - sendtask.cancel(); - sendtask = null; - return; - } + val se = sendevents.take(); // Wait until an element is available + e = se.getKey(); + time = se.getValue(); + final String authorPlayer = "[" + DPUtils.sanitizeString(e.getChannel().DisplayName) + "] " // + (e.getSender() instanceof DiscordSenderBase ? "[D]" : "") // + (DPUtils.sanitizeString(e.getSender() instanceof Player // @@ -102,7 +99,7 @@ public class MCChatListener implements Listener, IListener // embed.withFooterText(e.getChannel().DisplayName); embed.withTimestamp(time); final long nanoTime = System.nanoTime(); - Consumer doit = lastmsgdata -> { + InterruptibleConsumer doit = lastmsgdata -> { final EmbedObject embedObject = embed.build(); if (lastmsgdata.message == null || lastmsgdata.message.isDeleted() || !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().getName()) @@ -144,14 +141,19 @@ public class MCChatListener implements Listener, IListener while (iterator.hasNext()) { //TODO: Add cmd to fix mcchat val lmd = iterator.next(); if ((e.isFromcmd() || isdifferentchannel.test(lmd.channel)) //Test if msg is from Discord - && e.getChannel().ID.equals(lmd.mcchannel.ID)) //If it's from a command, the command msg has been deleted, so we need to send it - if (e.shouldSendTo(lmd.dcp) && e.getGroupID().equals(lmd.groupID)) //Check original user's permissions + && e.getChannel().ID.equals(lmd.mcchannel.ID) //If it's from a command, the command msg has been deleted, so we need to send it + && e.getGroupID().equals(lmd.groupID)) { //Check if this is the group we want to test - #58 + if (e.shouldSendTo(lmd.dcp)) //Check original user's permissions doit.accept(lmd); else { iterator.remove(); //If the user no longer has permission, remove the connection DiscordPlugin.sendMessageToChannel(lmd.channel, "The user no longer has permission to view the channel, connection removed."); } + } } + } catch (InterruptedException ex) { //Stop if interrupted anywhere + sendtask.cancel(); + sendtask = null; } catch (Exception ex) { TBMCCoreAPI.SendException("Error while sending message to Discord!", ex); } @@ -216,6 +218,7 @@ public class MCChatListener implements Listener, IListener * Used for town or nation chats or anything else */ private static ArrayList lastmsgCustom = new ArrayList<>(); + private static LongObjectHashMap lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks public static boolean privateMCChat(IChannel channel, boolean start, IUser user, DiscordPlayer dp) { TBMCPlayer mcp = dp.getAs(TBMCPlayer.class); @@ -233,6 +236,8 @@ public class MCChatListener implements Listener, IListener MCListener.callEventExcludingSome(new PlayerQuitEvent(sender, "")); } } + if (!start) + lastmsgfromd.remove(channel.getLongID()); return start // ? lastmsgPerUser.add(new LastMsgData(channel, user, dp)) // Doesn't support group DMs : lastmsgPerUser.removeIf(lmd -> lmd.channel.getLongID() == channel.getLongID()); @@ -273,6 +278,7 @@ public class MCChatListener implements Listener, IListener } public static boolean removeCustomChat(IChannel channel) { + lastmsgfromd.remove(channel.getLongID()); return lastmsgCustom.removeIf(lmd -> lmd.channel.getLongID() == channel.getLongID()); } @@ -337,14 +343,32 @@ public class MCChatListener implements Listener, IListener .map(data -> data.channel).forEach(action); } - public static void stop() { + /** + * Stop the listener. Any calls to onMCChat will restart it as long as we're not in safe mode. + * + * @param wait Wait 5 seconds for the threads to stop + */ + public static void stop(boolean wait) { if (sendthread != null) sendthread.interrupt(); if (recthread != null) recthread.interrupt(); + try { + if (sendthread != null) { + sendthread.interrupt(); + if (wait) + sendthread.join(5000); + } + if (recthread != null) { + recthread.interrupt(); + if (wait) + recthread.join(5000); + } + } catch (InterruptedException e) { + e.printStackTrace(); //This thread shouldn't be interrupted + } } private BukkitTask rectask; private LinkedBlockingQueue recevents = new LinkedBlockingQueue<>(); - private IMessage lastmsgfromd; // Last message sent by a Discord user, used for clearing checkmarks private Runnable recrun; private static Thread recthread; @@ -507,14 +531,15 @@ public class MCChatListener implements Listener, IListener } if (react) { try { - if (lastmsgfromd != null) { - DPUtils.perform(() -> lastmsgfromd.removeReaction(DiscordPlugin.dc.getOurUser(), + val lmfd = lastmsgfromd.get(event.getChannel().getLongID()); + if (lmfd != null) { + DPUtils.perform(() -> lmfd.removeReaction(DiscordPlugin.dc.getOurUser(), DiscordPlugin.DELIVERED_REACTION)); // Remove it no matter what, we know it's there 99.99% of the time } } catch (Exception e) { TBMCCoreAPI.SendException("An error occured while removing reactions from chat!", e); } - lastmsgfromd = event.getMessage(); + lastmsgfromd.put(event.getChannel().getLongID(), event.getMessage()); DPUtils.perform(() -> event.getMessage().addReaction(DiscordPlugin.DELIVERED_REACTION)); } } catch (Exception e) { @@ -573,4 +598,9 @@ public class MCChatListener implements Listener, IListener return Optional.of(dsender); }).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get(); } + + @FunctionalInterface + private interface InterruptibleConsumer { + void accept(T value) throws TimeoutException, InterruptedException; + } } diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/ResetMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/ResetMCCommand.java new file mode 100644 index 0000000..5d571a7 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mccommands/ResetMCCommand.java @@ -0,0 +1,35 @@ +package buttondevteam.discordplugin.mccommands; + +import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.discordplugin.listeners.MCChatListener; +import buttondevteam.lib.chat.CommandClass; +import buttondevteam.lib.chat.TBMCCommandBase; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +@CommandClass(path = "discord reset", modOnly = true) +public class ResetMCCommand extends TBMCCommandBase { //Not player-only, so not using DiscordMCCommandBase + @Override + public boolean OnCommand(CommandSender sender, String s, String[] strings) { + Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> { + sender.sendMessage("§bStopping MCChatListener..."); + DiscordPlugin.SafeMode = true; + MCChatListener.stop(true); + if (DiscordPlugin.dc.isLoggedIn()) { + sender.sendMessage("§bLogging out..."); + DiscordPlugin.dc.logout(); + } else + sender.sendMessage("§bWe're not logged in."); + sender.sendMessage("§bLogging in..."); + DiscordPlugin.dc.login(); + DiscordPlugin.SafeMode = false; + sender.sendMessage("§bChromaBot has been reset!"); + }); + return false; + } + + @Override + public String[] GetHelpText(String s) { + return new String[0]; + } +}