diff --git a/pom.xml b/pom.xml index 4fb93cb..f0c321a 100755 --- a/pom.xml +++ b/pom.xml @@ -1,208 +1,225 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - com.github.TBMCPlugins - DiscordPlugin - master-SNAPSHOT - jar + com.github.TBMCPlugins + DiscordPlugin + master-SNAPSHOT + jar - DiscordPlugin - http://maven.apache.org + DiscordPlugin + http://maven.apache.org - - - src/main/java - - - src - - **/*.java - - - - src/main/resources - - *.properties - *.yml - *.csv - *.txt - - true - - - DiscordPlugin - - - maven-compiler-plugin - 3.6.2 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-shade-plugin - 2.4.2 - - - package - - shade - - - - - org.spigotmc:spigot-api - com.github.TBMCPlugins.ButtonCore:ButtonCore - net.ess3:Essentials + + + src/main/java + + + src + + **/*.java + + + + src/main/resources + + *.properties + *.yml + *.csv + *.txt + + true + + + DiscordPlugin + + + maven-compiler-plugin + 3.6.2 + + 1.8 + 1.8 + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.2 + + + package + + shade + + + + + org.spigotmc:spigot-api + com.github.TBMCPlugins.ButtonCore:ButtonCore + net.ess3:Essentials - - - - - - - org.apache.maven.plugins - maven-resources-plugin - 3.0.1 - - - copy - compile - - copy-resources - - - target - - - resources - - - - - - - - - maven-surefire-plugin - - false - - - - - + + true + + + io.netty + btndvtm.dp.io.netty + + + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.0.1 + + + copy + compile + + copy-resources + + + target + + + resources + + + + + + + + + maven-surefire-plugin + 2.4.2 + + false + + + + + - - UTF-8 + + UTF-8 master - + - - - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots/ - - - jcenter - http://jcenter.bintray.com - - - jitpack.io - https://jitpack.io - + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + jcenter + http://jcenter.bintray.com + + + jitpack.io + https://jitpack.io + - - Essentials - http://repo.ess3.net/content/repositories/essrel/ - - - projectlombok.org - http://projectlombok.org/mavenrepo - + + Essentials + http://repo.ess3.net/content/repositories/essrel/ + + + projectlombok.org + http://projectlombok.org/mavenrepo + - + + - - - junit - junit - 3.8.1 - test - - - org.spigotmc - spigot-api - 1.12-R0.1-SNAPSHOT - provided - - - org.spigotmc - spigot - 1.12.2-R0.1-SNAPSHOT - provided - - + + + junit + junit + 3.8.1 + test + + + org.spigotmc + spigot-api + 1.12-R0.1-SNAPSHOT + provided + + + org.spigotmc + spigot + 1.12.2-R0.1-SNAPSHOT + provided + + com.discord4j - Discord4J - 2.10.1 + discord4j-core + 3.0.6 - - - org.slf4j - slf4j-jdk14 - 1.7.21 - - - com.github.TBMCPlugins.ButtonCore - ButtonCore - ${branch}-SNAPSHOT - provided - - - com.github.milkbowl - VaultAPI - master-SNAPSHOT - provided - - - net.ess3 - Essentials - 2.13.1 + + + org.slf4j + slf4j-jdk14 + 1.7.21 + + + com.github.TBMCPlugins.ButtonCore + ButtonCore + ${branch}-SNAPSHOT provided - - - com.github.xaanit - D4J-OAuth - master-SNAPSHOT - - - - org.projectlombok - lombok - 1.16.16 - provided - + + + com.github.milkbowl + VaultAPI + master-SNAPSHOT + provided + + + net.ess3 + Essentials + 2.13.1 + provided + + + + org.projectlombok + lombok + 1.16.16 + provided + - - - org.objenesis - objenesis - 2.6 - test - - - com.vdurmont - emoji-java - 4.0.0 - - + + + org.objenesis + objenesis + 2.6 + test + + + com.vdurmont + emoji-java + 4.0.0 + + + + + com.github.lucko + LuckPerms + v4.4 + provided + + - - - ci - - - env.TRAVIS_BRANCH - - - - - ${env.TRAVIS_BRANCH} - - - + + + ci + + + env.TRAVIS_BRANCH + + + + + ${env.TRAVIS_BRANCH} + + + diff --git a/src/main/java/buttondevteam/discordplugin/AsyncDiscordEvent.java b/src/main/java/buttondevteam/discordplugin/AsyncDiscordEvent.java deleted file mode 100644 index c4479f3..0000000 --- a/src/main/java/buttondevteam/discordplugin/AsyncDiscordEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -package buttondevteam.discordplugin; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import org.bukkit.event.Cancellable; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; - -@RequiredArgsConstructor -public class AsyncDiscordEvent extends Event implements Cancellable { - private final @Getter T event; - @Getter - @Setter - private boolean cancelled; - - private static final HandlerList handlers = new HandlerList(); - - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/ChromaBot.java b/src/main/java/buttondevteam/discordplugin/ChromaBot.java index 56a6fb9..6f74671 100755 --- a/src/main/java/buttondevteam/discordplugin/ChromaBot.java +++ b/src/main/java/buttondevteam/discordplugin/ChromaBot.java @@ -1,15 +1,14 @@ package buttondevteam.discordplugin; import buttondevteam.discordplugin.mcchat.MCChatUtils; +import discord4j.core.object.entity.Message; +import discord4j.core.object.entity.MessageChannel; import lombok.Getter; -import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitScheduler; -import sx.blah.discord.api.internal.json.objects.EmbedObject; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.util.EmbedBuilder; +import reactor.core.publisher.Mono; import javax.annotation.Nullable; -import java.awt.*; +import java.util.function.Function; public class ChromaBot { /** @@ -33,113 +32,26 @@ public class ChromaBot { instance = null; } - /** - * Send a message to the chat channel and private chats. - * - * @param message - * The message to send, duh - */ - public void sendMessage(String message) { - MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message)); - } - /** * Send a message to the chat channels and private chats. * * @param message - * The message to send, duh - * @param embed - * Custom fancy stuff, use {@link EmbedBuilder} to create one + * The message to send, duh (use {@link MessageChannel#createMessage(String)}) */ - public void sendMessage(String message, EmbedObject embed) { - MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, embed)); + public void sendMessage(Function, Mono> message) { + MCChatUtils.forAllMCChat(ch -> message.apply(ch).subscribe()); } /** * Send a message to the chat channels, private chats and custom chats. * * @param message The message to send, duh - * @param embed Custom fancy stuff, use {@link EmbedBuilder} to create one * @param toggle The toggle type for channelcon */ - public void sendMessageCustomAsWell(String message, EmbedObject embed, @Nullable ChannelconBroadcast toggle) { - MCChatUtils.forCustomAndAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, embed), toggle, false); + public void sendMessageCustomAsWell(Function, Mono> message, @Nullable ChannelconBroadcast toggle) { + MCChatUtils.forCustomAndAllMCChat(ch -> message.apply(ch).subscribe(), toggle, false); } - /** - * Send a message to an arbitrary channel. This will not send it to the private chats. - * - * @param channel - * The channel to send to, use the channel variables in {@link DiscordPlugin} - * @param message - * The message to send, duh - * @param embed - * Custom fancy stuff, use {@link EmbedBuilder} to create one - */ - public void sendMessage(IChannel channel, String message, EmbedObject embed) { - DiscordPlugin.sendMessageToChannel(channel, message, embed); - } - - /** - * Send a fancy message to the chat channels. This will show a bold text with a colored line. - * - * @param message - * The message to send, duh - * @param color - * The color of the line before the text - */ - public void sendMessage(String message, Color color) { - MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, - new EmbedBuilder().withTitle(message).withColor(color).build())); - } - - /** - * Send a fancy message to the chat channels. This will show a bold text with a colored line. - * - * @param message - * The message to send, duh - * @param color - * The color of the line before the text - * @param mcauthor - * The name of the Minecraft player who is the author of this message - */ - public void sendMessage(String message, Color color, String mcauthor) { - MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, - DPUtils.embedWithHead(new EmbedBuilder().withTitle(message).withColor(color), mcauthor).build())); - } - - /** - * Send a fancy message to the chat channels. This will show a bold text with a colored line. - * - * @param message - * The message to send, duh - * @param color - * The color of the line before the text - * @param authorname - * The name of the author of this message - * @param authorimg - * The URL of the avatar image for this message's author - */ - public void sendMessage(String message, Color color, String authorname, String authorimg) { - MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, new EmbedBuilder() - .withTitle(message).withColor(color).withAuthorName(authorname).withAuthorIcon(authorimg).build())); - } - - /** - * Send a message to the chat channels. This will show a bold text with a colored line. - * - * @param message - * The message to send, duh - * @param color - * The color of the line before the text - * @param sender - * The player who sends this message - */ - public void sendMessage(String message, Color color, Player sender) { - MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, DPUtils - .embedWithHead(new EmbedBuilder().withTitle(message).withColor(color), sender.getName()).build())); - } - public void updatePlayerList() { MCChatUtils.updatePlayerList(); } diff --git a/src/main/java/buttondevteam/discordplugin/DPUtils.java b/src/main/java/buttondevteam/discordplugin/DPUtils.java index c21cceb..616b750 100755 --- a/src/main/java/buttondevteam/discordplugin/DPUtils.java +++ b/src/main/java/buttondevteam/discordplugin/DPUtils.java @@ -4,128 +4,82 @@ import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.architecture.IHaveConfig; +import buttondevteam.lib.architecture.ReadOnlyConfigData; +import discord4j.core.object.entity.Guild; +import discord4j.core.object.entity.Message; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.Role; +import discord4j.core.object.util.Snowflake; +import discord4j.core.spec.EmbedCreateSpec; import lombok.val; -import org.bukkit.Bukkit; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IGuild; -import sx.blah.discord.handle.obj.IIDLinkedObject; -import sx.blah.discord.handle.obj.IRole; -import sx.blah.discord.util.EmbedBuilder; -import sx.blah.discord.util.RequestBuffer; -import sx.blah.discord.util.RequestBuffer.IRequest; -import sx.blah.discord.util.RequestBuffer.IVoidRequest; +import reactor.core.publisher.Mono; import javax.annotation.Nullable; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.logging.Logger; import java.util.regex.Matcher; public final class DPUtils { - public static EmbedBuilder embedWithHead(EmbedBuilder builder, String playername) { - return builder.withAuthorIcon("https://minotar.net/avatar/" + playername + "/32.png"); + public static EmbedCreateSpec embedWithHead(EmbedCreateSpec ecs, String displayname, String playername, String profileUrl) { + return ecs.setAuthor(displayname, profileUrl, "https://minotar.net/avatar/" + playername + "/32.png"); } - /** - * Removes §[char] colour codes from strings & escapes them for Discord
- * Ensure that this method only gets called once (escaping) - */ - public static String sanitizeString(String string) { - return escape(sanitizeStringNoEscape(string)); - } - - /** - * Removes §[char] colour codes from strings - */ - public static String sanitizeStringNoEscape(String string) { - String sanitizedString = ""; - boolean random = false; - for (int i = 0; i < string.length(); i++) { - if (string.charAt(i) == '§') { - i++;// Skips the data value, the 4 in "§4Alisolarflare" - random = string.charAt(i) == 'k'; - } else { - if (!random) // Skip random/obfuscated characters - sanitizedString += string.charAt(i); - } - } - return sanitizedString; - } - /** - * Performs Discord actions, retrying when ratelimited. May return null if action fails too many times or in safe mode. + * Removes §[char] colour codes from strings & escapes them for Discord
+ * Ensure that this method only gets called once (escaping) */ - @Nullable - public static T perform(IRequest action, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { - if (DiscordPlugin.SafeMode) - return null; - if (Bukkit.isPrimaryThread()) // TODO: Ignore shutdown message <-- - // throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag."); - getLogger().warning("Waiting for a Discord request on the main thread!"); - return RequestBuffer.request(action).get(timeout, unit); // Let the pros handle this - } - - /** - * 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) { - if (DiscordPlugin.SafeMode) - return null; - if (Bukkit.isPrimaryThread()) // TODO: Ignore shutdown message <-- - // throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag."); - getLogger().warning("Waiting for a Discord request on the main thread!"); - return RequestBuffer.request(action).get(); // Let the pros handle this - } + public static String sanitizeString(String string) { + return escape(sanitizeStringNoEscape(string)); + } /** - * Performs Discord actions, retrying when ratelimited. + * Removes §[char] colour codes from strings */ - public static Void perform(IVoidRequest action) { - if (DiscordPlugin.SafeMode) - return null; - if (Bukkit.isPrimaryThread()) - throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag."); - return RequestBuffer.request(action).get(); // Let the pros handle this + public static String sanitizeStringNoEscape(String string) { + StringBuilder sanitizedString = new StringBuilder(); + boolean random = false; + for (int i = 0; i < string.length(); i++) { + if (string.charAt(i) == '§') { + i++;// Skips the data value, the 4 in "§4Alisolarflare" + random = string.charAt(i) == 'k'; + } else { + if (!random) // Skip random/obfuscated characters + sanitizedString.append(string.charAt(i)); + } + } + return sanitizedString.toString(); } - public static void performNoWait(IVoidRequest action) { - if (DiscordPlugin.SafeMode) - return; - RequestBuffer.request(action); + private static String escape(String message) { + return message.replaceAll("([*_~])", Matcher.quoteReplacement("\\") + "$1"); } - public static void performNoWait(IRequest action) { - if (DiscordPlugin.SafeMode) - return; - RequestBuffer.request(action); - } - - public static String escape(String message) { - return message.replaceAll("([*_~])", Matcher.quoteReplacement("\\")+"$1"); - } - public static Logger getLogger() { if (DiscordPlugin.plugin == null || DiscordPlugin.plugin.getLogger() == null) return Logger.getLogger("DiscordPlugin"); return DiscordPlugin.plugin.getLogger(); } - public static ConfigData channelData(IHaveConfig config, String key, long defID) { - return config.getDataPrimDef(key, defID, id -> DiscordPlugin.dc.getChannelByID((long) id), IIDLinkedObject::getLongID); //We can afford to search for the channel in the cache once (instead of using mainServer) + public static ReadOnlyConfigData> channelData(IHaveConfig config, String key, long defID) { + return config.getReadOnlyDataPrimDef(key, defID, id -> getMessageChannel(key, Snowflake.of((Long) id)), ch -> defID); //We can afford to search for the channel in the cache once (instead of using mainServer) } - public static ConfigData roleData(IHaveConfig config, String key, String defName) { - return roleData(config, key, defName, DiscordPlugin.mainServer); + public static ReadOnlyConfigData> roleData(IHaveConfig config, String key, String defName) { + return roleData(config, key, defName, Mono.just(DiscordPlugin.mainServer)); } - public static ConfigData roleData(IHaveConfig config, String key, String defName, IGuild guild) { - return config.getDataPrimDef(key, defName, name -> { - if (!(name instanceof String)) return null; - val roles = guild.getRolesByName((String) name); - return roles.size() > 0 ? roles.get(0) : null; - }, IIDLinkedObject::getLongID); + /** + * Needs to be a {@link ConfigData} for checking if it's set + */ + public static ReadOnlyConfigData> roleData(IHaveConfig config, String key, String defName, Mono guild) { + return config.getReadOnlyDataPrimDef(key, defName, name -> { + if (!(name instanceof String)) return Mono.empty(); + return guild.flatMapMany(Guild::getRoles).filter(r -> r.getName().equals(name)).next(); + }, r -> defName); + } + + public static ConfigData snowflakeData(IHaveConfig config, String key, long defID) { + return config.getDataPrimDef(key, defID, id -> Snowflake.of((long) id), Snowflake::asLong); } /** @@ -134,10 +88,8 @@ public final class DPUtils { * @return The string for mentioning the channel */ public static String botmention() { - IChannel channel; - if (DiscordPlugin.plugin == null - || (channel = DiscordPlugin.plugin.CommandChannel().get()) == null) return "#bot"; - return channel.mention(); + if (DiscordPlugin.plugin == null) return "#bot"; + return channelMention(DiscordPlugin.plugin.commandChannel().get()); } /** @@ -149,23 +101,66 @@ public final class DPUtils { */ public static boolean disableIfConfigError(@Nullable Component component, ConfigData... configs) { for (val config : configs) { - if (config.get() == null) { - String path = null; - try { - if (component != null) - Component.setComponentEnabled(component, false); - val f = ConfigData.class.getDeclaredField("path"); - f.setAccessible(true); //Hacking my own plugin - path = (String) f.get(config); - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to disable component after config error!", e); - } - getLogger().warning("The config value " + path + " isn't set correctly " + (component == null ? "in global settings!" : "for component " + component.getClass().getSimpleName() + "!")); - getLogger().warning("Set the correct ID in the config" + (component == null ? "" : " or disable this component") + " to remove this message."); + Object v = config.get(); + if (disableIfConfigErrorRes(component, config, v)) return true; - } } return false; } + /** + * Disables the component if one of the given configs return null. Useful for channel/role configs. + * + * @param component The component to disable if needed + * @param config The (snowflake) config to check for null + * @param result The result of getting the value + * @return Whether the component got disabled and a warning logged + */ + public static boolean disableIfConfigErrorRes(@Nullable Component component, ConfigData config, Object result) { + //noinspection ConstantConditions + if (result == null || (result instanceof Mono && !((Mono) result).hasElement().block())) { + String path = null; + try { + if (component != null) + Component.setComponentEnabled(component, false); + path = config.getPath(); + } catch (Exception e) { + TBMCCoreAPI.SendException("Failed to disable component after config error!", e); + } + getLogger().warning("The config value " + path + " isn't set correctly " + (component == null ? "in global settings!" : "for component " + component.getClass().getSimpleName() + "!")); + getLogger().warning("Set the correct ID in the config" + (component == null ? "" : " or disable this component") + " to remove this message."); + return true; + } + return false; + } + + public static Mono reply(Message original, @Nullable MessageChannel channel, String message) { + Mono ch; + if (channel == null) + ch = original.getChannel(); + else + ch = Mono.just(channel); + return ch.flatMap(chan -> chan.createMessage((original.getAuthor().isPresent() + ? original.getAuthor().get().getMention() + ", " : "") + message)); + } + + public static String nickMention(Snowflake userId) { + return "<@!" + userId.asString() + ">"; + } + + public static String channelMention(Snowflake channelId) { + return "<#" + channelId.asString() + ">"; + } + + public static Mono getMessageChannel(String key, Snowflake id) { + return DiscordPlugin.dc.getChannelById(id).onErrorResume(e -> { + getLogger().warning("Failed to get channel data for " + key + "=" + id + " - " + e.getMessage()); + return Mono.empty(); + }).filter(ch -> ch instanceof MessageChannel).cast(MessageChannel.class); + } + + public static Mono getMessageChannel(ConfigData config) { + return getMessageChannel(config.getPath(), config.get()); + } + } diff --git a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java index f5b779f..6e30a53 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java @@ -1,19 +1,24 @@ package buttondevteam.discordplugin; +import buttondevteam.discordplugin.mcchat.MinecraftChatModule; import buttondevteam.discordplugin.playerfaker.DiscordFakePlayer; import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.User; import lombok.Getter; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IUser; +import lombok.Setter; import java.util.UUID; public class DiscordConnectedPlayer extends DiscordFakePlayer implements IMCPlayer { private static int nextEntityId = 10000; private @Getter VanillaCommandListener vanillaCmdListener; + @Getter + @Setter + private boolean loggedIn = false; - public DiscordConnectedPlayer(IUser user, IChannel channel, UUID uuid, String mcname) { - super(user, channel, nextEntityId++, uuid, mcname); + public DiscordConnectedPlayer(User user, MessageChannel channel, UUID uuid, String mcname, MinecraftChatModule module) { + super(user, channel, nextEntityId++, uuid, mcname, module); vanillaCmdListener = new VanillaCommandListener<>(this); } diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java b/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java index 0055792..d748433 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java @@ -1,28 +1,28 @@ -package buttondevteam.discordplugin; - -import buttondevteam.discordplugin.mcchat.MCChatPrivate; -import buttondevteam.lib.player.ChromaGamerBase; -import buttondevteam.lib.player.UserClass; - -@UserClass(foldername = "discord") -public class DiscordPlayer extends ChromaGamerBase { - private String did; - // private @Getter @Setter boolean minecraftChatEnabled; - - public DiscordPlayer() { - } - - public String getDiscordID() { - if (did == null) - did = plugindata.getString(getFolder() + "_id"); - return did; - } - - /** - * Returns true if player has the private Minecraft chat enabled. For setting the value, see - * {@link MCChatPrivate#privateMCChat(sx.blah.discord.handle.obj.IChannel, boolean, sx.blah.discord.handle.obj.IUser, DiscordPlayer)} - */ - public boolean isMinecraftChatEnabled() { - return MCChatPrivate.isMinecraftChatEnabled(this); - } -} +package buttondevteam.discordplugin; + +import buttondevteam.discordplugin.mcchat.MCChatPrivate; +import buttondevteam.lib.player.ChromaGamerBase; +import buttondevteam.lib.player.UserClass; + +@UserClass(foldername = "discord") +public class DiscordPlayer extends ChromaGamerBase { + private String did; + // private @Getter @Setter boolean minecraftChatEnabled; + + public DiscordPlayer() { + } + + public String getDiscordID() { + if (did == null) + did = plugindata.getString(getFolder() + "_id"); + return did; + } + + /** + * Returns true if player has the private Minecraft chat enabled. For setting the value, see + * {@link MCChatPrivate#privateMCChat(sx.blah.discord.handle.obj.MessageChannel, boolean, sx.blah.discord.handle.obj.User, DiscordPlayer)} + */ + public boolean isMinecraftChatEnabled() { + return MCChatPrivate.isMinecraftChatEnabled(this); + } +} diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java index 2c10314..807abd4 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java @@ -1,6 +1,8 @@ package buttondevteam.discordplugin; import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.User; import lombok.Getter; import org.bukkit.*; import org.bukkit.advancement.Advancement; @@ -26,8 +28,6 @@ import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.scoreboard.Scoreboard; import org.bukkit.util.Vector; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IUser; import java.net.InetSocketAddress; import java.util.*; @@ -38,7 +38,7 @@ public class DiscordPlayerSender extends DiscordSenderBase implements IMCPlayer< protected Player player; private @Getter VanillaCommandListener vanillaCmdListener; - public DiscordPlayerSender(IUser user, IChannel channel, Player player) { + public DiscordPlayerSender(User user, MessageChannel channel, Player player) { super(user, channel); this.player = player; vanillaCmdListener = new VanillaCommandListener(this); diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java index 6d71070..b5a0efc 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java @@ -10,16 +10,27 @@ import buttondevteam.discordplugin.listeners.MCListener; import buttondevteam.discordplugin.mcchat.MCChatPrivate; import buttondevteam.discordplugin.mcchat.MCChatUtils; import buttondevteam.discordplugin.mcchat.MinecraftChatModule; -import buttondevteam.discordplugin.mccommands.DiscordMCCommandBase; -import buttondevteam.discordplugin.mccommands.ResetMCCommand; +import buttondevteam.discordplugin.mccommands.DiscordMCCommand; import buttondevteam.discordplugin.role.GameRoleModule; +import buttondevteam.discordplugin.util.Timings; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.architecture.ButtonPlugin; import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.ConfigData; -import buttondevteam.lib.chat.TBMCChatAPI; +import buttondevteam.lib.architecture.IHaveConfig; import buttondevteam.lib.player.ChromaGamerBase; import com.google.common.io.Files; +import discord4j.core.DiscordClient; +import discord4j.core.DiscordClientBuilder; +import discord4j.core.event.domain.guild.GuildCreateEvent; +import discord4j.core.event.domain.lifecycle.ReadyEvent; +import discord4j.core.object.entity.Guild; +import discord4j.core.object.entity.Role; +import discord4j.core.object.presence.Activity; +import discord4j.core.object.presence.Presence; +import discord4j.core.object.reaction.ReactionEmoji; +import discord4j.core.object.util.Snowflake; +import discord4j.store.jdk.JdkStoreService; import lombok.Getter; import lombok.val; import net.milkbowl.vault.permission.Permission; @@ -27,310 +38,254 @@ import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.plugin.RegisteredServiceProvider; -import org.bukkit.scheduler.BukkitTask; -import sx.blah.discord.api.ClientBuilder; -import sx.blah.discord.api.IDiscordClient; -import sx.blah.discord.api.events.IListener; -import sx.blah.discord.api.internal.json.objects.EmbedObject; -import sx.blah.discord.handle.impl.events.ReadyEvent; -import sx.blah.discord.handle.impl.obj.ReactionEmoji; -import sx.blah.discord.handle.obj.*; -import sx.blah.discord.util.EmbedBuilder; -import sx.blah.discord.util.RequestBuffer; +import reactor.core.publisher.Mono; import java.awt.*; import java.io.File; import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -public class DiscordPlugin extends ButtonPlugin implements IListener { - public static IDiscordClient dc; - public static DiscordPlugin plugin; - public static boolean SafeMode = true; +@ButtonPlugin.ConfigOpts(disableConfigGen = true) +public class DiscordPlugin extends ButtonPlugin { + public static DiscordClient dc; + public static DiscordPlugin plugin; + public static boolean SafeMode = true; @Getter private Command2DC manager; - public ConfigData Prefix() { - return getIConfig().getData("prefix", '/', str -> ((String) str).charAt(0), Object::toString); - } - - public static char getPrefix() { - if (plugin == null) return '/'; - return plugin.Prefix().get(); - } - - public ConfigData MainServer() { - return getIConfig().getDataPrimDef("mainServer", 219529124321034241L, id -> dc.getGuildByID((long) id), IIDLinkedObject::getLongID); + private ConfigData prefix() { + return getIConfig().getData("prefix", '/', str -> ((String) str).charAt(0), Object::toString); } - public ConfigData CommandChannel() { - return DPUtils.channelData(getIConfig(), "commandChannel", 239519012529111040L); + public static char getPrefix() { + if (plugin == null) return '/'; + return plugin.prefix().get(); } - public ConfigData ModRole() { + private ConfigData> mainServer() { + return getIConfig().getDataPrimDef("mainServer", 0L, + id -> { + //It attempts to get the default as well + if ((long) id == 0L) + return Optional.empty(); //Hack? + return dc.getGuildById(Snowflake.of((long) id)) + .onErrorResume(t -> Mono.fromRunnable(() -> getLogger().warning("Failed to get guild: " + t.getMessage()))).blockOptional(); + }, + g -> g.map(gg -> gg.getId().asLong()).orElse(0L)); + } + + public ConfigData commandChannel() { + return DPUtils.snowflakeData(getIConfig(), "commandChannel", 239519012529111040L); + } + + /** + * If the role doesn't exist, then it will only allow for the owner. + */ + public ConfigData> modRole() { return DPUtils.roleData(getIConfig(), "modRole", "Moderator"); } - @Override - public void pluginEnable() { - try { - getLogger().info("Initializing..."); - plugin = this; - manager = new Command2DC(); - ClientBuilder cb = new ClientBuilder(); - File tokenFile = new File("TBMC", "Token.txt"); - if (tokenFile.exists()) //Legacy support - //noinspection UnstableApiUsage - cb.withToken(Files.readFirstLine(tokenFile, StandardCharsets.UTF_8)); - else { - File privateFile = new File(getDataFolder(), "private.yml"); - val conf = YamlConfiguration.loadConfiguration(privateFile); - String token = conf.getString("token"); - if (token == null) { - conf.set("token", "Token goes here"); - conf.save(privateFile); + /** + * The invite link to show by /discord invite. If empty, it defaults to the first invite if the bot has access. + */ + public ConfigData inviteLink() { + return getIConfig().getData("inviteLink", ""); + } - getLogger().severe("Token not found! Set it in private.yml"); - Bukkit.getPluginManager().disablePlugin(this); - return; - } else - cb.withToken(token); - } - dc = cb.login(); - dc.getDispatcher().registerListener(this); - } catch (Exception e) { - e.printStackTrace(); - Bukkit.getPluginManager().disablePlugin(this); - } - } + @Override + public void pluginEnable() { + try { + getLogger().info("Initializing..."); + plugin = this; + manager = new Command2DC(); + String token; + File tokenFile = new File("TBMC", "Token.txt"); + if (tokenFile.exists()) //Legacy support + //noinspection UnstableApiUsage + token = Files.readFirstLine(tokenFile, StandardCharsets.UTF_8); + else { + File privateFile = new File(getDataFolder(), "private.yml"); + val conf = YamlConfiguration.loadConfiguration(privateFile); + token = conf.getString("token"); + if (token == null || token.equalsIgnoreCase("Token goes here")) { + conf.set("token", "Token goes here"); + conf.save(privateFile); - public static IGuild mainServer; + getLogger().severe("Token not found! Set it in private.yml"); + Bukkit.getPluginManager().disablePlugin(this); + return; + } + } + val cb = new DiscordClientBuilder(token); + cb.setInitialPresence(Presence.doNotDisturb(Activity.playing("booting"))); + cb.setStoreService(new JdkStoreService()); //The default doesn't work for some reason - it's waaay faster now + dc = cb.build(); + dc.getEventDispatcher().on(ReadyEvent.class) // Listen for ReadyEvent(s) + .map(event -> event.getGuilds().size()) // Get how many guilds the bot is in + .flatMap(size -> dc.getEventDispatcher() + .on(GuildCreateEvent.class) // Listen for GuildCreateEvent(s) + .take(size) // Take only the first `size` GuildCreateEvent(s) to be received + .collectList()) // Take all received GuildCreateEvents and make it a List + .subscribe(this::handleReady); /* All guilds have been received, client is fully connected */ + dc.login().subscribe(); + } catch (Exception e) { + e.printStackTrace(); + Bukkit.getPluginManager().disablePlugin(this); + } + } - private static volatile BukkitTask task; - private static volatile boolean sent = false; + public static Guild mainServer; - @Override - public void handle(ReadyEvent event) { - try { - dc.changePresence(StatusType.DND, ActivityType.PLAYING, "booting"); - val tries = new AtomicInteger(); - task = Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> { - tries.incrementAndGet(); - if (tries.get() > 10) { //5 seconds - task.cancel(); - getLogger().severe("Main server not found! Invite the bot and do /discord reset"); - //getIConfig().getConfig().set("mainServer", 219529124321034241L); //Needed because it won't save as long as it's null - made it save - saveConfig(); //Put default there - return; - } - mainServer = MainServer().get(); //Shouldn't change afterwards - if (mainServer == null) { - val guilds = dc.getGuilds(); - if (guilds.size() == 0) - return; //If there are no guilds in cache, retry - mainServer = guilds.get(0); - getLogger().warning("Main server set to first one: " + mainServer.getName()); - MainServer().set(mainServer); //Save in config - } - if (!TBMCCoreAPI.IsTestServer()) { //Don't change conditions here, see mainServer=devServer=null in onDisable() - dc.changePresence(StatusType.ONLINE, ActivityType.PLAYING, "Minecraft"); - } else { - dc.changePresence(StatusType.ONLINE, ActivityType.PLAYING, "testing"); - } - SafeMode = false; - if (task != null) - task.cancel(); - if (!sent) { - DPUtils.disableIfConfigError(null, CommandChannel(), ModRole()); //Won't disable, just prints the warning here + private void handleReady(List event) { + try { + mainServer = mainServer().get().orElse(null); //Shouldn't change afterwards + if (mainServer == null) { + if (event.size() == 0) { + getLogger().severe("Main server not found! Invite the bot and do /discord reset"); + saveConfig(); //Put default there + return; //We should have all guilds by now, no need to retry + } + mainServer = event.get(0).getGuild(); + getLogger().warning("Main server set to first one: " + mainServer.getName()); + mainServer().set(Optional.of(mainServer)); //Save in config + } + SafeMode = false; + DPUtils.disableIfConfigErrorRes(null, commandChannel(), DPUtils.getMessageChannel(commandChannel())); + DPUtils.disableIfConfigError(null, modRole()); //Won't disable, just prints the warning here - Component.registerComponent(this, new GeneralEventBroadcasterModule()); - Component.registerComponent(this, new MinecraftChatModule()); - Component.registerComponent(this, new ExceptionListenerModule()); - Component.registerComponent(this, new GameRoleModule()); //Needs the mainServer to be set - Component.registerComponent(this, new AnnouncerModule()); - Component.registerComponent(this, new FunModule()); - new ChromaBot(this).updatePlayerList(); //Initialize ChromaBot - The MCCHatModule is tested to be enabled + Component.registerComponent(this, new GeneralEventBroadcasterModule()); + Component.registerComponent(this, new MinecraftChatModule()); + Component.registerComponent(this, new ExceptionListenerModule()); + Component.registerComponent(this, new GameRoleModule()); //Needs the mainServer to be set + Component.registerComponent(this, new AnnouncerModule()); + Component.registerComponent(this, new FunModule()); + new ChromaBot(this).updatePlayerList(); //Initialize ChromaBot - The MCCHatModule is tested to be enabled - getManager().registerCommand(new VersionCommand()); - getManager().registerCommand(new UserinfoCommand()); - getManager().registerCommand(new HelpCommand()); - getManager().registerCommand(new DebugCommand()); - getManager().registerCommand(new ConnectCommand()); - if (ResetMCCommand.resetting) //These will only execute if the chat is enabled - ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.CYAN) - .withTitle("Discord plugin restarted - chat connected.").build(), ChannelconBroadcast.RESTART); //Really important to note the chat, hmm - else if (getConfig().getBoolean("serverup", false)) { - ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.YELLOW) - .withTitle("Server recovered from a crash - chat connected.").build(), ChannelconBroadcast.RESTART); - val thr = new Throwable( - "The server shut down unexpectedly. See the log of the previous run for more details."); - thr.setStackTrace(new StackTraceElement[0]); - TBMCCoreAPI.SendException("The server crashed!", thr); - } else - ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.GREEN) - .withTitle("Server started - chat connected.").build(), ChannelconBroadcast.RESTART); + getManager().registerCommand(new VersionCommand()); + getManager().registerCommand(new UserinfoCommand()); + getManager().registerCommand(new HelpCommand()); + getManager().registerCommand(new DebugCommand()); + getManager().registerCommand(new ConnectCommand()); + if (DiscordMCCommand.resetting) //These will only execute if the chat is enabled + ChromaBot.getInstance().sendMessageCustomAsWell(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(Color.CYAN) + .setTitle("Discord plugin restarted - chat connected."))), ChannelconBroadcast.RESTART); //Really important to note the chat, hmm + else if (getConfig().getBoolean("serverup", false)) { + ChromaBot.getInstance().sendMessageCustomAsWell(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(Color.YELLOW) + .setTitle("Server recovered from a crash - chat connected."))), ChannelconBroadcast.RESTART); + val thr = new Throwable( + "The server shut down unexpectedly. See the log of the previous run for more details."); + thr.setStackTrace(new StackTraceElement[0]); + TBMCCoreAPI.SendException("The server crashed!", thr); + } else + ChromaBot.getInstance().sendMessageCustomAsWell(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(Color.GREEN) + .setTitle("Server started - chat connected."))), ChannelconBroadcast.RESTART); - ResetMCCommand.resetting = false; //This is the last event handling this flag + DiscordMCCommand.resetting = false; //This is the last event handling this flag - getConfig().set("serverup", true); - saveConfig(); - sent = true; - if (TBMCCoreAPI.IsTestServer() && !dc.getOurUser().getName().toLowerCase().contains("test")) { - TBMCCoreAPI.SendException( - "Won't load because we're in testing mode and not using a separate account.", - new Exception( - "The plugin refuses to load until you change the token to a testing account. (The account needs to have \"test\" in it's name.)")); - Bukkit.getPluginManager().disablePlugin(this); - } - TBMCCoreAPI.SendUnsentExceptions(); - TBMCCoreAPI.SendUnsentDebugMessages(); - } - }, 0, 10); - for (IListener listener : CommonListeners.getListeners()) - dc.getDispatcher().registerListener(listener); - TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this); - TBMCChatAPI.AddCommands(this, DiscordMCCommandBase.class); - TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class); - ChromaGamerBase.addConverter(sender -> Optional.ofNullable(sender instanceof DiscordSenderBase - ? ((DiscordSenderBase) sender).getChromaUser() : null)); - setupProviders(); - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while enabling DiscordPlugin!", e); - } - } + getConfig().set("serverup", true); + saveConfig(); + if (TBMCCoreAPI.IsTestServer() && !Objects.requireNonNull(dc.getSelf().block()).getUsername().toLowerCase().contains("test")) { + TBMCCoreAPI.SendException( + "Won't load because we're in testing mode and not using a separate account.", + new Exception( + "The plugin refuses to load until you change the token to a testing account. (The account needs to have \"test\" in its name.)" + + "\nYou can disable test mode in ThorpeCore config.")); + Bukkit.getPluginManager().disablePlugin(this); + } + TBMCCoreAPI.SendUnsentExceptions(); + TBMCCoreAPI.SendUnsentDebugMessages(); + + CommonListeners.register(dc.getEventDispatcher()); + TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this); + getCommand2MC().registerCommand(new DiscordMCCommand()); + TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class); + ChromaGamerBase.addConverter(sender -> Optional.ofNullable(sender instanceof DiscordSenderBase + ? ((DiscordSenderBase) sender).getChromaUser() : null)); + setupProviders(); + + IHaveConfig.pregenConfig(this, null); + if (!TBMCCoreAPI.IsTestServer()) { + dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe(); + } else { + dc.updatePresence(Presence.online(Activity.playing("testing"))).subscribe(); + } + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occurred while enabling DiscordPlugin!", e); + } + } /** - * Always true, except when running "stop" from console - */ - public static boolean Restart; + * Always true, except when running "stop" from console + */ + public static boolean Restart; @Override public void pluginPreDisable() { if (ChromaBot.getInstance() == null) return; //Failed to load - EmbedObject embed; - if (ResetMCCommand.resetting) - embed = new EmbedBuilder().withColor(Color.ORANGE).withTitle("Discord plugin restarting").build(); - else - embed = 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 ") - + "kicked the hell out.") //TODO: Make configurable - : "") //If 'restart' is disabled then this isn't shown even if joinleave is enabled - .build(); - MCChatUtils.forCustomAndAllMCChat(ch -> { - try { - DiscordPlugin.sendMessageToChannelWait(ch, "", - embed, 5, TimeUnit.SECONDS); - } catch (TimeoutException | InterruptedException e) { - e.printStackTrace(); - } - }, ChannelconBroadcast.RESTART, false); + Timings timings = new Timings(); + timings.printElapsed("Disable start"); + MCChatUtils.forCustomAndAllMCChat(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> { + timings.printElapsed("Sending message to " + ch.getMention()); + if (DiscordMCCommand.resetting) + ecs.setColor(Color.ORANGE).setTitle("Discord plugin restarting"); + else + ecs.setColor(Restart ? Color.ORANGE : Color.RED) + .setTitle(Restart ? "Server restarting" : "Server stopping") + .setDescription( + Bukkit.getOnlinePlayers().size() > 0 + ? (DPUtils + .sanitizeString(Bukkit.getOnlinePlayers().stream() + .map(Player::getDisplayName).collect(Collectors.joining(", "))) + + (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ") + + "kicked the hell out.") //TODO: Make configurable + : ""); //If 'restart' is disabled then this isn't shown even if joinleave is enabled + })).subscribe(), ChannelconBroadcast.RESTART, false); + timings.printElapsed("Updating player list"); ChromaBot.getInstance().updatePlayerList(); + timings.printElapsed("Done"); } @Override public void pluginDisable() { + Timings timings = new Timings(); + timings.printElapsed("Actual disable start (logout)"); MCChatPrivate.logoutAll(); + timings.printElapsed("Config setup"); getConfig().set("serverup", false); if (ChromaBot.getInstance() == null) return; //Failed to load saveConfig(); - try { - SafeMode = true; // Stop interacting with Discord - ChromaBot.delete(); - dc.changePresence(StatusType.IDLE, ActivityType.PLAYING, "Chromacraft"); //No longer using the same account for testing - dc.logout(); - //Configs are emptied so channels and servers are fetched again - sent = false; - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e); - } - } + try { + SafeMode = true; // Stop interacting with Discord + ChromaBot.delete(); + timings.printElapsed("Updating presence..."); + dc.updatePresence(Presence.idle(Activity.playing("Chromacraft"))).block(); //No longer using the same account for testing + timings.printElapsed("Logging out..."); + dc.logout().block(); + //Configs are emptied so channels and servers are fetched again + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e); + } + } - public static final ReactionEmoji DELIVERED_REACTION = ReactionEmoji.of("✅"); + public static final ReactionEmoji DELIVERED_REACTION = ReactionEmoji.unicode("✅"); - public static void sendMessageToChannel(IChannel channel, String message) { - sendMessageToChannel(channel, message, null); - } + public static Permission perms; - public static void sendMessageToChannel(IChannel channel, String message, EmbedObject embed) { - try { - sendMessageToChannel(channel, message, embed, false); - } catch (TimeoutException | InterruptedException e) { - e.printStackTrace(); //Shouldn't happen, as we're not waiting on the result - } - } + private boolean setupProviders() { + try { + Class.forName("net.milkbowl.vault.permission.Permission"); + Class.forName("net.milkbowl.vault.chat.Chat"); + } catch (ClassNotFoundException e) { + return false; + } - 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) throws TimeoutException, InterruptedException { - return sendMessageToChannel(channel, message, embed, true); - } - - 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) 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) throws TimeoutException, InterruptedException { - if (message.length() > 1980) { - message = message.substring(0, 1980); - DPUtils.getLogger() - .warning("Message was too long to send to discord and got truncated. In " + channel.getName()); - } - try { - MCChatUtils.resetLastMessage(channel); // If this is a chat message, it'll be set again - final String content = message; - RequestBuffer.IRequest r = () -> embed == null ? channel.sendMessage(content) - : channel.sendMessage(content, embed, false); - if (wait) { - if (unit != null) - return DPUtils.perform(r, timeout, unit); - else - return DPUtils.perform(r); - } else { - if (unit != null) - plugin.getLogger().warning("Tried to set timeout for non-waiting call."); - else - DPUtils.performNoWait(r); - return null; - } - } catch (TimeoutException | InterruptedException e) { - throw e; - } catch (Exception e) { - DPUtils.getLogger().warning( - "Failed to deliver message to Discord! Channel: " + channel.getName() + " Message: " + message); - throw new RuntimeException(e); - } - } - - public static Permission perms; - - public boolean setupProviders() { - try { - Class.forName("net.milkbowl.vault.permission.Permission"); - Class.forName("net.milkbowl.vault.chat.Chat"); - } catch (ClassNotFoundException e) { - return false; - } - - RegisteredServiceProvider permsProvider = Bukkit.getServer().getServicesManager() - .getRegistration(Permission.class); - perms = permsProvider.getProvider(); - return perms != null; - } + RegisteredServiceProvider permsProvider = Bukkit.getServer().getServicesManager() + .getRegistration(Permission.class); + perms = permsProvider.getProvider(); + return perms != null; + } } diff --git a/src/main/java/buttondevteam/discordplugin/DiscordRunnable.java b/src/main/java/buttondevteam/discordplugin/DiscordRunnable.java deleted file mode 100755 index fb27234..0000000 --- a/src/main/java/buttondevteam/discordplugin/DiscordRunnable.java +++ /dev/null @@ -1,10 +0,0 @@ -package buttondevteam.discordplugin; - -import sx.blah.discord.util.DiscordException; -import sx.blah.discord.util.MissingPermissionsException; -import sx.blah.discord.util.RateLimitException; - -@FunctionalInterface -public interface DiscordRunnable { - public abstract void run() throws DiscordException, RateLimitException, MissingPermissionsException; -} diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSender.java b/src/main/java/buttondevteam/discordplugin/DiscordSender.java index 8ad7445..9eda278 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordSender.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordSender.java @@ -1,5 +1,9 @@ package buttondevteam.discordplugin; +import discord4j.core.object.entity.Member; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.User; +import lombok.val; import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.command.CommandSender; @@ -8,8 +12,6 @@ import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionAttachment; import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.plugin.Plugin; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IUser; import java.util.Set; @@ -18,12 +20,13 @@ public class DiscordSender extends DiscordSenderBase implements CommandSender { private String name; - public DiscordSender(IUser user, IChannel channel) { + public DiscordSender(User user, MessageChannel channel) { super(user, channel); - name = user == null ? "Discord user" : user.getDisplayName(DiscordPlugin.mainServer); + val def = "Discord user"; + name = user == null ? def : user.asMember(DiscordPlugin.mainServer.getId()).blockOptional().map(Member::getDisplayName).orElse(def); } - public DiscordSender(IUser user, IChannel channel, String name) { + public DiscordSender(User user, MessageChannel channel, String name) { super(user, channel); this.name = name; } diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java b/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java index 6d4098b..103a350 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java @@ -1,20 +1,20 @@ package buttondevteam.discordplugin; import buttondevteam.lib.TBMCCoreAPI; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.User; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.scheduler.BukkitTask; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IUser; public abstract class DiscordSenderBase implements CommandSender { /** * May be null. */ - protected IUser user; - protected IChannel channel; + protected User user; + protected MessageChannel channel; - protected DiscordSenderBase(IUser user, IChannel channel) { + protected DiscordSenderBase(User user, MessageChannel channel) { this.user = user; this.channel = channel; } @@ -27,11 +27,11 @@ public abstract class DiscordSenderBase implements CommandSender { * * @return The user or null. */ - public IUser getUser() { + public User getUser() { return user; } - public IChannel getChannel() { + public MessageChannel getChannel() { return channel; } @@ -43,7 +43,7 @@ public abstract class DiscordSenderBase implements CommandSender { * @return A Chroma user of Discord or a Discord user of Chroma */ public DiscordPlayer getChromaUser() { - if (chromaUser == null) chromaUser = DiscordPlayer.getUser(user.getStringID(), DiscordPlayer.class); + if (chromaUser == null) chromaUser = DiscordPlayer.getUser(user.getId().asString(), DiscordPlayer.class); return chromaUser; } @@ -58,8 +58,7 @@ public abstract class DiscordSenderBase implements CommandSender { msgtosend += "\n" + sendmsg; if (sendtask == null) sendtask = Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> { - DiscordPlugin.sendMessageToChannel(channel, - (!broadcast && user != null ? user.mention() + "\n" : "") + msgtosend.trim()); + channel.createMessage((!broadcast && user != null ? user.getMention() + "\n" : "") + msgtosend.trim()).subscribe(); sendtask = null; msgtosend = ""; }, 4); // Waits a 0.2 second to gather all/most of the different messages diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSupplier.java b/src/main/java/buttondevteam/discordplugin/DiscordSupplier.java deleted file mode 100755 index e2fb570..0000000 --- a/src/main/java/buttondevteam/discordplugin/DiscordSupplier.java +++ /dev/null @@ -1,11 +0,0 @@ -package buttondevteam.discordplugin; - -import sx.blah.discord.handle.obj.IDiscordObject; -import sx.blah.discord.util.DiscordException; -import sx.blah.discord.util.MissingPermissionsException; -import sx.blah.discord.util.RateLimitException; - -@FunctionalInterface -public interface DiscordSupplier> { - public abstract T get() throws DiscordException, RateLimitException, MissingPermissionsException; -} diff --git a/src/main/java/buttondevteam/discordplugin/announcer/AnnouncerModule.java b/src/main/java/buttondevteam/discordplugin/announcer/AnnouncerModule.java index c434dce..11250a8 100644 --- a/src/main/java/buttondevteam/discordplugin/announcer/AnnouncerModule.java +++ b/src/main/java/buttondevteam/discordplugin/announcer/AnnouncerModule.java @@ -6,40 +6,48 @@ import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.ConfigData; +import buttondevteam.lib.architecture.ReadOnlyConfigData; import buttondevteam.lib.player.ChromaGamerBase; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import discord4j.core.object.entity.Message; +import discord4j.core.object.entity.MessageChannel; import lombok.val; import org.bukkit.configuration.file.YamlConfiguration; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IMessage; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import java.io.File; -import java.util.List; public class AnnouncerModule extends Component { - public ConfigData channel() { + /** + * Channel to post new posts. + */ + public ReadOnlyConfigData> channel() { return DPUtils.channelData(getConfig(), "channel", 239519012529111040L); } - public ConfigData modChannel() { + /** + * Channel where distinguished (moderator) posts go. + */ + public ReadOnlyConfigData> modChannel() { return DPUtils.channelData(getConfig(), "modChannel", 239519012529111040L); } /** - * Set to 0 or >50 to disable + * Automatically unpins all messages except the last few. Set to 0 or >50 to disable */ public ConfigData keepPinned() { return getConfig().getData("keepPinned", (short) 40); } - private ConfigData lastannouncementtime() { + private ConfigData lastAnnouncementTime() { return getConfig().getData("lastAnnouncementTime", 0L); } - private ConfigData lastseentime() { + private ConfigData lastSeenTime() { return getConfig().getData("lastSeenTime", 0L); } @@ -50,24 +58,15 @@ public class AnnouncerModule extends Component { protected void enable() { if (DPUtils.disableIfConfigError(this, channel(), modChannel())) return; stop = false; //If not the first time - DPUtils.performNoWait(() -> { - try { - val keepPinned = keepPinned().get(); - if (keepPinned == 0) return; - val channel = channel().get(); - List msgs = channel.getPinnedMessages(); - for (int i = msgs.size() - 1; i >= keepPinned; i--) { // Unpin all pinned messages except the newest 10 - channel.unpin(msgs.get(i)); - Thread.sleep(10); - } - } catch (InterruptedException ignore) { - } - }); + val keepPinned = keepPinned().get(); + if (keepPinned == 0) return; + Flux msgs = channel().get().flatMapMany(MessageChannel::getPinnedMessages); + msgs.subscribe(Message::unpin); val yc = YamlConfiguration.loadConfiguration(new File("plugins/DiscordPlugin", "config.yml")); //Name change - if (lastannouncementtime().get() == 0) //Load old data - lastannouncementtime().set(yc.getLong("lastannouncementtime")); - if (lastseentime().get() == 0) - lastseentime().set(yc.getLong("lastseentime")); + if (lastAnnouncementTime().get() == 0) //Load old data + lastAnnouncementTime().set(yc.getLong("lastannouncementtime")); + if (lastSeenTime().get() == 0) + lastSeenTime().set(yc.getLong("lastseentime")); new Thread(this::AnnouncementGetterThreadMethod).start(); } @@ -88,7 +87,7 @@ public class AnnouncerModule extends Component { .get("children").getAsJsonArray(); StringBuilder msgsb = new StringBuilder(); StringBuilder modmsgsb = new StringBuilder(); - long lastanntime = lastannouncementtime().get(); + long lastanntime = lastAnnouncementTime().get(); for (int i = json.size() - 1; i >= 0; i--) { JsonObject item = json.get(i).getAsJsonObject(); final JsonObject data = item.get("data").getAsJsonObject(); @@ -101,9 +100,9 @@ public class AnnouncerModule extends Component { distinguished = distinguishedjson.getAsString(); String permalink = "https://www.reddit.com" + data.get("permalink").getAsString(); long date = data.get("created_utc").getAsLong(); - if (date > lastseentime().get()) - lastseentime().set(date); - else if (date > lastannouncementtime().get()) { + if (date > lastSeenTime().get()) + lastSeenTime().set(date); + else if (date > lastAnnouncementTime().get()) { do { val reddituserclass = ChromaGamerBase.getTypeForFolder("reddit"); if (reddituserclass == null) @@ -122,13 +121,13 @@ public class AnnouncerModule extends Component { } } if (msgsb.length() > 0) - channel().get().pin(DiscordPlugin.sendMessageToChannelWait(channel().get(), msgsb.toString())); + channel().get().flatMap(ch -> ch.createMessage(msgsb.toString())) + .flatMap(Message::pin).subscribe(); if (modmsgsb.length() > 0) - DiscordPlugin.sendMessageToChannel(modChannel().get(), modmsgsb.toString()); - if (lastannouncementtime().get() != lastanntime) { - lastannouncementtime().set(lastanntime); // If sending succeeded - getPlugin().saveConfig(); //TODO: Won't be needed if I implement auto-saving - } + modChannel().get().flatMap(ch -> ch.createMessage(modmsgsb.toString())) + .flatMap(Message::pin).subscribe(); + if (lastAnnouncementTime().get() != lastanntime) + lastAnnouncementTime().set(lastanntime); // If sending succeeded } catch (Exception e) { e.printStackTrace(); } diff --git a/src/main/java/buttondevteam/discordplugin/commands/Command2DC.java b/src/main/java/buttondevteam/discordplugin/commands/Command2DC.java index a0b8fda..ab56eb8 100644 --- a/src/main/java/buttondevteam/discordplugin/commands/Command2DC.java +++ b/src/main/java/buttondevteam/discordplugin/commands/Command2DC.java @@ -3,6 +3,8 @@ package buttondevteam.discordplugin.commands; import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.lib.chat.Command2; +import java.lang.reflect.Method; + public class Command2DC extends Command2 { @Override public void registerCommand(ICommand2DC command) { @@ -10,8 +12,8 @@ public class Command2DC extends Command2 { } @Override - public boolean hasPermission(Command2DCSender sender, ICommand2DC command) { - //return !command.isModOnly() || sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.ModRole().get()); //TODO: ModRole may be null; more customisable way? + public boolean hasPermission(Command2DCSender sender, ICommand2DC command, Method method) { + //return !command.isModOnly() || sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.modRole().get()); //TODO: modRole may be null; more customisable way? return true; } } diff --git a/src/main/java/buttondevteam/discordplugin/commands/Command2DCSender.java b/src/main/java/buttondevteam/discordplugin/commands/Command2DCSender.java index bdff52d..8b705fd 100644 --- a/src/main/java/buttondevteam/discordplugin/commands/Command2DCSender.java +++ b/src/main/java/buttondevteam/discordplugin/commands/Command2DCSender.java @@ -2,20 +2,28 @@ package buttondevteam.discordplugin.commands; import buttondevteam.discordplugin.DPUtils; import buttondevteam.lib.chat.Command2Sender; +import discord4j.core.object.entity.Message; import lombok.Getter; import lombok.RequiredArgsConstructor; -import sx.blah.discord.handle.obj.IMessage; +import lombok.val; @RequiredArgsConstructor public class Command2DCSender implements Command2Sender { - private final @Getter IMessage message; + private final @Getter + Message message; @Override public void sendMessage(String message) { if (message.length() == 0) return; message = DPUtils.sanitizeString(message); message = Character.toLowerCase(message.charAt(0)) + message.substring(1); - this.message.reply(message); + val msg = message; + /*this.message.getAuthorAsMember().flatMap(author -> + this.message.getChannel().flatMap(ch -> + ch.createMessage(author.getNicknameMention() + ", " + msg))).subscribe();*/ + this.message.getChannel().flatMap(ch -> + ch.createMessage(this.message.getAuthor().map(u -> DPUtils.nickMention(u.getId()) + ", ").orElse("") + + msg)).subscribe(); } @Override diff --git a/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java b/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java index 4e37e35..afa4cfb 100755 --- a/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java +++ b/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java @@ -1,7 +1,6 @@ package buttondevteam.discordplugin.commands; import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.CommandClass; @@ -28,34 +27,37 @@ public class ConnectCommand extends ICommand2DC { @Command2.Subcommand public boolean def(Command2DCSender sender, String Minecraftname) { val message = sender.getMessage(); - if (WaitingToConnect.inverse().containsKey(message.getAuthor().getStringID())) { - DiscordPlugin.sendMessageToChannel(message.getChannel(), - "Replacing " + WaitingToConnect.inverse().get(message.getAuthor().getStringID()) + " with " + Minecraftname); - WaitingToConnect.inverse().remove(message.getAuthor().getStringID()); + val channel = message.getChannel().block(); + val author = message.getAuthor().orElse(null); + if (author == null || channel == null) return true; + if (WaitingToConnect.inverse().containsKey(author.getId().asString())) { + channel.createMessage( + "Replacing " + WaitingToConnect.inverse().get(author.getId().asString()) + " with " + Minecraftname).subscribe(); + WaitingToConnect.inverse().remove(author.getId().asString()); } @SuppressWarnings("deprecation") OfflinePlayer p = Bukkit.getOfflinePlayer(Minecraftname); if (p == null) { - DiscordPlugin.sendMessageToChannel(message.getChannel(), "The specified Minecraft player cannot be found"); + channel.createMessage("The specified Minecraft player cannot be found").subscribe(); return true; } try (TBMCPlayer pl = TBMCPlayerBase.getPlayer(p.getUniqueId(), TBMCPlayer.class)) { DiscordPlayer dp = pl.getAs(DiscordPlayer.class); - if (dp != null && message.getAuthor().getStringID().equals(dp.getDiscordID())) { - DiscordPlugin.sendMessageToChannel(message.getChannel(), "You already have this account connected."); + if (dp != null && author.getId().asString().equals(dp.getDiscordID())) { + channel.createMessage("You already have this account connected.").subscribe(); return true; } } catch (Exception e) { TBMCCoreAPI.SendException("An error occured while connecting a Discord account!", e); - DiscordPlugin.sendMessageToChannel(message.getChannel(), "An internal error occured!\n" + e); + channel.createMessage("An internal error occured!\n" + e).subscribe(); } - WaitingToConnect.put(p.getName(), message.getAuthor().getStringID()); - DiscordPlugin.sendMessageToChannel(message.getChannel(), + WaitingToConnect.put(p.getName(), author.getId().asString()); + channel.createMessage( "Alright! Now accept the connection in Minecraft from the account " + Minecraftname - + " before the next server restart. You can also adjust the Minecraft name you want to connect to with the same command."); + + " before the next server restart. You can also adjust the Minecraft name you want to connect to with the same command.").subscribe(); if (p.isOnline()) - ((Player) p).sendMessage("§bTo connect with the Discord account " + message.getAuthor().getName() + "#" - + message.getAuthor().getDiscriminator() + " do /discord accept"); + ((Player) p).sendMessage("§bTo connect with the Discord account " + author.getUsername() + "#" + + author.getDiscriminator() + " do /discord accept"); return true; } diff --git a/src/main/java/buttondevteam/discordplugin/commands/DebugCommand.java b/src/main/java/buttondevteam/discordplugin/commands/DebugCommand.java index 2cf87fb..e1c0686 100644 --- a/src/main/java/buttondevteam/discordplugin/commands/DebugCommand.java +++ b/src/main/java/buttondevteam/discordplugin/commands/DebugCommand.java @@ -4,17 +4,27 @@ import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.listeners.CommonListeners; import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.CommandClass; +import reactor.core.publisher.Mono; @CommandClass(helpText = { "Switches debug mode." }) public class DebugCommand extends ICommand2DC { @Command2.Subcommand - public boolean def(Command2DCSender sender, String args) { - if (sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.ModRole().get())) - sender.sendMessage("debug " + (CommonListeners.debug() ? "enabled" : "disabled")); - else - sender.sendMessage("you need to be a moderator to use this command."); - return true; - } + public boolean def(Command2DCSender sender) { + sender.getMessage().getAuthorAsMember() + .switchIfEmpty(sender.getMessage().getAuthor() //Support DMs + .map(u -> u.asMember(DiscordPlugin.mainServer.getId())) + .orElse(Mono.empty())) + .flatMap(m -> DiscordPlugin.plugin.modRole().get() + .map(mr -> m.getRoleIds().stream().anyMatch(r -> r.equals(mr.getId()))) + .switchIfEmpty(Mono.fromSupplier(() -> DiscordPlugin.mainServer.getOwnerId().asLong() == m.getId().asLong()))) //Role not found + .subscribe(success -> { + if (success) + sender.sendMessage("debug " + (CommonListeners.debug() ? "enabled" : "disabled")); + else + sender.sendMessage("you need to be a moderator to use this command."); + }); + return true; + } } diff --git a/src/main/java/buttondevteam/discordplugin/commands/HelpCommand.java b/src/main/java/buttondevteam/discordplugin/commands/HelpCommand.java index 0194dab..546d4ee 100755 --- a/src/main/java/buttondevteam/discordplugin/commands/HelpCommand.java +++ b/src/main/java/buttondevteam/discordplugin/commands/HelpCommand.java @@ -1,5 +1,6 @@ package buttondevteam.discordplugin.commands; +import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.CommandClass; @CommandClass(helpText = { @@ -7,12 +8,17 @@ import buttondevteam.lib.chat.CommandClass; "Shows some info about a command or lists the available commands.", // }) public class HelpCommand extends ICommand2DC { - @Override - public boolean def(Command2DCSender sender, String args) { - if (args.length() == 0) + @Command2.Subcommand + public boolean def(Command2DCSender sender, @Command2.TextArg @Command2.OptionalArg String args) { + if (args == null || args.length() == 0) sender.sendMessage(getManager().getCommandsText()); - else - sender.sendMessage("Soon:tm:"); //TODO - return true; + else { + String[] ht = getManager().getHelpText(args); + if (ht == null) + sender.sendMessage("Command not found: " + args); + else + sender.sendMessage(ht); + } + return true; } } diff --git a/src/main/java/buttondevteam/discordplugin/commands/UserinfoCommand.java b/src/main/java/buttondevteam/discordplugin/commands/UserinfoCommand.java index 0433c81..40f98cb 100755 --- a/src/main/java/buttondevteam/discordplugin/commands/UserinfoCommand.java +++ b/src/main/java/buttondevteam/discordplugin/commands/UserinfoCommand.java @@ -7,13 +7,11 @@ import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.player.ChromaGamerBase; import buttondevteam.lib.player.ChromaGamerBase.InfoTarget; +import discord4j.core.object.entity.Message; +import discord4j.core.object.entity.User; import lombok.val; -import sx.blah.discord.handle.obj.IMessage; -import sx.blah.discord.handle.obj.IUser; import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; @CommandClass(helpText = { "User information", // @@ -24,67 +22,71 @@ public class UserinfoCommand extends ICommand2DC { @Command2.Subcommand public boolean def(Command2DCSender sender, @Command2.OptionalArg @Command2.TextArg String user) { val message = sender.getMessage(); - IUser target = null; + User target = null; + val channel = message.getChannel().block(); + assert channel != null; if (user == null || user.length() == 0) - target = message.getAuthor(); + target = message.getAuthor().orElse(null); else { - final Optional firstmention = message.getMentions().stream() - .filter(m -> !m.getStringID().equals(DiscordPlugin.dc.getOurUser().getStringID())).findFirst(); - if (firstmention.isPresent()) - target = firstmention.get(); + @SuppressWarnings("OptionalGetWithoutIsPresent") final User firstmention = message.getUserMentions() + .filter(m -> !m.getId().asString().equals(DiscordPlugin.dc.getSelfId().get().asString())).blockFirst(); + if (firstmention != null) + target = firstmention; else if (user.contains("#")) { String[] targettag = user.split("#"); - final List targets = getUsers(message, targettag[0]); + final List targets = getUsers(message, targettag[0]); if (targets.size() == 0) { - DiscordPlugin.sendMessageToChannel(message.getChannel(), - "The user cannot be found (by name): " + user); - return true; + channel.createMessage("The user cannot be found (by name): " + user).subscribe(); + return true; } - for (IUser ptarget : targets) { + for (User ptarget : targets) { if (ptarget.getDiscriminator().equalsIgnoreCase(targettag[1])) { target = ptarget; break; } } if (target == null) { - DiscordPlugin.sendMessageToChannel(message.getChannel(), - "The user cannot be found (by discriminator): " + user + "(Found " + targets.size() - + " users with the name.)"); - return true; + channel.createMessage("The user cannot be found (by discriminator): " + user + "(Found " + targets.size() + + " users with the name.)").subscribe(); + return true; } } else { - final List targets = getUsers(message, user); + final List targets = getUsers(message, user); if (targets.size() == 0) { - DiscordPlugin.sendMessageToChannel(message.getChannel(), - "The user cannot be found on Discord: " + user); - return true; + channel.createMessage("The user cannot be found on Discord: " + user).subscribe(); + return true; } if (targets.size() > 1) { - DiscordPlugin.sendMessageToChannel(message.getChannel(), - "Multiple users found with that (nick)name. Please specify the whole tag, like ChromaBot#6338 or use a ping."); - return true; + channel.createMessage("Multiple users found with that (nick)name. Please specify the whole tag, like ChromaBot#6338 or use a ping.").subscribe(); + return true; } target = targets.get(0); } } - try (DiscordPlayer dp = ChromaGamerBase.getUser(target.getStringID(), DiscordPlayer.class)) { - StringBuilder uinfo = new StringBuilder("User info for ").append(target.getName()).append(":\n"); - uinfo.append(dp.getInfo(InfoTarget.Discord)); - DiscordPlugin.sendMessageToChannel(message.getChannel(), uinfo.toString()); - } catch (Exception e) { - DiscordPlugin.sendMessageToChannel(message.getChannel(), "An error occured while getting the user!"); - TBMCCoreAPI.SendException("Error while getting info about " + target.getName() + "!", e); + if (target == null) { + sender.sendMessage("An error occurred."); + return true; } - return true; + try (DiscordPlayer dp = ChromaGamerBase.getUser(target.getId().asString(), DiscordPlayer.class)) { + StringBuilder uinfo = new StringBuilder("User info for ").append(target.getUsername()).append(":\n"); + uinfo.append(dp.getInfo(InfoTarget.Discord)); + channel.createMessage(uinfo.toString()).subscribe(); + } catch (Exception e) { + channel.createMessage("An error occured while getting the user!").subscribe(); + TBMCCoreAPI.SendException("Error while getting info about " + target.getUsername() + "!", e); + } + return true; } - private List getUsers(IMessage message, String args) { - final List targets; - if (message.getChannel().isPrivate()) - targets = DiscordPlugin.dc.getUsers().stream().filter(u -> u.getName().equalsIgnoreCase(args)) - .collect(Collectors.toList()); + private List getUsers(Message message, String args) { + final List targets; + val guild = message.getGuild().block(); + if (guild == null) //Private channel + targets = DiscordPlugin.dc.getUsers().filter(u -> u.getUsername().equalsIgnoreCase(args)) + .collectList().block(); else - targets = message.getGuild().getUsersByName(args, true); + targets = guild.getMembers().filter(m -> m.getUsername().equalsIgnoreCase(args)) + .map(m -> (User) m).collectList().block(); return targets; } diff --git a/src/main/java/buttondevteam/discordplugin/exceptions/DebugMessageListener.java b/src/main/java/buttondevteam/discordplugin/exceptions/DebugMessageListener.java index 95c3cdb..0f05934 100755 --- a/src/main/java/buttondevteam/discordplugin/exceptions/DebugMessageListener.java +++ b/src/main/java/buttondevteam/discordplugin/exceptions/DebugMessageListener.java @@ -3,10 +3,12 @@ package buttondevteam.discordplugin.exceptions; import buttondevteam.core.ComponentManager; import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.lib.TBMCDebugMessageEvent; +import discord4j.core.object.entity.MessageChannel; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import reactor.core.publisher.Mono; -public class DebugMessageListener implements Listener{ +public class DebugMessageListener implements Listener { @EventHandler public void onDebugMessage(TBMCDebugMessageEvent e) { SendMessage(e.getDebugMessage()); @@ -17,13 +19,15 @@ public class DebugMessageListener implements Listener{ if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(ExceptionListenerModule.class)) return; try { + Mono mc = ExceptionListenerModule.getChannel(); + if (mc == null) return; StringBuilder sb = new StringBuilder(); sb.append("```").append("\n"); if (message.length() > 2000) message = message.substring(0, 2000); sb.append(message).append("\n"); sb.append("```"); - DiscordPlugin.sendMessageToChannel(ExceptionListenerModule.getChannel(), sb.toString()); + mc.flatMap(ch -> ch.createMessage(sb.toString())).subscribe(); } catch (Exception ex) { ex.printStackTrace(); } diff --git a/src/main/java/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.java b/src/main/java/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.java index 5d21bb1..d57c313 100755 --- a/src/main/java/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.java +++ b/src/main/java/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.java @@ -7,13 +7,16 @@ import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCExceptionEvent; import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.ConfigData; +import buttondevteam.lib.architecture.ReadOnlyConfigData; +import discord4j.core.object.entity.Guild; +import discord4j.core.object.entity.GuildChannel; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.Role; import org.apache.commons.lang.exception.ExceptionUtils; import org.bukkit.Bukkit; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IGuild; -import sx.blah.discord.handle.obj.IRole; +import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.Arrays; @@ -21,64 +24,71 @@ import java.util.List; import java.util.stream.Collectors; public class ExceptionListenerModule extends Component implements Listener { - private List lastthrown = new ArrayList<>(); - private List lastsourcemsg = new ArrayList<>(); + private List lastthrown = new ArrayList<>(); + private List lastsourcemsg = new ArrayList<>(); - @EventHandler - public void onException(TBMCExceptionEvent e) { - if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(getClass())) - return; - if (lastthrown.stream() - .anyMatch(ex -> Arrays.equals(e.getException().getStackTrace(), ex.getStackTrace()) - && (e.getException().getMessage() == null ? ex.getMessage() == null - : e.getException().getMessage().equals(ex.getMessage()))) // e.Exception.Message==ex.Message - && lastsourcemsg.contains(e.getSourceMessage())) - return; - SendException(e.getException(), e.getSourceMessage()); - if (lastthrown.size() >= 10) - lastthrown.remove(0); - if (lastsourcemsg.size() >= 10) - lastsourcemsg.remove(0); - lastthrown.add(e.getException()); - lastsourcemsg.add(e.getSourceMessage()); - e.setHandled(); - } + @EventHandler + public void onException(TBMCExceptionEvent e) { + if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(getClass())) + return; + if (lastthrown.stream() + .anyMatch(ex -> Arrays.equals(e.getException().getStackTrace(), ex.getStackTrace()) + && (e.getException().getMessage() == null ? ex.getMessage() == null + : e.getException().getMessage().equals(ex.getMessage()))) // e.Exception.Message==ex.Message + && lastsourcemsg.contains(e.getSourceMessage())) + return; + SendException(e.getException(), e.getSourceMessage()); + if (lastthrown.size() >= 10) + lastthrown.remove(0); + if (lastsourcemsg.size() >= 10) + lastsourcemsg.remove(0); + lastthrown.add(e.getException()); + lastsourcemsg.add(e.getSourceMessage()); + e.setHandled(); + } - private static void SendException(Throwable e, String sourcemessage) { + private static void SendException(Throwable e, String sourcemessage) { if (instance == null) return; - try { - IChannel channel = getChannel(); - assert channel != null; - IRole coderRole = instance.pingRole(channel.getGuild()).get(); - StringBuilder sb = TBMCCoreAPI.IsTestServer() ? new StringBuilder() - : new StringBuilder(coderRole == null ? "" : coderRole.mention()).append("\n"); - sb.append(sourcemessage).append("\n"); - sb.append("```").append("\n"); - String stackTrace = Arrays.stream(ExceptionUtils.getStackTrace(e).split("\\n")) - .filter(s -> !s.contains("\tat ") || s.contains("\tat buttondevteam.")) - .collect(Collectors.joining("\n")); - if (stackTrace.length() > 1800) - stackTrace = stackTrace.substring(0, 1800); - sb.append(stackTrace).append("\n"); - sb.append("```"); - DiscordPlugin.sendMessageToChannel(channel, sb.toString()); //Instance isn't null here - } catch (Exception ex) { - ex.printStackTrace(); - } - } + try { + Mono channel = getChannel(); + assert channel != null; + Mono coderRole; + if (channel instanceof GuildChannel) + coderRole = instance.pingRole(((GuildChannel) channel).getGuild()).get(); + else + coderRole = Mono.empty(); + coderRole.map(role -> TBMCCoreAPI.IsTestServer() ? new StringBuilder() + : new StringBuilder(role.getMention()).append("\n")) + .defaultIfEmpty(new StringBuilder()) + .flatMap(sb -> { + sb.append(sourcemessage).append("\n"); + sb.append("```").append("\n"); + String stackTrace = Arrays.stream(ExceptionUtils.getStackTrace(e).split("\\n")) + .filter(s -> !s.contains("\tat ") || s.contains("\tat buttondevteam.")) + .collect(Collectors.joining("\n")); + if (sb.length() + stackTrace.length() >= 1980) + stackTrace = stackTrace.substring(0, 1980 - sb.length()); + sb.append(stackTrace).append("\n"); + sb.append("```"); + return channel.flatMap(ch -> ch.createMessage(sb.toString())); + }).subscribe(); + } catch (Exception ex) { + ex.printStackTrace(); + } + } private static ExceptionListenerModule instance; - public static IChannel getChannel() { + public static Mono getChannel() { if (instance != null) return instance.channel().get(); - return null; + return Mono.empty(); } - private ConfigData channel() { + private ReadOnlyConfigData> channel() { return DPUtils.channelData(getConfig(), "channel", 239519012529111040L); } - private ConfigData pingRole(IGuild guild) { + private ConfigData> pingRole(Mono guild) { return DPUtils.roleData(getConfig(), "pingRole", "Coder", guild); } diff --git a/src/main/java/buttondevteam/discordplugin/fun/FunModule.java b/src/main/java/buttondevteam/discordplugin/fun/FunModule.java index f5adf0d..fa5e7d3 100644 --- a/src/main/java/buttondevteam/discordplugin/fun/FunModule.java +++ b/src/main/java/buttondevteam/discordplugin/fun/FunModule.java @@ -6,15 +6,17 @@ import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.ConfigData; +import buttondevteam.lib.architecture.ReadOnlyConfigData; import com.google.common.collect.Lists; +import discord4j.core.event.domain.PresenceUpdateEvent; +import discord4j.core.object.entity.*; +import discord4j.core.object.presence.Status; import lombok.val; import org.bukkit.Bukkit; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; -import sx.blah.discord.handle.impl.events.user.PresenceUpdateEvent; -import sx.blah.discord.handle.obj.*; -import sx.blah.discord.util.EmbedBuilder; +import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.Arrays; @@ -23,36 +25,43 @@ import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; +/** + * The YEEHAW event uses an emoji named :YEEHAW: if available + */ public class FunModule extends Component implements Listener { private static final String[] serverReadyStrings = new String[]{"In one week from now", // Ali - "Between now and the heat-death of the universe.", // Ghostise - "Soon™", "Ask again this time next month", // Ghostise - "In about 3 seconds", // Nicolai - "After we finish 8 plugins", // Ali - "Tomorrow.", // Ali - "After one tiiiny feature", // Ali - "Next commit", // Ali - "After we finish strangling Towny", // Ali - "When we kill every *fucking* bug", // Ali - "Once the server stops screaming.", // Ali - "After HL3 comes out", // Ali - "Next time you ask", // Ali - "When will *you* be open?" // Ali + "Between now and the heat-death of the universe.", // Ghostise + "Soon™", "Ask again this time next month", // Ghostise + "In about 3 seconds", // Nicolai + "After we finish 8 plugins", // Ali + "Tomorrow.", // Ali + "After one tiiiny feature", // Ali + "Next commit", // Ali + "After we finish strangling Towny", // Ali + "When we kill every *fucking* bug", // Ali + "Once the server stops screaming.", // Ali + "After HL3 comes out", // Ali + "Next time you ask", // Ali + "When will *you* be open?" // Ali }; - private ConfigData serverReady() { - return getConfig().getData("serverReady", true); + /** + * Questions that the bot will choose a random answer to give to. + */ + private ConfigData serverReady() { + return getConfig().getData("serverReady", () -> new String[]{"when will the server be open", + "when will the server be ready", "when will the server be done", "when will the server be complete", + "when will the server be finished", "when's the server ready", "when's the server open", + "Vhen vill ze server be open?"}); } + /** + * Answers for a recognized question. Selected randomly. + */ private ConfigData> serverReadyAnswers() { return getConfig().getData("serverReadyAnswers", () -> Lists.newArrayList(serverReadyStrings)); //TODO: Test } - private static final String[] serverReadyQuestions = new String[]{"when will the server be open", - "when will the server be ready", "when will the server be done", "when will the server be complete", - "when will the server be finished", "when's the server ready", "when's the server open", - "Vhen vill ze server be open?"}; - private static final Random serverReadyRandom = new Random(); private static final ArrayList usableServerReadyStrings = new ArrayList<>(0); @@ -76,10 +85,10 @@ public class FunModule extends Component implements Listener { private static short ListC = 0; - public static boolean executeMemes(IMessage message) { + public static boolean executeMemes(Message message) { val fm = ComponentManager.getIfEnabled(FunModule.class); if (fm == null) return false; - String msglowercased = message.getContent().toLowerCase(); + String msglowercased = message.getContent().orElse("").toLowerCase(); lastlist++; if (lastlist > 5) { ListC = 0; @@ -87,22 +96,20 @@ public class FunModule extends Component implements Listener { } if (msglowercased.equals("list") && Bukkit.getOnlinePlayers().size() == lastlistp && ListC++ > 2) // Lowered already { - message.reply("Stop it. You know the answer."); + DPUtils.reply(message, null, "Stop it. You know the answer.").subscribe(); lastlist = 0; lastlistp = (short) Bukkit.getOnlinePlayers().size(); return true; //Handled } lastlistp = (short) Bukkit.getOnlinePlayers().size(); //Didn't handle - if (fm.serverReady().get()) { - if (!TBMCCoreAPI.IsTestServer() - && Arrays.stream(serverReadyQuestions).anyMatch(msglowercased::contains)) { - int next; - if (usableServerReadyStrings.size() == 0) - fm.createUsableServerReadyStrings(); - next = usableServerReadyStrings.remove(serverReadyRandom.nextInt(usableServerReadyStrings.size())); - DiscordPlugin.sendMessageToChannel(message.getChannel(), serverReadyStrings[next]); - return false; //Still process it as a command/mcchat if needed - } + if (!TBMCCoreAPI.IsTestServer() + && Arrays.stream(fm.serverReady().get()).anyMatch(msglowercased::contains)) { + int next; + if (usableServerReadyStrings.size() == 0) + fm.createUsableServerReadyStrings(); + next = usableServerReadyStrings.remove(serverReadyRandom.nextInt(usableServerReadyStrings.size())); + DPUtils.reply(message, null, fm.serverReadyAnswers().get().get(next)).subscribe(); + return false; //Still process it as a command/mcchat if needed } return false; } @@ -112,12 +119,12 @@ public class FunModule extends Component implements Listener { ListC = 0; } - private ConfigData fullHouseDevRole(IGuild guild) { + private ConfigData> fullHouseDevRole(Mono guild) { return DPUtils.roleData(getConfig(), "fullHouseDevRole", "Developer", guild); } - private ConfigData fullHouseChannel() { + private ReadOnlyConfigData> fullHouseChannel() { return DPUtils.channelData(getConfig(), "fullHouseChannel", 219626707458457603L); } @@ -126,24 +133,24 @@ public class FunModule extends Component implements Listener { public static void handleFullHouse(PresenceUpdateEvent event) { val fm = ComponentManager.getIfEnabled(FunModule.class); if (fm == null) return; - val channel = fm.fullHouseChannel().get(); - if (channel == null) return; - val devrole = fm.fullHouseDevRole(channel.getGuild()).get(); - if (devrole == null) return; - if (event.getOldPresence().getStatus().equals(StatusType.OFFLINE) - && !event.getNewPresence().getStatus().equals(StatusType.OFFLINE) - && event.getUser().getRolesForGuild(channel.getGuild()).stream() - .anyMatch(r -> r.getLongID() == devrole.getLongID()) - && channel.getGuild().getUsersByRole(devrole).stream() - .noneMatch(u -> u.getPresence().getStatus().equals(StatusType.OFFLINE)) - && lasttime + 10 < TimeUnit.NANOSECONDS.toHours(System.nanoTime()) - && Calendar.getInstance().get(Calendar.DAY_OF_MONTH) % 5 == 0) { - DiscordPlugin.sendMessageToChannel(channel, "Full house!", - new EmbedBuilder() - .withImage( - "https://cdn.discordapp.com/attachments/249295547263877121/249687682618359808/poker-hand-full-house-aces-kings-playing-cards-15553791.png") - .build()); - lasttime = TimeUnit.NANOSECONDS.toHours(System.nanoTime()); - } + if (Calendar.getInstance().get(Calendar.DAY_OF_MONTH) % 5 != 0) return; + fm.fullHouseChannel().get() + .filter(ch -> ch instanceof GuildChannel) + .flatMap(channel -> fm.fullHouseDevRole(((GuildChannel) channel).getGuild()).get() + .filter(role -> event.getOld().map(p -> p.getStatus().equals(Status.OFFLINE)).orElse(false)) + .filter(role -> !event.getCurrent().getStatus().equals(Status.OFFLINE)) + .filterWhen(devrole -> event.getMember().flatMap(m -> m.getRoles() + .any(r -> r.getId().asLong() == devrole.getId().asLong()))) + .filterWhen(devrole -> + event.getGuild().flatMapMany(g -> g.getMembers().filter(m -> m.getRoleIds().stream().anyMatch(s -> s.equals(devrole.getId())))) + .flatMap(Member::getPresence).all(pr -> !pr.getStatus().equals(Status.OFFLINE))) + .filter(devrole -> lasttime + 10 < TimeUnit.NANOSECONDS.toHours(System.nanoTime())) //This should stay so it checks this last + .flatMap(devrole -> { + lasttime = TimeUnit.NANOSECONDS.toHours(System.nanoTime()); + return channel.createMessage(mcs -> mcs.setContent("Full house!").setEmbed(ecs -> + ecs.setImage( + "https://cdn.discordapp.com/attachments/249295547263877121/249687682618359808/poker-hand-full-house-aces-kings-playing-cards-15553791.png") + )); + })).subscribe(); } } diff --git a/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java b/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java index 4f99d35..45aa261 100644 --- a/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java +++ b/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java @@ -1,11 +1,18 @@ package buttondevteam.discordplugin.listeners; +import buttondevteam.discordplugin.DPUtils; import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.commands.Command2DCSender; +import buttondevteam.discordplugin.util.Timings; import buttondevteam.lib.TBMCCoreAPI; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IMessage; -import sx.blah.discord.handle.obj.IRole; +import discord4j.core.object.entity.Message; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.PrivateChannel; +import discord4j.core.object.entity.Role; +import lombok.val; +import reactor.core.publisher.Mono; + +import java.util.concurrent.atomic.AtomicBoolean; public class CommandListener { /** @@ -13,44 +20,61 @@ public class CommandListener { * * @param message The Discord message * @param mentionedonly Only run the command if ChromaBot is mentioned at the start of the message - * @return Whether it ran the command + * @return Whether it did not run the command */ - public static boolean runCommand(IMessage message, boolean mentionedonly) { - if (message.getContent().length() == 0) - return false; //Pin messages and such, let the mcchat listener deal with it - final IChannel channel = message.getChannel(); - if (!mentionedonly) { //mentionedonly conditions are in CommonListeners - if (!message.getChannel().isPrivate() - && !(message.getContent().charAt(0) == DiscordPlugin.getPrefix() - && channel.getStringID().equals(DiscordPlugin.plugin.CommandChannel().get().getStringID()))) // - return false; - message.getChannel().setTypingStatus(true); // Fun - } - final StringBuilder cmdwithargs = new StringBuilder(message.getContent()); - final String mention = DiscordPlugin.dc.getOurUser().mention(false); - final String mentionNick = DiscordPlugin.dc.getOurUser().mention(true); - boolean gotmention = checkanddeletemention(cmdwithargs, mention, message); - gotmention = checkanddeletemention(cmdwithargs, mentionNick, message) || gotmention; - for (String mentionRole : (Iterable) message.getRoleMentions().stream().filter(r -> DiscordPlugin.dc.getOurUser().hasRole(r)).map(IRole::mention)::iterator) - gotmention = checkanddeletemention(cmdwithargs, mentionRole, message) || gotmention; // Delete all mentions - if (mentionedonly && !gotmention) { - message.getChannel().setTypingStatus(false); - return false; - } - message.getChannel().setTypingStatus(true); - String cmdwithargsString = cmdwithargs.toString(); - try { - if (!DiscordPlugin.plugin.getManager().handleCommand(new Command2DCSender(message), cmdwithargsString)) - message.reply("Unknown command. Do " + DiscordPlugin.getPrefix() + "help for help.\n" + cmdwithargsString); - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e); - } - message.getChannel().setTypingStatus(false); - return true; + public static Mono runCommand(Message message, MessageChannel commandChannel, boolean mentionedonly) { + Timings timings = CommonListeners.timings; + Mono ret = Mono.just(true); + if (!message.getContent().isPresent()) + return ret; //Pin messages and such, let the mcchat listener deal with it + val content = message.getContent().get(); + timings.printElapsed("A"); + return message.getChannel().flatMap(channel -> { + Mono tmp = ret; + if (!mentionedonly) { //mentionedonly conditions are in CommonListeners + timings.printElapsed("B"); + if (!(channel instanceof PrivateChannel) + && !(content.charAt(0) == DiscordPlugin.getPrefix() + && channel.getId().asLong() == commandChannel.getId().asLong())) // + return ret; + timings.printElapsed("C"); + tmp = ret.then(channel.type()).thenReturn(true); // Fun (this true is ignored - x) + } + final StringBuilder cmdwithargs = new StringBuilder(content); + val gotmention = new AtomicBoolean(); + timings.printElapsed("Before self"); + return tmp.flatMapMany(x -> + DiscordPlugin.dc.getSelf().flatMap(self -> self.asMember(DiscordPlugin.mainServer.getId())) + .flatMapMany(self -> { + timings.printElapsed("D"); + gotmention.set(checkanddeletemention(cmdwithargs, self.getMention(), message)); + gotmention.set(checkanddeletemention(cmdwithargs, self.getNicknameMention(), message) || gotmention.get()); + val mentions = message.getRoleMentions(); + return self.getRoles().filterWhen(r -> mentions.any(rr -> rr.getName().equals(r.getName()))) + .map(Role::getMention); + }).map(mentionRole -> { + timings.printElapsed("E"); + gotmention.set(checkanddeletemention(cmdwithargs, mentionRole, message) || gotmention.get()); // Delete all mentions + return !mentionedonly || gotmention.get(); //Stops here if false + }).switchIfEmpty(Mono.fromSupplier(() -> !mentionedonly || gotmention.get()))) + .filter(b -> b).last(false).filter(b -> b).doOnNext(b -> channel.type().subscribe()).flatMap(b -> { + String cmdwithargsString = cmdwithargs.toString(); + try { + timings.printElapsed("F"); + if (!DiscordPlugin.plugin.getManager().handleCommand(new Command2DCSender(message), cmdwithargsString)) + return DPUtils.reply(message, channel, "Unknown command. Do " + DiscordPlugin.getPrefix() + "help for help.\n" + cmdwithargsString) + .map(m -> false); + } catch (Exception e) { + TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e); + } + return Mono.just(false); //If the command succeeded or there was an error, return false + }).defaultIfEmpty(true); + }); } - private static boolean checkanddeletemention(StringBuilder cmdwithargs, String mention, IMessage message) { - if (message.getContent().startsWith(mention)) // TODO: Resolve mentions: Compound arguments, either a mention or text + private static boolean checkanddeletemention(StringBuilder cmdwithargs, String mention, Message message) { + final char prefix = DiscordPlugin.getPrefix(); + if (message.getContent().orElse("").startsWith(mention)) // TODO: Resolve mentions: Compound arguments, either a mention or text if (cmdwithargs.length() > mention.length() + 1) { int i = cmdwithargs.indexOf(" ", mention.length()); if (i == -1) @@ -60,14 +84,16 @@ public class CommandListener { for (; i < cmdwithargs.length() && cmdwithargs.charAt(i) == ' '; i++) ; //Removes any space before the command cmdwithargs.delete(0, i); - cmdwithargs.insert(0, DiscordPlugin.getPrefix()); //Always use the prefix for processing + cmdwithargs.insert(0, prefix); //Always use the prefix for processing } else - cmdwithargs.replace(0, cmdwithargs.length(), DiscordPlugin.getPrefix() + "help"); + cmdwithargs.replace(0, cmdwithargs.length(), prefix + "help"); else { + if (cmdwithargs.length() == 0) + cmdwithargs.replace(0, cmdwithargs.length(), prefix + "help"); + else if (cmdwithargs.charAt(0) != prefix) + cmdwithargs.insert(0, prefix); return false; //Don't treat / as mention, mentions can be used in public mcchat } - if (cmdwithargs.length() == 0) - cmdwithargs.replace(0, cmdwithargs.length(), DiscordPlugin.getPrefix() + "help"); return true; } } diff --git a/src/main/java/buttondevteam/discordplugin/listeners/CommonListeners.java b/src/main/java/buttondevteam/discordplugin/listeners/CommonListeners.java index 5350b32..510e71a 100755 --- a/src/main/java/buttondevteam/discordplugin/listeners/CommonListeners.java +++ b/src/main/java/buttondevteam/discordplugin/listeners/CommonListeners.java @@ -5,18 +5,23 @@ import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.fun.FunModule; import buttondevteam.discordplugin.mcchat.MinecraftChatModule; import buttondevteam.discordplugin.role.GameRoleModule; +import buttondevteam.discordplugin.util.Timings; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.architecture.Component; +import discord4j.core.event.EventDispatcher; +import discord4j.core.event.domain.PresenceUpdateEvent; +import discord4j.core.event.domain.message.MessageCreateEvent; +import discord4j.core.event.domain.role.RoleCreateEvent; +import discord4j.core.event.domain.role.RoleDeleteEvent; +import discord4j.core.event.domain.role.RoleUpdateEvent; +import discord4j.core.object.entity.PrivateChannel; import lombok.val; -import sx.blah.discord.api.events.IListener; -import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent; -import sx.blah.discord.handle.impl.events.guild.role.RoleCreateEvent; -import sx.blah.discord.handle.impl.events.guild.role.RoleDeleteEvent; -import sx.blah.discord.handle.impl.events.guild.role.RoleUpdateEvent; -import sx.blah.discord.handle.impl.events.user.PresenceUpdateEvent; +import reactor.core.publisher.Mono; public class CommonListeners { + public static final Timings timings = new Timings(); + /* MentionEvent: - CommandListener (starts with mention, only 'channelcon' and not in #bot) @@ -26,52 +31,59 @@ public class CommonListeners { - Minecraft chat (is enabled in the channel and message isn't [/]mcchat) - CommandListener (with the correct prefix in #bot, or in private) */ - public static IListener[] getListeners() { - return new IListener[]{new IListener() { - @Override - public void handle(MessageReceivedEvent event) { - if (DiscordPlugin.SafeMode) - return; - if (event.getMessage().getAuthor().isBot()) - return; - if (FunModule.executeMemes(event.getMessage())) - return; - try { - boolean handled = false; - val commandChannel = DiscordPlugin.plugin.CommandChannel().get(); - if ((commandChannel != null && event.getChannel().getLongID() == commandChannel.getLongID()) //If mentioned, that's higher than chat - || event.getMessage().getContent().contains("channelcon")) //Only 'channelcon' is allowed in other channels - handled = CommandListener.runCommand(event.getMessage(), true); //#bot is handled here - if (handled) return; - val mcchat = Component.getComponents().get(MinecraftChatModule.class); - if (mcchat != null && mcchat.isEnabled()) //ComponentManager.isEnabled() searches the component again - handled = ((MinecraftChatModule) mcchat).getListener().handleDiscord(event); //Also runs Discord commands in chat channels - if (!handled) - handled = CommandListener.runCommand(event.getMessage(), false); - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while handling a message!", e); - } - } - }, new IListener() { - @Override - public void handle(PresenceUpdateEvent event) { - if (DiscordPlugin.SafeMode) - return; - FunModule.handleFullHouse(event); - } - }, (IListener) GameRoleModule::handleRoleEvent, // - (IListener) GameRoleModule::handleRoleEvent, // - (IListener) GameRoleModule::handleRoleEvent}; + public static void register(EventDispatcher dispatcher) { + dispatcher.on(MessageCreateEvent.class).flatMap(event -> { + timings.printElapsed("Message received"); + val def = Mono.empty(); + if (DiscordPlugin.SafeMode) + return def; + val author = event.getMessage().getAuthor(); + if (!author.isPresent() || author.get().isBot()) + return def; + if (FunModule.executeMemes(event.getMessage())) + return def; + val commandChannel = DiscordPlugin.plugin.commandChannel().get(); + val commandCh = DPUtils.getMessageChannel(DiscordPlugin.plugin.commandChannel()); + return commandCh.filterWhen(ch -> event.getMessage().getChannel().map(mch -> + (commandChannel != null && mch.getId().asLong() == commandChannel.asLong()) //If mentioned, that's higher than chat + || mch instanceof PrivateChannel + || event.getMessage().getContent().orElse("").contains("channelcon")) //Only 'channelcon' is allowed in other channels + .flatMap(shouldRun -> { //Only continue if this doesn't handle the event + if (!shouldRun) + return Mono.just(true); //The condition is only for the first command execution, not mcchat + timings.printElapsed("Run command 1"); + return CommandListener.runCommand(event.getMessage(), ch, true); //#bot is handled here + })).filterWhen(ch -> { + timings.printElapsed("mcchat"); + val mcchat = Component.getComponents().get(MinecraftChatModule.class); + if (mcchat != null && mcchat.isEnabled()) //ComponentManager.isEnabled() searches the component again + return ((MinecraftChatModule) mcchat).getListener().handleDiscord(event); //Also runs Discord commands in chat channels + return Mono.empty(); //Wasn't handled, continue + }).filterWhen(ch -> { + timings.printElapsed("Run command 2"); + return CommandListener.runCommand(event.getMessage(), ch, false); + }); + }).onErrorContinue((err, obj) -> TBMCCoreAPI.SendException("An error occured while handling a message!", err)) + .subscribe(); + dispatcher.on(PresenceUpdateEvent.class).subscribe(event -> { + if (DiscordPlugin.SafeMode) + return; + FunModule.handleFullHouse(event); + }); + dispatcher.on(RoleCreateEvent.class).subscribe(GameRoleModule::handleRoleEvent); + dispatcher.on(RoleDeleteEvent.class).subscribe(GameRoleModule::handleRoleEvent); + dispatcher.on(RoleUpdateEvent.class).subscribe(GameRoleModule::handleRoleEvent); + } - private static boolean debug = false; + private static boolean debug = false; - public static void debug(String debug) { - if (CommonListeners.debug) //Debug - DPUtils.getLogger().info(debug); - } + public static void debug(String debug) { + if (CommonListeners.debug) //Debug + DPUtils.getLogger().info(debug); + } - public static boolean debug() { - return debug = !debug; - } + public static boolean debug() { + return debug = !debug; + } } diff --git a/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java b/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java index c23b8fa..6062711 100755 --- a/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java +++ b/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java @@ -5,39 +5,50 @@ import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.commands.ConnectCommand; import buttondevteam.lib.player.TBMCPlayerGetInfoEvent; import buttondevteam.lib.player.TBMCPlayerJoinEvent; +import discord4j.core.object.entity.Member; +import discord4j.core.object.entity.User; +import discord4j.core.object.util.Snowflake; +import lombok.val; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.server.ServerCommandEvent; -import sx.blah.discord.handle.obj.IUser; public class MCListener implements Listener { @EventHandler public void onPlayerJoin(TBMCPlayerJoinEvent e) { if (ConnectCommand.WaitingToConnect.containsKey(e.GetPlayer().PlayerName().get())) { - @SuppressWarnings("ConstantConditions") IUser user = DiscordPlugin.dc - .getUserByID(Long.parseLong(ConnectCommand.WaitingToConnect.get(e.GetPlayer().PlayerName().get()))); - e.getPlayer().sendMessage("§bTo connect with the Discord account @" + user.getName() + "#" + user.getDiscriminator() + @SuppressWarnings("ConstantConditions") User user = DiscordPlugin.dc + .getUserById(Snowflake.of(ConnectCommand.WaitingToConnect.get(e.GetPlayer().PlayerName().get()))).block(); + if (user == null) return; + e.getPlayer().sendMessage("§bTo connect with the Discord account @" + user.getUsername() + "#" + user.getDiscriminator() + " do /discord accept"); e.getPlayer().sendMessage("§bIf it wasn't you, do /discord decline"); } } - @EventHandler - public void onGetInfo(TBMCPlayerGetInfoEvent e) { - if (DiscordPlugin.SafeMode) - return; - DiscordPlayer dp = e.getPlayer().getAs(DiscordPlayer.class); - if (dp == null || dp.getDiscordID() == null || dp.getDiscordID().equals("")) - return; - IUser user = DiscordPlugin.dc.getUserByID(Long.parseLong(dp.getDiscordID())); - e.addInfo("Discord tag: " + user.getName() + "#" + user.getDiscriminator()); - e.addInfo(user.getPresence().getStatus().toString()); - if (user.getPresence().getActivity().isPresent() && user.getPresence().getText().isPresent()) - e.addInfo(user.getPresence().getActivity().get() + ": " + user.getPresence().getText().get()); - } + @EventHandler + public void onGetInfo(TBMCPlayerGetInfoEvent e) { + if (DiscordPlugin.SafeMode) + return; + DiscordPlayer dp = e.getPlayer().getAs(DiscordPlayer.class); + if (dp == null || dp.getDiscordID() == null || dp.getDiscordID().equals("")) + return; + User user = DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID())).block(); + if (user == null) return; + e.addInfo("Discord tag: " + user.getUsername() + "#" + user.getDiscriminator()); + Member member = user.asMember(DiscordPlugin.mainServer.getId()).block(); + if (member == null) return; + val pr = member.getPresence().block(); + if (pr == null) return; + e.addInfo(pr.getStatus().toString()); + if (pr.getActivity().isPresent()) { + val activity = pr.getActivity().get(); + e.addInfo(activity.getType() + ": " + activity.getName()); + } + } - @EventHandler - public void onServerCommand(ServerCommandEvent e) { - DiscordPlugin.Restart = !e.getCommand().equalsIgnoreCase("stop"); // The variable is always true except if stopped - } + @EventHandler + public void onServerCommand(ServerCommandEvent e) { + DiscordPlugin.Restart = !e.getCommand().equalsIgnoreCase("stop"); // The variable is always true except if stopped + } } diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.java b/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.java index 2bd2bb6..41599b7 100644 --- a/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.java +++ b/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.java @@ -9,15 +9,16 @@ import buttondevteam.lib.TBMCSystemChatEvent; import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.player.TBMCPlayer; +import discord4j.core.object.entity.Message; +import discord4j.core.object.util.Permission; +import lombok.RequiredArgsConstructor; import lombok.val; import org.bukkit.Bukkit; -import sx.blah.discord.handle.obj.IMessage; -import sx.blah.discord.handle.obj.Permissions; -import sx.blah.discord.util.PermissionUtils; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashSet; +import java.util.Objects; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -32,15 +33,17 @@ import java.util.stream.Collectors; "Mentioning the bot is needed in this case because the / prefix only works in #bot.", // "Invite link: " // }) +@RequiredArgsConstructor public class ChannelconCommand extends ICommand2DC { + private final MinecraftChatModule module; @Command2.Subcommand public boolean remove(Command2DCSender sender) { val message = sender.getMessage(); if (checkPerms(message)) return true; - if (MCChatCustom.removeCustomChat(message.getChannel())) - message.reply("channel connection removed."); + if (MCChatCustom.removeCustomChat(message.getChannelId())) + DPUtils.reply(message, null, "channel connection removed.").subscribe(); else - message.reply("this channel isn't connected."); + DPUtils.reply(message, null, "this channel isn't connected.").subscribe(); return true; } @@ -48,13 +51,13 @@ public class ChannelconCommand extends ICommand2DC { public boolean toggle(Command2DCSender sender, @Command2.OptionalArg String toggle) { val message = sender.getMessage(); if (checkPerms(message)) return true; - val cc = MCChatCustom.getCustomChat(message.getChannel()); + val cc = MCChatCustom.getCustomChat(message.getChannelId()); if (cc == null) return respond(sender, "this channel isn't connected."); Supplier togglesString = () -> Arrays.stream(ChannelconBroadcast.values()).map(t -> t.toString().toLowerCase() + ": " + ((cc.toggles & t.flag) == 0 ? "disabled" : "enabled")).collect(Collectors.joining("\n")) + "\n\n" + TBMCSystemChatEvent.BroadcastTarget.stream().map(target -> target.getName() + ": " + (cc.brtoggles.contains(target) ? "enabled" : "disabled")).collect(Collectors.joining("\n")); if (toggle == null) { - message.reply("toggles:\n" + togglesString.get()); + DPUtils.reply(message, null, "toggles:\n" + togglesString.get()).subscribe(); return true; } String arg = toggle.toUpperCase(); @@ -62,7 +65,7 @@ public class ChannelconCommand extends ICommand2DC { if (!b.isPresent()) { val bt = TBMCSystemChatEvent.BroadcastTarget.get(arg); if (bt == null) { - message.reply("cannot find toggle. Toggles:\n" + togglesString.get()); + DPUtils.reply(message, null, "cannot find toggle. Toggles:\n" + togglesString.get()).subscribe(); return true; } final boolean add; @@ -80,7 +83,7 @@ public class ChannelconCommand extends ICommand2DC { //1 1 | 0 // XOR cc.toggles ^= b.get().flag; - message.reply("'" + b.get().toString().toLowerCase() + "' " + ((cc.toggles & b.get().flag) == 0 ? "disabled" : "enabled")); + DPUtils.reply(message, null, "'" + b.get().toString().toLowerCase() + "' " + ((cc.toggles & b.get().flag) == 0 ? "disabled" : "enabled")).subscribe(); return true; } @@ -88,45 +91,49 @@ public class ChannelconCommand extends ICommand2DC { public boolean def(Command2DCSender sender, String channelID) { val message = sender.getMessage(); if (checkPerms(message)) return true; - if (MCChatCustom.hasCustomChat(message.getChannel())) + if (MCChatCustom.hasCustomChat(message.getChannelId())) return respond(sender, "this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it."); val chan = Channel.getChannels().filter(ch -> ch.ID.equalsIgnoreCase(channelID) || (Arrays.stream(ch.IDs().get()).anyMatch(cid -> cid.equalsIgnoreCase(channelID)))).findAny(); if (!chan.isPresent()) { //TODO: Red embed that disappears over time (kinda like the highlight messages in OW) - message.reply("MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /."); + DPUtils.reply(message, null, "MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /.").subscribe(); return true; } - val dp = DiscordPlayer.getUser(message.getAuthor().getStringID(), DiscordPlayer.class); + if (!message.getAuthor().isPresent()) return true; + val author = message.getAuthor().get(); + val dp = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class); val chp = dp.getAs(TBMCPlayer.class); if (chp == null) { - message.reply("you need to connect your Minecraft account. On our server in " + DPUtils.botmention() + " do " + DiscordPlugin.getPrefix() + "connect "); + DPUtils.reply(message, null, "you need to connect your Minecraft account. On our server in " + DPUtils.botmention() + " do " + DiscordPlugin.getPrefix() + "connect ").subscribe(); return true; } - DiscordConnectedPlayer dcp = new DiscordConnectedPlayer(message.getAuthor(), message.getChannel(), chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName()); + val channel = message.getChannel().block(); + DiscordConnectedPlayer dcp = new DiscordConnectedPlayer(message.getAuthor().get(), channel, chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName(), module); //Using a fake player with no login/logout, should be fine for this event String groupid = chan.get().getGroupID(dcp); if (groupid == null && !(chan.get() instanceof ChatRoom)) { //ChatRooms don't allow it unless the user joins, which happens later - message.reply("sorry, you cannot use that Minecraft channel."); + DPUtils.reply(message, null, "sorry, you cannot use that Minecraft channel.").subscribe(); return true; } if (chan.get() instanceof ChatRoom) { //ChatRooms don't work well - message.reply("chat rooms are not supported yet."); + DPUtils.reply(message, null, "chat rooms are not supported yet.").subscribe(); return true; } /*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) { - message.reply("sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm."); + DPUtils.reply(message, null, "sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm."); return true; }*/ //TODO: "Channel admins" that can connect channels? - MCChatCustom.addCustomChat(message.getChannel(), groupid, chan.get(), message.getAuthor(), dcp, 0, new HashSet<>()); + MCChatCustom.addCustomChat(channel, groupid, chan.get(), author, dcp, 0, new HashSet<>()); if (chan.get() instanceof ChatRoom) - message.reply("alright, connection made to the room!"); + DPUtils.reply(message, null, "alright, connection made to the room!").subscribe(); else - message.reply("alright, connection made to group `" + groupid + "`!"); + DPUtils.reply(message, null, "alright, connection made to group `" + groupid + "`!").subscribe(); return true; } - private boolean checkPerms(IMessage message) { - if (!PermissionUtils.hasPermissions(message.getChannel(), message.getAuthor(), Permissions.MANAGE_CHANNEL)) { - message.reply("you need to have manage permissions for this channel!"); + @SuppressWarnings("ConstantConditions") + private boolean checkPerms(Message message) { + if (!message.getAuthorAsMember().block().getBasePermissions().block().contains(Permission.MANAGE_CHANNELS)) { + DPUtils.reply(message, null, "you need to have manage permissions for this channel!").subscribe(); return true; } return false; @@ -140,7 +147,7 @@ public class ChannelconCommand extends ICommand2DC { "You need to have access to the MC channel and have manage permissions on the Discord channel.", // "You also need to have your Minecraft account connected. In " + DPUtils.botmention() + " use " + DiscordPlugin.getPrefix() + "connect .", // "Call this command from the channel you want to use.", // - "Usage: @" + DiscordPlugin.dc.getOurUser().getName() + " channelcon ", // + "Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf().block()).getMention() + " channelcon ", // "Use the ID (command) of the channel, for example `g` for the global chat.", // "To remove a connection use @ChromaBot channelcon remove in the channel.", // "Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix() + " prefix only works in " + DPUtils.botmention() + ".", // diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.java index 23af46f..63b2fb5 100755 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.java +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.java @@ -1,5 +1,6 @@ package buttondevteam.discordplugin.mcchat; +import buttondevteam.discordplugin.DPUtils; import buttondevteam.discordplugin.DiscordPlayer; import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.commands.Command2DCSender; @@ -7,6 +8,7 @@ import buttondevteam.discordplugin.commands.ICommand2DC; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.CommandClass; +import discord4j.core.object.entity.PrivateChannel; import lombok.val; @CommandClass(helpText = { @@ -20,18 +22,20 @@ public class MCChatCommand extends ICommand2DC { @Command2.Subcommand public boolean def(Command2DCSender sender) { val message = sender.getMessage(); - if (!message.getChannel().isPrivate()) { - message.reply("this command can only be issued in a direct message with the bot."); + val channel = message.getChannel().block(); + @SuppressWarnings("OptionalGetWithoutIsPresent") val author = message.getAuthor().get(); + if (!(channel instanceof PrivateChannel)) { + DPUtils.reply(message, null, "this command can only be issued in a direct message with the bot.").subscribe(); return true; } - try (final DiscordPlayer user = DiscordPlayer.getUser(message.getAuthor().getStringID(), DiscordPlayer.class)) { + try (final DiscordPlayer user = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class)) { boolean mcchat = !user.isMinecraftChatEnabled(); - MCChatPrivate.privateMCChat(message.getChannel(), mcchat, message.getAuthor(), user); - message.reply("Minecraft chat " + (mcchat // + MCChatPrivate.privateMCChat(channel, mcchat, author, user); + DPUtils.reply(message, null, "Minecraft chat " + (mcchat // ? "enabled. Use '" + DiscordPlugin.getPrefix() + "mcchat' again to turn it off." // - : "disabled.")); + : "disabled.")).subscribe(); } catch (Exception e) { - TBMCCoreAPI.SendException("Error while setting mcchat for user" + message.getAuthor().getName(), e); + TBMCCoreAPI.SendException("Error while setting mcchat for user " + author.getUsername() + "#" + author.getDiscriminator(), e); } return true; } // TODO: Pin channel switching to indicate the current channel diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java index 3d1b52f..c9edaca 100644 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java @@ -4,10 +4,11 @@ import buttondevteam.core.component.channel.Channel; import buttondevteam.core.component.channel.ChatRoom; import buttondevteam.discordplugin.DiscordConnectedPlayer; import buttondevteam.lib.TBMCSystemChatEvent; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.User; +import discord4j.core.object.util.Snowflake; import lombok.NonNull; import lombok.val; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IUser; import javax.annotation.Nullable; import java.util.ArrayList; @@ -21,7 +22,7 @@ public class MCChatCustom { */ static ArrayList lastmsgCustom = new ArrayList<>(); - public static void addCustomChat(IChannel channel, String groupid, Channel mcchannel, IUser user, DiscordConnectedPlayer dcp, int toggles, Set brtoggles) { + public static void addCustomChat(MessageChannel channel, String groupid, Channel mcchannel, User user, DiscordConnectedPlayer dcp, int toggles, Set brtoggles) { if (mcchannel instanceof ChatRoom) { ((ChatRoom) mcchannel).joinRoom(dcp); if (groupid == null) groupid = mcchannel.getGroupID(dcp); @@ -30,19 +31,19 @@ public class MCChatCustom { lastmsgCustom.add(lmd); } - public static boolean hasCustomChat(IChannel channel) { - return lastmsgCustom.stream().anyMatch(lmd -> lmd.channel.getLongID() == channel.getLongID()); + public static boolean hasCustomChat(Snowflake channel) { + return lastmsgCustom.stream().anyMatch(lmd -> lmd.channel.getId().asLong() == channel.asLong()); } @Nullable - public static CustomLMD getCustomChat(IChannel channel) { - return lastmsgCustom.stream().filter(lmd -> lmd.channel.getLongID() == channel.getLongID()).findAny().orElse(null); + public static CustomLMD getCustomChat(Snowflake channel) { + return lastmsgCustom.stream().filter(lmd -> lmd.channel.getId().asLong() == channel.asLong()).findAny().orElse(null); } - public static boolean removeCustomChat(IChannel channel) { - MCChatUtils.lastmsgfromd.remove(channel.getLongID()); + public static boolean removeCustomChat(Snowflake channel) { + MCChatUtils.lastmsgfromd.remove(channel.asLong()); return lastmsgCustom.removeIf(lmd -> { - if (lmd.channel.getLongID() != channel.getLongID()) + if (lmd.channel.getId().asLong() != channel.asLong()) return false; if (lmd.mcchannel instanceof ChatRoom) ((ChatRoom) lmd.mcchannel).leaveRoom(lmd.dcp); @@ -61,8 +62,8 @@ public class MCChatCustom { public int toggles; public Set brtoggles; - private CustomLMD(@NonNull IChannel channel, @NonNull IUser user, - @NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp, int toggles, Set brtoggles) { + private CustomLMD(@NonNull MessageChannel channel, @NonNull User user, + @NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp, int toggles, Set brtoggles) { super(channel, user); groupID = groupid; this.mcchannel = mcchannel; diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java index 5113f3c..aa7e278 100755 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java @@ -8,26 +8,26 @@ import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.DiscordSender; import buttondevteam.discordplugin.DiscordSenderBase; import buttondevteam.discordplugin.listeners.CommandListener; +import buttondevteam.discordplugin.listeners.CommonListeners; import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; +import buttondevteam.discordplugin.util.Timings; import buttondevteam.lib.*; import buttondevteam.lib.chat.ChatMessage; import buttondevteam.lib.chat.TBMCChatAPI; import buttondevteam.lib.player.TBMCPlayer; import com.vdurmont.emoji.EmojiParser; +import discord4j.core.event.domain.message.MessageCreateEvent; +import discord4j.core.object.Embed; +import discord4j.core.object.entity.*; +import discord4j.core.object.util.Snowflake; +import discord4j.core.spec.EmbedCreateSpec; import lombok.val; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.scheduler.BukkitTask; -import sx.blah.discord.api.internal.json.objects.EmbedObject; -import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IMessage; -import sx.blah.discord.handle.obj.IUser; -import sx.blah.discord.util.DiscordException; -import sx.blah.discord.util.EmbedBuilder; -import sx.blah.discord.util.MissingPermissionsException; +import reactor.core.publisher.Mono; import java.awt.*; import java.time.Instant; @@ -36,15 +36,16 @@ import java.util.Arrays; import java.util.Optional; 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; import java.util.stream.Collectors; public class MCChatListener implements Listener { - private BukkitTask sendtask; - private LinkedBlockingQueue> sendevents = new LinkedBlockingQueue<>(); - private Runnable sendrunnable; - private static Thread sendthread; + private BukkitTask sendtask; + private LinkedBlockingQueue> sendevents = new LinkedBlockingQueue<>(); + private Runnable sendrunnable; + private static Thread sendthread; private final MinecraftChatModule module; public MCChatListener(MinecraftChatModule minecraftChatModule) { @@ -52,359 +53,363 @@ public class MCChatListener implements Listener { } @EventHandler // Minecraft - public void onMCChat(TBMCChatEvent ev) { - if (!ComponentManager.isEnabled(MinecraftChatModule.class) || ev.isCancelled()) //SafeMode: Needed so it doesn't restart after server shutdown - return; - sendevents.add(new AbstractMap.SimpleEntry<>(ev, Instant.now())); - if (sendtask != null) - return; - sendrunnable = () -> { - sendthread = Thread.currentThread(); - processMCToDiscord(); - if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down - sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); - }; - sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); - } + public void onMCChat(TBMCChatEvent ev) { + if (!ComponentManager.isEnabled(MinecraftChatModule.class) || ev.isCancelled()) //SafeMode: Needed so it doesn't restart after server shutdown + return; + sendevents.add(new AbstractMap.SimpleEntry<>(ev, Instant.now())); + if (sendtask != null) + return; + sendrunnable = () -> { + sendthread = Thread.currentThread(); + processMCToDiscord(); + if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down + sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); + }; + sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); + } - private void processMCToDiscord() { - try { - TBMCChatEvent e; - Instant time; - val se = sendevents.take(); // Wait until an element is available - e = se.getKey(); - time = se.getValue(); + private void processMCToDiscord() { + try { + TBMCChatEvent e; + Instant time; + val se = sendevents.take(); // Wait until an element is available + e = se.getKey(); + time = se.getValue(); - final String authorPlayer = "[" + DPUtils.sanitizeStringNoEscape(e.getChannel().DisplayName().get()) + "] " // - + ("Minecraft".equals(e.getOrigin()) ? "" : "[" + e.getOrigin().substring(0, 1) + "]") // - + (DPUtils.sanitizeStringNoEscape(e.getSender() instanceof Player // - ? ((Player) e.getSender()).getDisplayName() // - : e.getSender().getName())); - val color = e.getChannel().Color().get(); - final EmbedBuilder embed = new EmbedBuilder().withAuthorName(authorPlayer) - .withDescription(e.getMessage()).withColor(new Color(color.getRed(), - color.getGreen(), color.getBlue())); - // embed.appendField("Channel", ((e.getSender() instanceof DiscordSenderBase ? "d|" : "") - // + DiscordPlugin.sanitizeString(e.getChannel().DisplayName)), false); - if (e.getSender() instanceof Player) - DPUtils.embedWithHead( - embed.withAuthorUrl("https://tbmcplugins.github.io/profile.html?type=minecraft&id=" - + ((Player) e.getSender()).getUniqueId()), - e.getSender().getName()); - else if (e.getSender() instanceof DiscordSenderBase) - embed.withAuthorIcon(((DiscordSenderBase) e.getSender()).getUser().getAvatarURL()) - .withAuthorUrl("https://tbmcplugins.github.io/profile.html?type=discord&id=" - + ((DiscordSenderBase) e.getSender()).getUser().getStringID()); // TODO: Constant/method to get URLs like this - // embed.withFooterText(e.getChannel().DisplayName); - embed.withTimestamp(time); - final long nanoTime = System.nanoTime(); - InterruptibleConsumer doit = lastmsgdata -> { - final EmbedObject embedObject = embed.build(); - if (lastmsgdata.message == null || lastmsgdata.message.isDeleted() - || !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().getName()) - || lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120 - || !lastmsgdata.mcchannel.ID.equals(e.getChannel().ID)) { - lastmsgdata.message = DiscordPlugin.sendMessageToChannelWait(lastmsgdata.channel, "", - embedObject); // TODO Use ChromaBot API - lastmsgdata.time = nanoTime; - lastmsgdata.mcchannel = e.getChannel(); - lastmsgdata.content = embedObject.description; - } else - try { - lastmsgdata.content = embedObject.description = lastmsgdata.content + "\n" - + embedObject.description;// The message object doesn't get updated - final MCChatUtils.LastMsgData _lastmsgdata = lastmsgdata; - DPUtils.perform(() -> _lastmsgdata.message.edit("", embedObject)); - } catch (MissingPermissionsException | DiscordException e1) { - TBMCCoreAPI.SendException("An error occurred while editing chat message!", e1); - } - }; - // Checks if the given channel is different than where the message was sent from - // Or if it was from MC - Predicate isdifferentchannel = ch -> !(e.getSender() instanceof DiscordSenderBase) - || ((DiscordSenderBase) e.getSender()).getChannel().getLongID() != ch.getLongID(); + final String authorPlayer = "[" + DPUtils.sanitizeStringNoEscape(e.getChannel().DisplayName().get()) + "] " // + + ("Minecraft".equals(e.getOrigin()) ? "" : "[" + e.getOrigin().substring(0, 1) + "]") // + + (DPUtils.sanitizeStringNoEscape(ThorpeUtils.getDisplayName(e.getSender()))); + val color = e.getChannel().Color().get(); + final Consumer embed = ecs -> { + ecs.setDescription(e.getMessage()).setColor(new Color(color.getRed(), + color.getGreen(), color.getBlue())); + if (e.getSender() instanceof Player) + DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(), + "https://tbmcplugins.github.io/profile.html?type=minecraft&id=" + + ((Player) e.getSender()).getUniqueId()); + else if (e.getSender() instanceof DiscordSenderBase) + ecs.setAuthor(authorPlayer, "https://tbmcplugins.github.io/profile.html?type=discord&id=" // TODO: Constant/method to get URLs like this + + ((DiscordSenderBase) e.getSender()).getUser().getId().asString(), + ((DiscordSenderBase) e.getSender()).getUser().getAvatarUrl()); + else + DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(), null); + ecs.setTimestamp(time); + }; + final long nanoTime = System.nanoTime(); + InterruptibleConsumer doit = lastmsgdata -> { + if (lastmsgdata.message == null + || !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().map(Embed.Author::getName).orElse(null)) + || lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120 + || !lastmsgdata.mcchannel.ID.equals(e.getChannel().ID)) { + lastmsgdata.message = lastmsgdata.channel.createEmbed(embed).block(); + lastmsgdata.time = nanoTime; + lastmsgdata.mcchannel = e.getChannel(); + lastmsgdata.content = e.getMessage(); + } else { + lastmsgdata.content = lastmsgdata.content + "\n" + + e.getMessage(); // The message object doesn't get updated + lastmsgdata.message.edit(mes -> mes.setEmbed(embed.andThen(ecs -> + ecs.setDescription(lastmsgdata.content)))).block(); + } + }; + // Checks if the given channel is different than where the message was sent from + // Or if it was from MC + Predicate isdifferentchannel = id -> !(e.getSender() instanceof DiscordSenderBase) + || ((DiscordSenderBase) e.getSender()).getChannel().getId().asLong() != id.asLong(); - if (e.getChannel().isGlobal() - && (e.isFromCommand() || isdifferentchannel.test(module.chatChannel().get()))) - doit.accept(MCChatUtils.lastmsgdata == null - ? MCChatUtils.lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannel().get(), null) - : MCChatUtils.lastmsgdata); + if (e.getChannel().isGlobal() + && (e.isFromCommand() || isdifferentchannel.test(module.chatChannel().get()))) + doit.accept(MCChatUtils.lastmsgdata == null + ? MCChatUtils.lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannelMono().block(), null) + : MCChatUtils.lastmsgdata); - for (MCChatUtils.LastMsgData data : MCChatPrivate.lastmsgPerUser) { - if ((e.isFromCommand() || isdifferentchannel.test(data.channel)) - && e.shouldSendTo(MCChatUtils.getSender(data.channel, data.user))) - doit.accept(data); - } + for (MCChatUtils.LastMsgData data : MCChatPrivate.lastmsgPerUser) { + if ((e.isFromCommand() || isdifferentchannel.test(data.channel.getId())) + && e.shouldSendTo(MCChatUtils.getSender(data.channel.getId(), data.user))) + doit.accept(data); + } - val iterator = MCChatCustom.lastmsgCustom.iterator(); - while (iterator.hasNext()) { - val lmd = iterator.next(); - if ((e.isFromCommand() || 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 - && 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); - } - } + val iterator = MCChatCustom.lastmsgCustom.iterator(); + while (iterator.hasNext()) { + val lmd = iterator.next(); + if ((e.isFromCommand() || isdifferentchannel.test(lmd.channel.getId())) //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 + && 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 + lmd.channel.createMessage("The user no longer has permission to view the channel, connection removed.").subscribe(); + } + } + } + } catch (InterruptedException ex) { //Stop if interrupted anywhere + sendtask.cancel(); + sendtask = null; + } catch (Exception ex) { + TBMCCoreAPI.SendException("Error while sending message to Discord!", ex); + } + } - @EventHandler - public void onChatPreprocess(TBMCChatPreprocessEvent event) { - int start = -1; - while ((start = event.getMessage().indexOf('@', start + 1)) != -1) { - int mid = event.getMessage().indexOf('#', start + 1); - if (mid == -1) - return; - int end_ = event.getMessage().indexOf(' ', mid + 1); - if (end_ == -1) - end_ = event.getMessage().length(); - final int end = end_; - final int startF = start; - DiscordPlugin.dc.getUsersByName(event.getMessage().substring(start + 1, mid)).stream() - .filter(u -> u.getDiscriminator().equals(event.getMessage().substring(mid + 1, end))).findAny() - .ifPresent(user -> event.setMessage(event.getMessage().substring(0, startF) + "@" + user.getName() - + (event.getMessage().length() > end ? event.getMessage().substring(end) : ""))); // TODO: Add formatting - start = end; // Skip any @s inside the mention - } - } + @EventHandler + public void onChatPreprocess(TBMCChatPreprocessEvent event) { + int start = -1; + while ((start = event.getMessage().indexOf('@', start + 1)) != -1) { + int mid = event.getMessage().indexOf('#', start + 1); + if (mid == -1) + return; + int end_ = event.getMessage().indexOf(' ', mid + 1); + if (end_ == -1) + end_ = event.getMessage().length(); + final int end = end_; + final int startF = start; + val user = DiscordPlugin.dc.getUsers().filter(u -> u.getUsername().equals(event.getMessage().substring(startF + 1, mid))) + .filter(u -> u.getDiscriminator().equals(event.getMessage().substring(mid + 1, end))).blockFirst(); + if (user != null) //TODO: Nicknames + event.setMessage(event.getMessage().substring(0, startF) + "@" + user.getUsername() + + (event.getMessage().length() > end ? event.getMessage().substring(end) : "")); // TODO: Add formatting + start = end; // Skip any @s inside the mention + } + } - // ......................DiscordSender....DiscordConnectedPlayer.DiscordPlayerSender - // Offline public chat......x............................................ - // Online public chat.......x...........................................x - // Offline private chat.....x.......................x.................... - // Online private chat......x.......................x...................x - // If online and enabling private chat, don't login - // If leaving the server and private chat is enabled (has ConnectedPlayer), call login in a task on lowest priority - // If private chat is enabled and joining the server, logout the fake player on highest priority - // If online and disabling private chat, don't logout - // The maps may not contain the senders for UnconnectedSenders + // ......................DiscordSender....DiscordConnectedPlayer.DiscordPlayerSender + // Offline public chat......x............................................ + // Online public chat.......x...........................................x + // Offline private chat.....x.......................x.................... + // Online private chat......x.......................x...................x + // If online and enabling private chat, don't login + // If leaving the server and private chat is enabled (has ConnectedPlayer), call login in a task on lowest priority + // If private chat is enabled and joining the server, logout the fake player on highest priority + // If online and disabling private chat, don't logout + // The maps may not contain the senders for UnconnectedSenders - /** - * 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); - } - MCChatUtils.lastmsgdata = null; - MCChatPrivate.lastmsgPerUser.clear(); - MCChatCustom.lastmsgCustom.clear(); - MCChatUtils.lastmsgfromd.clear(); - MCChatUtils.ConnectedSenders.clear(); - MCChatUtils.UnconnectedSenders.clear(); - recthread = sendthread = null; - } catch (InterruptedException e) { - e.printStackTrace(); //This thread shouldn't be interrupted - } - } + /** + * 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); + } + MCChatUtils.lastmsgdata = null; + MCChatPrivate.lastmsgPerUser.clear(); + MCChatCustom.lastmsgCustom.clear(); + MCChatUtils.lastmsgfromd.clear(); + MCChatUtils.ConnectedSenders.clear(); + MCChatUtils.UnconnectedSenders.clear(); + recthread = sendthread = null; + } catch (InterruptedException e) { + e.printStackTrace(); //This thread shouldn't be interrupted + } + } - private BukkitTask rectask; - private LinkedBlockingQueue recevents = new LinkedBlockingQueue<>(); - private Runnable recrun; - private static Thread recthread; + private BukkitTask rectask; + private LinkedBlockingQueue recevents = new LinkedBlockingQueue<>(); + private Runnable recrun; + private static Thread recthread; // Discord - public boolean handleDiscord(MessageReceivedEvent ev) { - if (!ComponentManager.isEnabled(MinecraftChatModule.class)) - return false; - val author = ev.getMessage().getAuthor(); - final boolean hasCustomChat = MCChatCustom.hasCustomChat(ev.getChannel()); - if (ev.getMessage().getChannel().getLongID() != module.chatChannel().get().getLongID() - && !(ev.getMessage().getChannel().isPrivate() && MCChatPrivate.isMinecraftChatEnabled(author.getStringID())) - && !hasCustomChat) - return false; //Chat isn't enabled on this channel - if (ev.getMessage().getChannel().isPrivate() //Only in private chat - && ev.getMessage().getContent().length() < "/mcchat<>".length() - && ev.getMessage().getContent().replace("/", "") - .equalsIgnoreCase("mcchat")) //Either mcchat or /mcchat - return false; //Allow disabling the chat if needed - if (CommandListener.runCommand(ev.getMessage(), true)) - return true; //Allow running commands in chat channels - MCChatUtils.resetLastMessage(ev.getChannel()); - recevents.add(ev); - if (rectask != null) - return true; - recrun = () -> { //Don't return in a while loop next time - recthread = Thread.currentThread(); - processDiscordToMC(); - if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down - rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Continue message processing - }; - rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Start message processing - return true; - } + public Mono handleDiscord(MessageCreateEvent ev) { + val ret = Mono.just(true); + if (!ComponentManager.isEnabled(MinecraftChatModule.class)) + return ret; + Timings timings = CommonListeners.timings; + timings.printElapsed("Chat event"); + val author = ev.getMessage().getAuthor(); + final boolean hasCustomChat = MCChatCustom.hasCustomChat(ev.getMessage().getChannelId()); + return ev.getMessage().getChannel().filter(channel -> { + timings.printElapsed("Filter 1"); + return !(ev.getMessage().getChannelId().asLong() != module.chatChannel().get().asLong() + && !(channel instanceof PrivateChannel + && author.map(u -> MCChatPrivate.isMinecraftChatEnabled(u.getId().asString())).orElse(false) + && !hasCustomChat)); //Chat isn't enabled on this channel + }).filter(channel -> { + timings.printElapsed("Filter 2"); + return !(channel instanceof PrivateChannel //Only in private chat + && ev.getMessage().getContent().isPresent() + && ev.getMessage().getContent().get().length() < "/mcchat<>".length() + && ev.getMessage().getContent().get().replace("/", "") + .equalsIgnoreCase("mcchat")); //Either mcchat or /mcchat + //Allow disabling the chat if needed + }).filterWhen(channel -> CommandListener.runCommand(ev.getMessage(), channel, true)) + //Allow running commands in chat channels + .filter(channel -> { + MCChatUtils.resetLastMessage(channel); + recevents.add(ev); + timings.printElapsed("Message event added"); + if (rectask != null) + return true; + recrun = () -> { //Don't return in a while loop next time + recthread = Thread.currentThread(); + processDiscordToMC(); + if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down + rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Continue message processing + }; + rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Start message processing + return true; + }).map(b -> false).defaultIfEmpty(true); + } - private void processDiscordToMC() { - @val - sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent event; - try { - event = recevents.take(); - } catch (InterruptedException e1) { - rectask.cancel(); - return; - } - val sender = event.getMessage().getAuthor(); - String dmessage = event.getMessage().getContent(); - try { - final DiscordSenderBase dsender = MCChatUtils.getSender(event.getMessage().getChannel(), sender); - val user = dsender.getChromaUser(); + private void processDiscordToMC() { + MessageCreateEvent event; + try { + event = recevents.take(); + } catch (InterruptedException e1) { + rectask.cancel(); + return; + } + val sender = event.getMessage().getAuthor().orElse(null); + String dmessage = event.getMessage().getContent().orElse(""); + try { + final DiscordSenderBase dsender = MCChatUtils.getSender(event.getMessage().getChannelId(), sender); + val user = dsender.getChromaUser(); - for (IUser u : event.getMessage().getMentions()) { - dmessage = dmessage.replace(u.mention(false), "@" + u.getName()); // TODO: IG Formatting - final String nick = u.getNicknameForGuild(DiscordPlugin.mainServer); - dmessage = dmessage.replace(u.mention(true), "@" + (nick != null ? nick : u.getName())); - } - for (IChannel ch : event.getMessage().getChannelMentions()) { - dmessage = dmessage.replace(ch.mention(), "#" + ch.getName()); // TODO: IG Formatting - } + for (User u : event.getMessage().getUserMentions().toIterable()) { //TODO: Role mentions + dmessage = dmessage.replace(u.getMention(), "@" + u.getUsername()); // TODO: IG Formatting + val m = u.asMember(DiscordPlugin.mainServer.getId()).block(); + if (m != null) { + final String nick = m.getDisplayName(); + dmessage = dmessage.replace(m.getNicknameMention(), "@" + nick); + } + } + for (GuildChannel ch : event.getGuild().flux().flatMap(Guild::getChannels).toIterable()) { + dmessage = dmessage.replace(ch.getMention(), "#" + ch.getName()); // TODO: IG Formatting + } - dmessage = EmojiParser.parseToAliases(dmessage, EmojiParser.FitzpatrickAction.PARSE); //Converts emoji to text- TODO: Add option to disable (resource pack?) - dmessage = dmessage.replaceAll(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:"); //Convert to Discord's format so it still shows up + dmessage = EmojiParser.parseToAliases(dmessage, EmojiParser.FitzpatrickAction.PARSE); //Converts emoji to text- TODO: Add option to disable (resource pack?) + dmessage = dmessage.replaceAll(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:"); //Convert to Discord's format so it still shows up - Function getChatMessage = msg -> // - msg + (event.getMessage().getAttachments().size() > 0 ? "\n" + event.getMessage() - .getAttachments().stream().map(IMessage.Attachment::getUrl).collect(Collectors.joining("\n")) - : ""); + Function getChatMessage = msg -> // + msg + (event.getMessage().getAttachments().size() > 0 ? "\n" + event.getMessage() + .getAttachments().stream().map(Attachment::getUrl).collect(Collectors.joining("\n")) + : ""); - MCChatCustom.CustomLMD clmd = MCChatCustom.getCustomChat(event.getChannel()); + MCChatCustom.CustomLMD clmd = MCChatCustom.getCustomChat(event.getMessage().getChannelId()); - boolean react = false; + boolean react = false; - if (dmessage.startsWith("/")) { // Ingame command - DPUtils.perform(() -> { - if (!event.getMessage().isDeleted() && !event.getChannel().isPrivate()) - event.getMessage().delete(); - }); - final String cmd = dmessage.substring(1); - final String cmdlowercased = cmd.toLowerCase(); - if (dsender instanceof DiscordSender && module.whitelistedCommands().get().stream() - .noneMatch(s -> cmdlowercased.equals(s) || cmdlowercased.startsWith(s + " "))) { - // Command not whitelisted - dsender.sendMessage("Sorry, you can only access these commands:\n" - + module.whitelistedCommands().get().stream().map(uc -> "/" + uc) - .collect(Collectors.joining(", ")) - + (user.getConnectedID(TBMCPlayer.class) == null - ? "\nTo access your commands, first please connect your accounts, using /connect in " - + DPUtils.botmention() - + "\nThen y" - : "\nY") - + "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!"); - return; - } - val ev = new TBMCCommandPreprocessEvent(dsender, dmessage); - Bukkit.getPluginManager().callEvent(ev); - if (ev.isCancelled()) - return; - int spi = cmdlowercased.indexOf(' '); - final String topcmd = spi == -1 ? cmdlowercased : cmdlowercased.substring(0, spi); - Optional ch = Channel.getChannels() - .filter(c -> c.ID.equalsIgnoreCase(topcmd) - || (c.IDs().get().length > 0 - && Arrays.stream(c.IDs().get()).anyMatch(id -> id.equalsIgnoreCase(topcmd)))).findAny(); - if (!ch.isPresent()) //TODO: What if talking in the public chat while we have it on a different one - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, //Commands need to be run sync - () -> { //TODO: Better handling... - val channel = user.channel(); - val chtmp = channel.get(); - if (clmd != null) { - channel.set(clmd.mcchannel); //Hack to send command in the channel - } //TODO: Permcheck isn't implemented for commands - VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd); - Bukkit.getLogger().info(dsender.getName() + " issued command from Discord: /" + cmdlowercased); - if (clmd != null) - channel.set(chtmp); - }); - else { - Channel chc = ch.get(); - if (!chc.isGlobal() && !event.getMessage().getChannel().isPrivate()) - dsender.sendMessage( - "You can only talk in a public chat here. DM `mcchat` to enable private chat to talk in the other channels."); - else { - if (spi == -1) // Switch channels - { - val channel = dsender.getChromaUser().channel(); - val oldch = channel.get(); - if (oldch instanceof ChatRoom) - ((ChatRoom) oldch).leaveRoom(dsender); - if (!oldch.ID.equals(chc.ID)) { - channel.set(chc); - if (chc instanceof ChatRoom) - ((ChatRoom) chc).joinRoom(dsender); - } else - channel.set(Channel.GlobalChat); - dsender.sendMessage("You're now talking in: " - + DPUtils.sanitizeString(channel.get().DisplayName().get())); - } else { // Send single message - final String msg = cmd.substring(spi + 1); - val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(msg)).fromCommand(true); - if (clmd == null) - TBMCChatAPI.SendChatMessage(cmb.build(), chc); - else - TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), chc); - react = true; - } - } - } - } else {// Not a command - if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0 - && !event.getChannel().isPrivate() && event.getMessage().isSystemMessage()) { - val rtr = clmd != null ? clmd.mcchannel.getRTR(clmd.dcp) - : dsender.getChromaUser().channel().get().getRTR(dsender); - TBMCChatAPI.SendSystemMessage(clmd != null ? clmd.mcchannel : dsender.getChromaUser().channel().get(), rtr, - (dsender instanceof Player ? ((Player) dsender).getDisplayName() - : dsender.getName()) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL); - } - else { - val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(dmessage)).fromCommand(false); - if (clmd != null) - TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), clmd.mcchannel); - else - TBMCChatAPI.SendChatMessage(cmb.build()); - react = true; - } - } - if (react) { - try { - val lmfd = MCChatUtils.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); - } - MCChatUtils.lastmsgfromd.put(event.getChannel().getLongID(), event.getMessage()); - DPUtils.perform(() -> event.getMessage().addReaction(DiscordPlugin.DELIVERED_REACTION)); - } - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e); - } - } + val sendChannel = event.getMessage().getChannel().block(); + boolean isPrivate = sendChannel instanceof PrivateChannel; + if (dmessage.startsWith("/")) { // Ingame command + if (!isPrivate) + event.getMessage().delete().subscribe(); + final String cmd = dmessage.substring(1); + final String cmdlowercased = cmd.toLowerCase(); + if (dsender instanceof DiscordSender && module.whitelistedCommands().get().stream() + .noneMatch(s -> cmdlowercased.equals(s) || cmdlowercased.startsWith(s + " "))) { + // Command not whitelisted + dsender.sendMessage("Sorry, you can only access these commands:\n" + + module.whitelistedCommands().get().stream().map(uc -> "/" + uc) + .collect(Collectors.joining(", ")) + + (user.getConnectedID(TBMCPlayer.class) == null + ? "\nTo access your commands, first please connect your accounts, using /connect in " + + DPUtils.botmention() + + "\nThen y" + : "\nY") + + "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!"); + return; + } + val ev = new TBMCCommandPreprocessEvent(dsender, dmessage); + Bukkit.getPluginManager().callEvent(ev); + if (ev.isCancelled()) + return; + int spi = cmdlowercased.indexOf(' '); + final String topcmd = spi == -1 ? cmdlowercased : cmdlowercased.substring(0, spi); + Optional ch = Channel.getChannels() + .filter(c -> c.ID.equalsIgnoreCase(topcmd) + || (c.IDs().get().length > 0 + && Arrays.stream(c.IDs().get()).anyMatch(id -> id.equalsIgnoreCase(topcmd)))).findAny(); + if (!ch.isPresent()) //TODO: What if talking in the public chat while we have it on a different one + Bukkit.getScheduler().runTask(DiscordPlugin.plugin, //Commands need to be run sync + () -> { //TODO: Better handling... + val channel = user.channel(); + val chtmp = channel.get(); + if (clmd != null) { + channel.set(clmd.mcchannel); //Hack to send command in the channel + } //TODO: Permcheck isn't implemented for commands + VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd); + Bukkit.getLogger().info(dsender.getName() + " issued command from Discord: /" + cmdlowercased); + if (clmd != null) + channel.set(chtmp); + }); + else { + Channel chc = ch.get(); + if (!chc.isGlobal() && !isPrivate) + dsender.sendMessage( + "You can only talk in a public chat here. DM `mcchat` to enable private chat to talk in the other channels."); + else { + if (spi == -1) // Switch channels + { + val channel = dsender.getChromaUser().channel(); + val oldch = channel.get(); + if (oldch instanceof ChatRoom) + ((ChatRoom) oldch).leaveRoom(dsender); + if (!oldch.ID.equals(chc.ID)) { + channel.set(chc); + if (chc instanceof ChatRoom) + ((ChatRoom) chc).joinRoom(dsender); + } else + channel.set(Channel.GlobalChat); + dsender.sendMessage("You're now talking in: " + + DPUtils.sanitizeString(channel.get().DisplayName().get())); + } else { // Send single message + final String msg = cmd.substring(spi + 1); + val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(msg)).fromCommand(true); + if (clmd == null) + TBMCChatAPI.SendChatMessage(cmb.build(), chc); + else + TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), chc); + react = true; + } + } + } + } else {// Not a command + if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0 + && !isPrivate && event.getMessage().getType() == Message.Type.CHANNEL_PINNED_MESSAGE) { + val rtr = clmd != null ? clmd.mcchannel.getRTR(clmd.dcp) + : dsender.getChromaUser().channel().get().getRTR(dsender); + TBMCChatAPI.SendSystemMessage(clmd != null ? clmd.mcchannel : dsender.getChromaUser().channel().get(), rtr, + (dsender instanceof Player ? ((Player) dsender).getDisplayName() + : dsender.getName()) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL); + } else { + val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(dmessage)).fromCommand(false); + if (clmd != null) + TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), clmd.mcchannel); + else + TBMCChatAPI.SendChatMessage(cmb.build()); + react = true; + } + } + if (react) { + try { + val lmfd = MCChatUtils.lastmsgfromd.get(event.getMessage().getChannelId().asLong()); + if (lmfd != null) { + lmfd.removeSelfReaction(DiscordPlugin.DELIVERED_REACTION).subscribe(); // 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); + } + MCChatUtils.lastmsgfromd.put(event.getMessage().getChannelId().asLong(), event.getMessage()); + event.getMessage().addReaction(DiscordPlugin.DELIVERED_REACTION).subscribe(); + } + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e); + } + } - @FunctionalInterface - private interface InterruptibleConsumer { - void accept(T value) throws TimeoutException, InterruptedException; - } + @FunctionalInterface + private interface InterruptibleConsumer { + void accept(T value) throws TimeoutException, InterruptedException; + } } diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.java index 7d87376..344ecf5 100644 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.java +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.java @@ -1,17 +1,14 @@ package buttondevteam.discordplugin.mcchat; +import buttondevteam.core.ComponentManager; import buttondevteam.discordplugin.DiscordConnectedPlayer; import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.lib.player.TBMCPlayer; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.PrivateChannel; +import discord4j.core.object.entity.User; import lombok.val; import org.bukkit.Bukkit; -import org.bukkit.event.Event; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IPrivateChannel; -import sx.blah.discord.handle.obj.IUser; import java.util.ArrayList; @@ -22,27 +19,31 @@ public class MCChatPrivate { */ static ArrayList lastmsgPerUser = new ArrayList<>(); - public static boolean privateMCChat(IChannel channel, boolean start, IUser user, DiscordPlayer dp) { + public static boolean privateMCChat(MessageChannel channel, boolean start, User user, DiscordPlayer dp) { TBMCPlayer mcp = dp.getAs(TBMCPlayer.class); if (mcp != null) { // If the accounts aren't connected, can't make a connected sender val p = Bukkit.getPlayer(mcp.getUUID()); val op = Bukkit.getOfflinePlayer(mcp.getUUID()); + val mcm = ComponentManager.getIfEnabled(MinecraftChatModule.class); if (start) { - val sender = new DiscordConnectedPlayer(user, channel, mcp.getUUID(), op.getName()); + val sender = new DiscordConnectedPlayer(user, channel, mcp.getUUID(), op.getName(), mcm); MCChatUtils.addSender(MCChatUtils.ConnectedSenders, user, sender); if (p == null)// Player is offline - If the player is online, that takes precedence - callEventSync(new PlayerJoinEvent(sender, "")); + MCChatUtils.callLoginEvents(sender); } else { - val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel, user); - if (p == null)// Player is offline - If the player is online, that takes precedence - callEventSync(new PlayerQuitEvent(sender, "")); + val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel.getId(), user); + assert sender != null; + if (p == null // Player is offline - If the player is online, that takes precedence + && sender.isLoggedIn()) //Don't call the quit event if login failed + MCChatUtils.callLogoutEvent(sender, true); + sender.setLoggedIn(false); } } // ---- PermissionsEx warning is normal on logout ---- if (!start) - MCChatUtils.lastmsgfromd.remove(channel.getLongID()); + MCChatUtils.lastmsgfromd.remove(channel.getId().asLong()); return start // - ? lastmsgPerUser.add(new MCChatUtils.LastMsgData(channel, user)) // Doesn't support group DMs - : lastmsgPerUser.removeIf(lmd -> lmd.channel.getLongID() == channel.getLongID()); + ? lastmsgPerUser.add(new MCChatUtils.LastMsgData(channel, user)) // Doesn't support group DMs + : lastmsgPerUser.removeIf(lmd -> lmd.channel.getId().asLong() == channel.getId().asLong()); } public static boolean isMinecraftChatEnabled(DiscordPlayer dp) { @@ -51,18 +52,16 @@ public class MCChatPrivate { public static boolean isMinecraftChatEnabled(String did) { // Don't load the player data just for this return lastmsgPerUser.stream() - .anyMatch(lmd -> ((IPrivateChannel) lmd.channel).getRecipient().getStringID().equals(did)); + .anyMatch(lmd -> ((PrivateChannel) lmd.channel) + .getRecipientIds().stream().anyMatch(u -> u.asString().equals(did))); } public static void logoutAll() { for (val entry : MCChatUtils.ConnectedSenders.entrySet()) for (val valueEntry : entry.getValue().entrySet()) if (MCChatUtils.getSender(MCChatUtils.OnlineSenders, valueEntry.getKey(), valueEntry.getValue().getUser()) == null) //If the player is online then the fake player was already logged out - MCChatUtils.callEventExcludingSome(new PlayerQuitEvent(valueEntry.getValue(), "")); //This is sync + MCChatUtils.callLogoutEvent(valueEntry.getValue(), false); //This is sync MCChatUtils.ConnectedSenders.clear(); } - private static void callEventSync(Event event) { - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> MCChatUtils.callEventExcludingSome(event)); - } } diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.java index 3f04e56..92af87d 100644 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.java +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.java @@ -1,10 +1,13 @@ package buttondevteam.discordplugin.mcchat; import buttondevteam.core.ComponentManager; -import buttondevteam.core.component.channel.Channel; import buttondevteam.discordplugin.*; import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule; +import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCSystemChatEvent; +import com.google.common.collect.Sets; +import discord4j.core.object.entity.*; +import discord4j.core.object.util.Snowflake; import io.netty.util.collection.LongObjectHashMap; import lombok.RequiredArgsConstructor; import lombok.experimental.var; @@ -13,16 +16,20 @@ import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.plugin.AuthorNagException; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.RegisteredListener; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IMessage; -import sx.blah.discord.handle.obj.IUser; +import reactor.core.publisher.Mono; import javax.annotation.Nullable; +import java.net.InetAddress; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; @@ -34,23 +41,22 @@ public class MCChatUtils { /** * May contain P<DiscordID> as key for public chat */ - public static final HashMap> UnconnectedSenders = new HashMap<>(); - public static final HashMap> ConnectedSenders = new HashMap<>(); + public static final HashMap> UnconnectedSenders = new HashMap<>(); + public static final HashMap> ConnectedSenders = new HashMap<>(); /** * May contain P<DiscordID> as key for public chat */ - public static final HashMap> OnlineSenders = new HashMap<>(); + public static final HashMap> OnlineSenders = new HashMap<>(); static @Nullable LastMsgData lastmsgdata; - static LongObjectHashMap lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks + static LongObjectHashMap lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks private static MinecraftChatModule module; + private static HashMap, HashSet> staticExcludedPlugins = new HashMap<>(); public static void updatePlayerList() { if (notEnabled()) return; - DPUtils.performNoWait(() -> { - if (lastmsgdata != null) - updatePL(lastmsgdata); - MCChatCustom.lastmsgCustom.forEach(MCChatUtils::updatePL); - }); + if (lastmsgdata != null) + updatePL(lastmsgdata); + MCChatCustom.lastmsgCustom.forEach(MCChatUtils::updatePL); } private static boolean notEnabled() { @@ -64,55 +70,60 @@ public class MCChatUtils { } private static void updatePL(LastMsgData lmd) { - String topic = lmd.channel.getTopic(); - if (topic == null || topic.length() == 0) + if (!(lmd.channel instanceof TextChannel)) { + TBMCCoreAPI.SendException("Failed to update player list for channel " + lmd.channel.getId(), + new Exception("The channel isn't a (guild) text channel.")); + return; + } + String topic = ((TextChannel) lmd.channel).getTopic().orElse(""); + if (topic.length() == 0) topic = ".\n----\nMinecraft chat\n----\n."; String[] s = topic.split("\\n----\\n"); if (s.length < 3) return; s[0] = Bukkit.getOnlinePlayers().size() + " player" + (Bukkit.getOnlinePlayers().size() != 1 ? "s" : "") - + " online"; + + " online"; s[s.length - 1] = "Players: " + Bukkit.getOnlinePlayers().stream() - .map(p -> DPUtils.sanitizeString(p.getDisplayName())).collect(Collectors.joining(", ")); - lmd.channel.changeTopic(String.join("\n----\n", s)); + .map(p -> DPUtils.sanitizeString(p.getDisplayName())).collect(Collectors.joining(", ")); + ((TextChannel) lmd.channel).edit(tce -> tce.setTopic(String.join("\n----\n", s)).setReason("Player list update")).subscribe(); //Don't wait } - public static T addSender(HashMap> senders, - IUser user, T sender) { - return addSender(senders, user.getStringID(), sender); + public static T addSender(HashMap> senders, + User user, T sender) { + return addSender(senders, user.getId().asString(), sender); } - public static T addSender(HashMap> senders, + public static T addSender(HashMap> senders, String did, T sender) { var map = senders.get(did); if (map == null) map = new HashMap<>(); - map.put(sender.getChannel(), sender); + map.put(sender.getChannel().getId(), sender); senders.put(did, map); return sender; } - public static T getSender(HashMap> senders, - IChannel channel, IUser user) { - var map = senders.get(user.getStringID()); + public static T getSender(HashMap> senders, + Snowflake channel, User user) { + var map = senders.get(user.getId().asString()); if (map != null) return map.get(channel); return null; } - public static T removeSender(HashMap> senders, - IChannel channel, IUser user) { - var map = senders.get(user.getStringID()); + public static T removeSender(HashMap> senders, + Snowflake channel, User user) { + var map = senders.get(user.getId().asString()); if (map != null) return map.remove(channel); return null; } - public static void forAllMCChat(Consumer action) { + public static void forAllMCChat(Consumer> action) { if (notEnabled()) return; - action.accept(module.chatChannel().get()); + action.accept(module.chatChannelMono()); for (LastMsgData data : MCChatPrivate.lastmsgPerUser) - action.accept(data.channel); + action.accept(Mono.just(data.channel)); // lastmsgCustom.forEach(cc -> action.accept(cc.channel)); - Only send relevant messages to custom chat } @@ -123,11 +134,11 @@ public class MCChatUtils { * @param toggle The toggle to check * @param hookmsg Whether the message is also sent from the hook */ - public static void forCustomAndAllMCChat(Consumer action, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { + public static void forCustomAndAllMCChat(Consumer> action, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { if (notEnabled()) return; if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg) forAllMCChat(action); - final Consumer customLMDConsumer = cc -> action.accept(cc.channel); + final Consumer customLMDConsumer = cc -> action.accept(Mono.just(cc.channel)); if (toggle == null) MCChatCustom.lastmsgCustom.forEach(customLMDConsumer); else @@ -141,7 +152,7 @@ public class MCChatUtils { * @param sender The sender to check perms of or null to send to all that has it toggled * @param toggle The toggle to check or null to send to all allowed */ - public static void forAllowedCustomMCChat(Consumer action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle) { + public static void forAllowedCustomMCChat(Consumer> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle) { if (notEnabled()) return; MCChatCustom.lastmsgCustom.stream().filter(clmd -> { //new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple @@ -150,7 +161,7 @@ public class MCChatUtils { if (sender == null) return true; return clmd.groupID.equals(clmd.mcchannel.getGroupID(sender)); - }).forEach(cc -> action.accept(cc.channel)); //TODO: Send error messages on channel connect + }).forEach(cc -> action.accept(Mono.just(cc.channel))); //TODO: Send error messages on channel connect } /** @@ -161,42 +172,42 @@ public class MCChatUtils { * @param toggle The toggle to check or null to send to all allowed * @param hookmsg Whether the message is also sent from the hook */ - public static void forAllowedCustomAndAllMCChat(Consumer action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { + public static void forAllowedCustomAndAllMCChat(Consumer> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { if (notEnabled()) return; if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg) forAllMCChat(action); forAllowedCustomMCChat(action, sender, toggle); } - public static Consumer send(String message) { - return ch -> DiscordPlugin.sendMessageToChannel(ch, DPUtils.sanitizeString(message)); + public static Consumer> send(String message) { + return ch -> ch.flatMap(mc -> mc.createMessage(DPUtils.sanitizeString(message))).subscribe(); } - public static void forAllowedMCChat(Consumer action, TBMCSystemChatEvent event) { + public static void forAllowedMCChat(Consumer> action, TBMCSystemChatEvent event) { if (notEnabled()) return; if (event.getChannel().isGlobal()) - action.accept(module.chatChannel().get()); + action.accept(module.chatChannelMono()); for (LastMsgData data : MCChatPrivate.lastmsgPerUser) - if (event.shouldSendTo(getSender(data.channel, data.user))) - action.accept(data.channel); + if (event.shouldSendTo(getSender(data.channel.getId(), data.user))) + action.accept(Mono.just(data.channel)); //TODO: Only store ID? MCChatCustom.lastmsgCustom.stream().filter(clmd -> { if (!clmd.brtoggles.contains(event.getTarget())) return false; return event.shouldSendTo(clmd.dcp); - }).map(clmd -> clmd.channel).forEach(action); + }).map(clmd -> Mono.just(clmd.channel)).forEach(action); } /** * This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc. */ - static DiscordSenderBase getSender(IChannel channel, final IUser author) { + static DiscordSenderBase getSender(Snowflake channel, final User author) { //noinspection OptionalGetWithoutIsPresent return Stream.>>of( // https://stackoverflow.com/a/28833677/2703239 - () -> Optional.ofNullable(getSender(OnlineSenders, channel, author)), // Find first non-null - () -> Optional.ofNullable(getSender(ConnectedSenders, channel, author)), // This doesn't support the public chat, but it'll always return null for it - () -> Optional.ofNullable(getSender(UnconnectedSenders, channel, author)), // - () -> Optional.of(addSender(UnconnectedSenders, author, - new DiscordSender(author, channel)))).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get(); + () -> Optional.ofNullable(getSender(OnlineSenders, channel, author)), // Find first non-null + () -> Optional.ofNullable(getSender(ConnectedSenders, channel, author)), // This doesn't support the public chat, but it'll always return null for it + () -> Optional.ofNullable(getSender(UnconnectedSenders, channel, author)), // + () -> Optional.of(addSender(UnconnectedSenders, author, + new DiscordSender(author, (MessageChannel) DiscordPlugin.dc.getChannelById(channel).block())))).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get(); } /** @@ -205,15 +216,15 @@ public class MCChatUtils { * * @param channel The channel to reset in - the process is slightly different for the public, private and custom chats */ - public static void resetLastMessage(IChannel channel) { + public static void resetLastMessage(Channel channel) { if (notEnabled()) return; - if (channel.getLongID() == module.chatChannel().get().getLongID()) { - (lastmsgdata == null ? lastmsgdata = new LastMsgData(module.chatChannel().get(), null) - : lastmsgdata).message = null; + if (channel.getId().asLong() == module.chatChannel().get().asLong()) { + (lastmsgdata == null ? lastmsgdata = new LastMsgData(module.chatChannelMono().block(), null) + : lastmsgdata).message = null; return; } // Don't set the whole object to null, the player and channel information should be preserved - for (LastMsgData data : channel.isPrivate() ? MCChatPrivate.lastmsgPerUser : MCChatCustom.lastmsgCustom) { - if (data.channel.getLongID() == channel.getLongID()) { + for (LastMsgData data : channel instanceof PrivateChannel ? MCChatPrivate.lastmsgPerUser : MCChatCustom.lastmsgCustom) { + if (data.channel.getId().asLong() == channel.getId().asLong()) { data.message = null; return; } @@ -221,9 +232,23 @@ public class MCChatUtils { //If it gets here, it's sending a message to a non-chat channel } + public static void addStaticExcludedPlugin(Class event, String plugin) { + staticExcludedPlugins.compute(event, (e, hs) -> hs == null + ? Sets.newHashSet(plugin) + : (hs.add(plugin) ? hs : hs)); + } + public static void callEventExcludingSome(Event event) { if (notEnabled()) return; - callEventExcluding(event, false, module.excludedPlugins().get()); + val second = staticExcludedPlugins.get(event.getClass()); + String[] first = module.excludedPlugins().get(); + String[] both = second == null ? first + : Arrays.copyOf(first, first.length + second.size()); + int i = first.length; + if (second != null) + for (String plugin : second) + both[i++] = plugin; + callEventExcluding(event, false, both); } /** @@ -284,13 +309,59 @@ public class MCChatUtils { } } + /** + * Call it from an async thread. + */ + public static void callLoginEvents(DiscordConnectedPlayer dcp) { + Consumer> loginFail = kickMsg -> { + dcp.sendMessage("Minecraft chat disabled, as the login failed: " + kickMsg.get()); + MCChatPrivate.privateMCChat(dcp.getChannel(), false, dcp.getUser(), dcp.getChromaUser()); + }; //Probably also happens if the user is banned or so + val event = new AsyncPlayerPreLoginEvent(dcp.getName(), InetAddress.getLoopbackAddress(), dcp.getUniqueId()); + callEventExcludingSome(event); + if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { + loginFail.accept(event::getKickMessage); + return; + } + Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> { + val ev = new PlayerLoginEvent(dcp, "localhost", InetAddress.getLoopbackAddress()); + callEventExcludingSome(ev); + if (ev.getResult() != PlayerLoginEvent.Result.ALLOWED) { + loginFail.accept(ev::getKickMessage); + return; + } + callEventExcludingSome(new PlayerJoinEvent(dcp, "")); + dcp.setLoggedIn(true); + DPUtils.getLogger().info(dcp.getName() + " (" + dcp.getUniqueId() + ") logged in from Discord"); + }); + } + + /** + * Only calls the events if the player is actually logged in + * + * @param dcp The player + * @param needsSync Whether we're in an async thread + */ + public static void callLogoutEvent(DiscordConnectedPlayer dcp, boolean needsSync) { + if (!dcp.isLoggedIn()) return; + val event = new PlayerQuitEvent(dcp, ""); + if (needsSync) callEventSync(event); + else callEventExcludingSome(event); + dcp.setLoggedIn(false); + DPUtils.getLogger().info(dcp.getName() + " (" + dcp.getUniqueId() + ") logged out from Discord"); + } + + static void callEventSync(Event event) { + Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> callEventExcludingSome(event)); + } + @RequiredArgsConstructor public static class LastMsgData { - public IMessage message; + public Message message; public long time; public String content; - public final IChannel channel; - public Channel mcchannel; - public final IUser user; + public final MessageChannel channel; + public buttondevteam.core.component.channel.Channel mcchannel; + public final User user; } } diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java index 3ed7bcf..7c44627 100644 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java @@ -1,11 +1,12 @@ package buttondevteam.discordplugin.mcchat; import buttondevteam.discordplugin.*; -import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCSystemChatEvent; import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.player.*; import com.earth2me.essentials.CommandSource; +import discord4j.core.object.entity.Role; +import discord4j.core.object.util.Snowflake; import lombok.RequiredArgsConstructor; import lombok.val; import net.ess3.api.events.AfkStatusChangeEvent; @@ -17,16 +18,14 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.PlayerDeathEvent; -import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerKickEvent; import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerLoginEvent.Result; -import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.server.BroadcastMessageEvent; -import sx.blah.discord.handle.obj.IRole; -import sx.blah.discord.handle.obj.IUser; -import sx.blah.discord.util.DiscordException; -import sx.blah.discord.util.MissingPermissionsException; +import reactor.core.publisher.Mono; + +import java.util.Objects; +import java.util.Optional; @RequiredArgsConstructor class MCListener implements Listener { @@ -36,9 +35,11 @@ class MCListener implements Listener { public void onPlayerLogin(PlayerLoginEvent e) { if (e.getResult() != Result.ALLOWED) return; + if (e.getPlayer() instanceof DiscordConnectedPlayer) + return; MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream()) //Only private mcchat should be in ConnectedSenders .filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny() - .ifPresent(dcp -> MCChatUtils.callEventExcludingSome(new PlayerQuitEvent(dcp, ""))); + .ifPresent(dcp -> MCChatUtils.callLogoutEvent(dcp, false)); } @EventHandler(priority = EventPriority.LOWEST) @@ -49,11 +50,11 @@ class MCListener implements Listener { final Player p = e.getPlayer(); DiscordPlayer dp = e.GetPlayer().getAs(DiscordPlayer.class); if (dp != null) { - val user = DiscordPlugin.dc.getUserByID(Long.parseLong(dp.getDiscordID())); + val user = DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID())).block(); MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(), - new DiscordPlayerSender(user, user.getOrCreatePMChannel(), p)); + new DiscordPlayerSender(user, Objects.requireNonNull(user).getPrivateChannel().block(), p)); //TODO: Don't block MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(), - new DiscordPlayerSender(user, module.chatChannel().get(), p)); //Stored per-channel + new DiscordPlayerSender(user, module.chatChannelMono().block(), p)); //Stored per-channel } final String message = e.GetPlayer().PlayerName().get() + " joined the game"; MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true); @@ -67,10 +68,10 @@ class MCListener implements Listener { return; // Only care about real users MCChatUtils.OnlineSenders.entrySet() .removeIf(entry -> entry.getValue().entrySet().stream().anyMatch(p -> p.getValue().getUniqueId().equals(e.getPlayer().getUniqueId()))); - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, + Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream()) .filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny() - .ifPresent(dcp -> MCChatUtils.callEventExcludingSome(new PlayerJoinEvent(dcp, "")))); + .ifPresent(MCChatUtils::callLoginEvents)); Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, ChromaBot.getInstance()::updatePlayerList, 5); final String message = e.GetPlayer().PlayerName().get() + " left the game"; @@ -99,38 +100,34 @@ class MCListener implements Listener { MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(msg), base, ChannelconBroadcast.AFK, false); } - private ConfigData muteRole() { + private ConfigData> muteRole() { return DPUtils.roleData(module.getConfig(), "muteRole", "Muted"); } @EventHandler public void onPlayerMute(MuteStatusChangeEvent e) { - try { - DPUtils.performNoWait(() -> { - final IRole role = muteRole().get(); - if (role == null) return; - final CommandSource source = e.getAffected().getSource(); - if (!source.isPlayer()) - return; - final DiscordPlayer p = TBMCPlayerBase.getPlayer(source.getPlayer().getUniqueId(), TBMCPlayer.class) - .getAs(DiscordPlayer.class); - if (p == null) return; - final IUser user = DiscordPlugin.dc.getUserByID( - Long.parseLong(p.getDiscordID())); + final Mono role = muteRole().get(); + if (role == null) return; + final CommandSource source = e.getAffected().getSource(); + if (!source.isPlayer()) + return; + final DiscordPlayer p = TBMCPlayerBase.getPlayer(source.getPlayer().getUniqueId(), TBMCPlayer.class) + .getAs(DiscordPlayer.class); + if (p == null) return; + DiscordPlugin.dc.getUserById(Snowflake.of(p.getDiscordID())) + .flatMap(user -> user.asMember(DiscordPlugin.mainServer.getId())) + .flatMap(user -> role.flatMap(r -> { if (e.getValue()) - user.addRole(role); + user.addRole(r.getId()); else - user.removeRole(role); + user.removeRole(r.getId()); val modlog = module.modlogChannel().get(); - String msg = (e.getValue() ? "M" : "Unm") + "uted user: " + user.getName(); - if (modlog != null) - DiscordPlugin.sendMessageToChannel(modlog, msg); + String msg = (e.getValue() ? "M" : "Unm") + "uted user: " + user.getUsername() + "#" + user.getDiscriminator(); DPUtils.getLogger().info(msg); - }); - } catch (DiscordException | MissingPermissionsException ex) { - TBMCCoreAPI.SendException("Failed to give/take Muted role to player " + e.getAffected().getName() + "!", - ex); - } + if (modlog != null) + return modlog.flatMap(ch -> ch.createMessage(msg)); + return Mono.empty(); + })).subscribe(); } @EventHandler @@ -148,8 +145,9 @@ class MCListener implements Listener { String name = event.getSender() instanceof Player ? ((Player) event.getSender()).getDisplayName() : event.getSender().getName(); //Channel channel = ChromaGamerBase.getFromSender(event.getSender()).channel().get(); - TODO - val yeehaw = DiscordPlugin.mainServer.getEmojiByName("YEEHAW"); - MCChatUtils.forAllMCChat(MCChatUtils.send(name + (yeehaw != null ? " <:YEEHAW:" + yeehaw.getStringID() + ">s" : " YEEHAWs"))); + DiscordPlugin.mainServer.getEmojis().filter(e -> "YEEHAW".equals(e.getName())) + .take(1).singleOrEmpty().map(Optional::of).defaultIfEmpty(Optional.empty()).subscribe(yeehaw -> + MCChatUtils.forAllMCChat(MCChatUtils.send(name + (yeehaw.map(guildEmoji -> " <:YEEHAW:" + guildEmoji.getId().asString() + ">s").orElse(" YEEHAWs"))))); } @EventHandler diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MinecraftChatModule.java b/src/main/java/buttondevteam/discordplugin/mcchat/MinecraftChatModule.java index 509ea66..28e1e72 100644 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MinecraftChatModule.java +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MinecraftChatModule.java @@ -1,18 +1,23 @@ package buttondevteam.discordplugin.mcchat; +import buttondevteam.core.MainPlugin; import buttondevteam.core.component.channel.Channel; import buttondevteam.discordplugin.DPUtils; import buttondevteam.discordplugin.DiscordConnectedPlayer; import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.discordplugin.playerfaker.perm.LPInjector; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCSystemChatEvent; import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.ConfigData; +import buttondevteam.lib.architecture.ReadOnlyConfigData; import com.google.common.collect.Lists; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.util.Snowflake; import lombok.Getter; import lombok.val; import org.bukkit.Bukkit; -import sx.blah.discord.handle.obj.IChannel; +import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.Objects; @@ -23,11 +28,12 @@ import java.util.stream.Collectors; * Provides Minecraft chat connection to Discord. Commands may be used either in a public chat (limited) or in a DM. */ public class MinecraftChatModule extends Component { - private @Getter MCChatListener listener; + private @Getter + MCChatListener listener; - public MCChatListener getListener() { //It doesn't want to generate - return listener; - } + /*public MCChatListener getListener() { //It doesn't want to generate + return listener; - And now ButtonProcessor didn't look beyond this - return instead of continue... + }*/ /** * A list of commands that can be used in public chats - Warning: Some plugins will treat players as OPs, always test before allowing a command! @@ -40,33 +46,46 @@ public class MinecraftChatModule extends Component { /** * The channel to use as the public Minecraft chat - everything public gets broadcasted here */ - public ConfigData chatChannel() { - return DPUtils.channelData(getConfig(), "chatChannel", 239519012529111040L); + public ConfigData chatChannel() { + return DPUtils.snowflakeData(getConfig(), "chatChannel", 239519012529111040L); + } + + public Mono chatChannelMono() { + return DPUtils.getMessageChannel(chatChannel().getPath(), chatChannel().get()); } /** * The channel where the plugin can log when it mutes a player on Discord because of a Minecraft mute */ - public ConfigData modlogChannel() { + public ReadOnlyConfigData> modlogChannel() { return DPUtils.channelData(getConfig(), "modlogChannel", 283840717275791360L); } /** - * 0 * The plugins to exclude from fake player events used for the 'mcchat' command - some plugins may crash, add them here + * The plugins to exclude from fake player events used for the 'mcchat' command - some plugins may crash, add them here */ public ConfigData excludedPlugins() { return getConfig().getData("excludedPlugins", new String[]{"ProtocolLib", "LibsDisguises", "JourneyMapServer"}); } + /** + * If this setting is on then players logged in through the 'mcchat' command will be able to teleport using plugin commands. + * They can then use commands like /tpahere to teleport others to that place.
+ * If this is off, then teleporting will have no effect. + */ + public ConfigData allowFakePlayerTeleports() { + return getConfig().getData("allowFakePlayerTeleports", false); + } + @Override protected void enable() { - if (DPUtils.disableIfConfigError(this, chatChannel())) return; + if (DPUtils.disableIfConfigErrorRes(this, chatChannel(), chatChannelMono())) + return; listener = new MCChatListener(this); - DiscordPlugin.dc.getDispatcher().registerListener(listener); TBMCCoreAPI.RegisterEventsForExceptions(listener, getPlugin()); TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(this), getPlugin());//These get undone if restarting/resetting - it will ignore events if disabled getPlugin().getManager().registerCommand(new MCChatCommand()); - getPlugin().getManager().registerCommand(new ChannelconCommand()); + getPlugin().getManager().registerCommand(new ChannelconCommand(this)); val chcons = getConfig().getConfig().getConfigurationSection("chcons"); if (chcons == null) //Fallback to old place @@ -76,20 +95,28 @@ public class MinecraftChatModule extends Component { for (val chconkey : chconkeys) { val chcon = chcons.getConfigurationSection(chconkey); val mcch = Channel.getChannels().filter(ch -> ch.ID.equals(chcon.getString("mcchid"))).findAny(); - val ch = DiscordPlugin.dc.getChannelByID(chcon.getLong("chid")); + val ch = DiscordPlugin.dc.getChannelById(Snowflake.of(chcon.getLong("chid"))).block(); val did = chcon.getLong("did"); - val user = DiscordPlugin.dc.fetchUser(did); + val user = DiscordPlugin.dc.getUserById(Snowflake.of(did)).block(); val groupid = chcon.getString("groupid"); val toggles = chcon.getInt("toggles"); val brtoggles = chcon.getStringList("brtoggles"); if (!mcch.isPresent() || ch == null || user == null || groupid == null) continue; Bukkit.getScheduler().runTask(getPlugin(), () -> { //<-- Needed because of occasional ConcurrentModificationExceptions when creating the player (PermissibleBase) - val dcp = new DiscordConnectedPlayer(user, ch, UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname")); - MCChatCustom.addCustomChat(ch, groupid, mcch.get(), user, dcp, toggles, brtoggles.stream().map(TBMCSystemChatEvent.BroadcastTarget::get).filter(Objects::nonNull).collect(Collectors.toSet())); + val dcp = new DiscordConnectedPlayer(user, (MessageChannel) ch, UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname"), this); + MCChatCustom.addCustomChat((MessageChannel) ch, groupid, mcch.get(), user, dcp, toggles, brtoggles.stream().map(TBMCSystemChatEvent.BroadcastTarget::get).filter(Objects::nonNull).collect(Collectors.toSet())); }); } } + + try { + new LPInjector(MainPlugin.Instance); + } catch (Exception e) { + TBMCCoreAPI.SendException("Failed to init LuckPerms injector", e); + } catch (NoClassDefFoundError e) { + getPlugin().getLogger().info("No LuckPerms, not injecting"); + } } @Override @@ -97,10 +124,10 @@ public class MinecraftChatModule extends Component { val chcons = MCChatCustom.getCustomChats(); val chconsc = getConfig().getConfig().createSection("chcons"); for (val chcon : chcons) { - val chconc = chconsc.createSection(chcon.channel.getStringID()); + val chconc = chconsc.createSection(chcon.channel.getId().asString()); chconc.set("mcchid", chcon.mcchannel.ID); - chconc.set("chid", chcon.channel.getLongID()); - chconc.set("did", chcon.user.getLongID()); + chconc.set("chid", chcon.channel.getId().asLong()); + chconc.set("did", chcon.user.getId().asLong()); chconc.set("mcuid", chcon.dcp.getUniqueId().toString()); chconc.set("mcname", chcon.dcp.getName()); chconc.set("groupid", chcon.groupID); diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/AcceptMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/AcceptMCCommand.java deleted file mode 100755 index 29ed176..0000000 --- a/src/main/java/buttondevteam/discordplugin/mccommands/AcceptMCCommand.java +++ /dev/null @@ -1,44 +0,0 @@ -package buttondevteam.discordplugin.mccommands; - -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.commands.ConnectCommand; -import buttondevteam.discordplugin.mcchat.MCChatUtils; -import buttondevteam.lib.chat.CommandClass; -import buttondevteam.lib.player.ChromaGamerBase; -import buttondevteam.lib.player.TBMCPlayer; -import buttondevteam.lib.player.TBMCPlayerBase; -import org.bukkit.entity.Player; - -@CommandClass(modOnly = false, path = "accept") -public class AcceptMCCommand extends DiscordMCCommandBase { - - @Override - public String[] GetHelpText(String alias) { - return new String[] { // - "§6---- Accept Discord connection ----", // - "Accept a pending connection between your Discord and Minecraft account.", // - "To start the connection process, do §b/connect §r in the " + DPUtils.botmention() + " channel on Discord", // - "Usage: /" + alias + " accept" // - }; - } - - @Override - public boolean OnCommand(Player player, String alias, String[] args) { - String did = ConnectCommand.WaitingToConnect.get(player.getName()); - if (did == null) { - player.sendMessage("§cYou don't have a pending connection to Discord."); - return true; - } - DiscordPlayer dp = ChromaGamerBase.getUser(did, DiscordPlayer.class); - TBMCPlayer mcp = TBMCPlayerBase.getPlayer(player.getUniqueId(), TBMCPlayer.class); - dp.connectWith(mcp); - dp.save(); - mcp.save(); - ConnectCommand.WaitingToConnect.remove(player.getName()); - MCChatUtils.UnconnectedSenders.remove(did); //Remove all unconnected, will be recreated where needed - player.sendMessage("§bAccounts connected."); - return true; - } - -} diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/DeclineMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/DeclineMCCommand.java deleted file mode 100755 index 813d6b9..0000000 --- a/src/main/java/buttondevteam/discordplugin/mccommands/DeclineMCCommand.java +++ /dev/null @@ -1,32 +0,0 @@ -package buttondevteam.discordplugin.mccommands; - -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.commands.ConnectCommand; -import buttondevteam.lib.chat.CommandClass; -import org.bukkit.entity.Player; - -@CommandClass(modOnly = false, path = "decline") -public class DeclineMCCommand extends DiscordMCCommandBase { - - @Override - public String[] GetHelpText(String alias) { - return new String[] { // - "§6---- Decline Discord connection ----", // - "Decline a pending connection between your Discord and Minecraft account.", // - "To start the connection process, do §b/connect §r in the " + DPUtils.botmention() + " channel on Discord", // - "Usage: /" + alias + " decline" // - }; - } - - @Override - public boolean OnCommand(Player player, String alias, String[] args) { - String did = ConnectCommand.WaitingToConnect.remove(player.getName()); - if (did == null) { - player.sendMessage("§cYou don't have a pending connection to Discord."); - return true; - } - player.sendMessage("§bPending connection declined."); - return true; - } - -} diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java new file mode 100644 index 0000000..29eb5ce --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java @@ -0,0 +1,131 @@ +package buttondevteam.discordplugin.mccommands; + +import buttondevteam.discordplugin.DPUtils; +import buttondevteam.discordplugin.DiscordPlayer; +import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.discordplugin.DiscordSenderBase; +import buttondevteam.discordplugin.commands.ConnectCommand; +import buttondevteam.discordplugin.commands.VersionCommand; +import buttondevteam.discordplugin.mcchat.MCChatUtils; +import buttondevteam.lib.chat.Command2; +import buttondevteam.lib.chat.CommandClass; +import buttondevteam.lib.chat.ICommand2MC; +import buttondevteam.lib.player.ChromaGamerBase; +import buttondevteam.lib.player.TBMCPlayer; +import buttondevteam.lib.player.TBMCPlayerBase; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import reactor.core.publisher.Mono; + +import java.lang.reflect.Method; + +@CommandClass(path = "discord", helpText = { + "Discord", + "This command allows performing Discord-related actions." +}) +public class DiscordMCCommand extends ICommand2MC { + @Command2.Subcommand + public boolean accept(Player player) { + String did = ConnectCommand.WaitingToConnect.get(player.getName()); + if (did == null) { + player.sendMessage("§cYou don't have a pending connection to Discord."); + return true; + } + DiscordPlayer dp = ChromaGamerBase.getUser(did, DiscordPlayer.class); + TBMCPlayer mcp = TBMCPlayerBase.getPlayer(player.getUniqueId(), TBMCPlayer.class); + dp.connectWith(mcp); + dp.save(); + mcp.save(); + ConnectCommand.WaitingToConnect.remove(player.getName()); + MCChatUtils.UnconnectedSenders.remove(did); //Remove all unconnected, will be recreated where needed + player.sendMessage("§bAccounts connected."); + return true; + } + + @Command2.Subcommand + public boolean decline(Player player) { + String did = ConnectCommand.WaitingToConnect.remove(player.getName()); + if (did == null) { + player.sendMessage("§cYou don't have a pending connection to Discord."); + return true; + } + player.sendMessage("§bPending connection declined."); + return true; + } + + @Command2.Subcommand(permGroup = Command2.Subcommand.MOD_GROUP, helpText = { + "Reload Discord plugin", + "Reloads the config. To apply some changes, you may need to also run /discord reset." + }) + public void reload(CommandSender sender) { + if (DiscordPlugin.plugin.tryReloadConfig()) + sender.sendMessage("§bConfig reloaded."); + else + sender.sendMessage("§cFailed to reload config."); + } + + public static boolean resetting = false; + + @Command2.Subcommand(permGroup = Command2.Subcommand.MOD_GROUP, helpText = { + "Reset ChromaBot", // + "This command disables and then enables the plugin." // + }) + public void reset(CommandSender sender) { + Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> { + resetting = true; //Turned off after sending enable message (ReadyEvent) + sender.sendMessage("§bDisabling DiscordPlugin..."); + Bukkit.getPluginManager().disablePlugin(DiscordPlugin.plugin); + if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors + sender.sendMessage("§bEnabling DiscordPlugin..."); + Bukkit.getPluginManager().enablePlugin(DiscordPlugin.plugin); + if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors + sender.sendMessage("§bReset finished!"); + }); + } + + @Command2.Subcommand(helpText = { + "Version command", + "Prints the plugin version" + }) + public void version(CommandSender sender) { + sender.sendMessage(VersionCommand.getVersion()); + } + + @Command2.Subcommand(helpText = { + "Invite", + "Shows an invite link to the server" + }) + public void invite(CommandSender sender) { + String invi = DiscordPlugin.plugin.inviteLink().get(); + if (invi.length() > 0) { + sender.sendMessage("§bInvite link: " + invi); + return; + } + DiscordPlugin.mainServer.getInvites().limitRequest(1) + .switchIfEmpty(Mono.fromRunnable(() -> sender.sendMessage("§cNo invites found for the server."))) + .subscribe(inv -> { + sender.sendMessage("§bInvite link: https://discord.gg/" + inv.getCode()); + }, e -> sender.sendMessage("§cThe invite link is not set and the bot has no permission to get it.")); + } + + @Override + public String[] getHelpText(Method method, Command2.Subcommand ann) { + switch (method.getName()) { + case "accept": + return new String[]{ // + "Accept Discord connection", // + "Accept a pending connection between your Discord and Minecraft account.", // + "To start the connection process, do §b/connect §r in the " + DPUtils.botmention() + " channel on Discord", // + }; + case "decline": + return new String[]{ // + "Decline Discord connection", // + "Decline a pending connection between your Discord and Minecraft account.", // + "To start the connection process, do §b/connect §r in the " + DPUtils.botmention() + " channel on Discord", // + }; + default: + return super.getHelpText(method, ann); + } + } +} diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommandBase.java b/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommandBase.java deleted file mode 100755 index 5edbafe..0000000 --- a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommandBase.java +++ /dev/null @@ -1,9 +0,0 @@ -package buttondevteam.discordplugin.mccommands; - -import buttondevteam.lib.chat.CommandClass; -import buttondevteam.lib.chat.PlayerCommandBase; - -@CommandClass(modOnly = false, path = "discord") -public abstract class DiscordMCCommandBase extends PlayerCommandBase { - -} diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/ReloadMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/ReloadMCCommand.java deleted file mode 100644 index 01433c9..0000000 --- a/src/main/java/buttondevteam/discordplugin/mccommands/ReloadMCCommand.java +++ /dev/null @@ -1,26 +0,0 @@ -package buttondevteam.discordplugin.mccommands; - -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.chat.CommandClass; -import buttondevteam.lib.chat.TBMCCommandBase; -import org.bukkit.command.CommandSender; - -@CommandClass(path = "discord reload") -public class ReloadMCCommand extends TBMCCommandBase { - @Override - public boolean OnCommand(CommandSender sender, String alias, String[] args) { - if (DiscordPlugin.plugin.tryReloadConfig()) - sender.sendMessage("§bConfig reloaded."); //TODO: Convert to new command system - else - sender.sendMessage("§cFailed to reload config."); - return true; - } - - @Override - public String[] GetHelpText(String alias) { - return new String[]{ - "Reload", - "Reloads the config. To apply some changes, you may need to also run /discord reset." - }; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/ResetMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/ResetMCCommand.java deleted file mode 100644 index 1f4855f..0000000 --- a/src/main/java/buttondevteam/discordplugin/mccommands/ResetMCCommand.java +++ /dev/null @@ -1,35 +0,0 @@ -package buttondevteam.discordplugin.mccommands; - -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.DiscordSenderBase; -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 - public static boolean resetting = false; - @Override - public boolean OnCommand(CommandSender sender, String s, String[] strings) { - Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> { - resetting = true; //Turned off after sending enable message (ReadyEvent) - sender.sendMessage("§bDisabling DiscordPlugin..."); - Bukkit.getPluginManager().disablePlugin(DiscordPlugin.plugin); - if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors - sender.sendMessage("§bEnabling DiscordPlugin..."); - Bukkit.getPluginManager().enablePlugin(DiscordPlugin.plugin); - if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors - sender.sendMessage("§bReset finished!"); - }); - return true; - } - - @Override - public String[] GetHelpText(String s) { - return new String[]{ // - "§6---- Reset ChromaBot ----", // - "This command disables and then enables the plugin." // - }; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/VersionMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/VersionMCCommand.java deleted file mode 100644 index a2cacba..0000000 --- a/src/main/java/buttondevteam/discordplugin/mccommands/VersionMCCommand.java +++ /dev/null @@ -1,20 +0,0 @@ -package buttondevteam.discordplugin.mccommands; - -import buttondevteam.discordplugin.commands.VersionCommand; -import buttondevteam.lib.chat.CommandClass; -import buttondevteam.lib.chat.TBMCCommandBase; -import org.bukkit.command.CommandSender; - -@CommandClass(path = "discord version") -public class VersionMCCommand extends TBMCCommandBase { - @Override - public boolean OnCommand(CommandSender commandSender, String s, String[] strings) { - commandSender.sendMessage(VersionCommand.getVersion()); - return true; - } - - @Override - public String[] GetHelpText(String s) { - return VersionCommand.getVersion(); //Heh - } -} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordEntity.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordEntity.java index 6ce85f8..9a75717 100755 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordEntity.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordEntity.java @@ -1,6 +1,10 @@ package buttondevteam.discordplugin.playerfaker; +import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.DiscordSenderBase; +import buttondevteam.discordplugin.mcchat.MinecraftChatModule; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.User; import lombok.Getter; import lombok.Setter; import org.bukkit.*; @@ -11,8 +15,6 @@ import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.metadata.MetadataValue; import org.bukkit.plugin.Plugin; import org.bukkit.util.Vector; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IUser; import java.util.*; @@ -20,10 +22,11 @@ import java.util.*; @Setter @SuppressWarnings("deprecated") public abstract class DiscordEntity extends DiscordSenderBase implements Entity { - protected DiscordEntity(IUser user, IChannel channel, int entityId, UUID uuid) { + protected DiscordEntity(User user, MessageChannel channel, int entityId, UUID uuid, MinecraftChatModule module) { super(user, channel); this.entityId = entityId; uniqueId = uuid; + this.module = module; } private HashMap metadata = new HashMap(); @@ -34,6 +37,7 @@ public abstract class DiscordEntity extends DiscordSenderBase implements Entity private EntityDamageEvent lastDamageCause; private final Set scoreboardTags = new HashSet(); private final UUID uniqueId; + private final MinecraftChatModule module; @Override public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { @@ -42,7 +46,7 @@ public abstract class DiscordEntity extends DiscordSenderBase implements Entity @Override public List getMetadata(String metadataKey) { - return Arrays.asList(metadata.get(metadataKey)); // Who needs multiple data anyways + return Collections.singletonList(metadata.get(metadataKey)); // Who needs multiple data anyways } @Override @@ -91,31 +95,35 @@ public abstract class DiscordEntity extends DiscordSenderBase implements Entity @Override public boolean teleport(Location location) { - this.location = location; + if (module.allowFakePlayerTeleports().get()) + this.location = location; return true; } @Override public boolean teleport(Location location, TeleportCause cause) { - this.location = location; + if (module.allowFakePlayerTeleports().get()) + this.location = location; return true; } @Override public boolean teleport(Entity destination) { - this.location = destination.getLocation(); + if (module.allowFakePlayerTeleports().get()) + this.location = destination.getLocation(); return true; } @Override public boolean teleport(Entity destination, TeleportCause cause) { - this.location = destination.getLocation(); + if (module.allowFakePlayerTeleports().get()) + this.location = destination.getLocation(); return true; } @Override public List getNearbyEntities(double x, double y, double z) { - return Arrays.asList(); + return Collections.emptyList(); } @Override @@ -163,7 +171,7 @@ public abstract class DiscordEntity extends DiscordSenderBase implements Entity @Override public List getPassengers() { - return Arrays.asList(); + return Collections.emptyList(); } @Override diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordFakePlayer.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordFakePlayer.java index a2b4a13..6879155 100755 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordFakePlayer.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordFakePlayer.java @@ -1,7 +1,11 @@ package buttondevteam.discordplugin.playerfaker; import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.discordplugin.mcchat.MinecraftChatModule; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.User; import lombok.Getter; +import lombok.Setter; import lombok.experimental.Delegate; import org.bukkit.*; import org.bukkit.advancement.Advancement; @@ -14,704 +18,716 @@ import org.bukkit.entity.Player; import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.map.MapView; import org.bukkit.permissions.PermissibleBase; +import org.bukkit.permissions.ServerOperator; import org.bukkit.plugin.Plugin; import org.bukkit.scoreboard.Scoreboard; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IUser; import java.net.InetSocketAddress; import java.util.*; @SuppressWarnings("deprecation") public class DiscordFakePlayer extends DiscordHumanEntity implements Player { - protected DiscordFakePlayer(IUser user, IChannel channel, int entityId, UUID uuid, String mcname) { - super(user, channel, entityId, uuid); - perm = new PermissibleBase(Bukkit.getOfflinePlayer(uuid)); - name = mcname; - } - - @Delegate - private PermissibleBase perm; - - private @Getter String name; - - @Override - public EntityType getType() { - return EntityType.PLAYER; - } - - @Override - public String getCustomName() { - return user.getName(); - } - - @Override - public void setCustomName(String name) { - } - - @Override - public boolean isConversing() { - - return false; - } - - @Override - public void acceptConversationInput(String input) { - } - - @Override - public boolean beginConversation(Conversation conversation) { - return false; - } - - @Override - public void abandonConversation(Conversation conversation) { - } - - @Override - public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) { - } - - @Override - public boolean isOnline() { - return true;// Let's pretend - } - - @Override - public boolean isBanned() { - return false; - } - - @Override - public boolean isWhitelisted() { - return true; - } - - @Override - public void setWhitelisted(boolean value) { - } - - @Override - public Player getPlayer() { - return this; - } - - @Override - public long getFirstPlayed() { - return 0; - } - - @Override - public long getLastPlayed() { - return 0; - } - - @Override - public boolean hasPlayedBefore() { - return false; - } - - @Override - public Map serialize() { - return new HashMap<>(); - } - - @Override - public void sendPluginMessage(Plugin source, String channel, byte[] message) { - } - - @Override - public Set getListeningPluginChannels() { - return Collections.emptySet(); - } - - @Override - public String getDisplayName() { - return user.getDisplayName(DiscordPlugin.mainServer); - } - - @Override - public void setDisplayName(String name) { - } - - @Override - public String getPlayerListName() { - return getName(); - } - - @Override - public void setPlayerListName(String name) { - } - - @Override - public void setCompassTarget(Location loc) { - } - - @Override - public Location getCompassTarget() { - return new Location(Bukkit.getWorlds().get(0), 0, 0, 0); - } - - @Override - public InetSocketAddress getAddress() { - return null; - } - - @Override - public void sendRawMessage(String message) { - sendMessage(message); - } - - @Override - public void kickPlayer(String message) { - } - - @Override - public void chat(String msg) { - Bukkit.getPluginManager() - .callEvent(new AsyncPlayerChatEvent(true, this, msg, new HashSet<>(Bukkit.getOnlinePlayers()))); - } - - @Override - public boolean performCommand(String command) { - return Bukkit.getServer().dispatchCommand(this, command); - } - - @Override - public boolean isSneaking() { - return false; - } - - @Override - public void setSneaking(boolean sneak) { - } - - @Override - public boolean isSprinting() { - return false; - } - - @Override - public void setSprinting(boolean sprinting) { - } - - @Override - public void saveData() { - } - - @Override - public void loadData() { - } - - @Override - public void setSleepingIgnored(boolean isSleeping) { - } - - @Override - public boolean isSleepingIgnored() { - return false; - } - - @Override - public void playNote(Location loc, byte instrument, byte note) { - } - - @Override - public void playNote(Location loc, Instrument instrument, Note note) { - } - - @Override - public void playSound(Location location, Sound sound, float volume, float pitch) { - } + protected DiscordFakePlayer(User user, MessageChannel channel, int entityId, UUID uuid, String mcname, MinecraftChatModule module) { + super(user, channel, entityId, uuid, module); + origPerm = perm = new PermissibleBase(basePlayer = Bukkit.getOfflinePlayer(uuid)); + name = mcname; + } + + @Delegate(excludes = ServerOperator.class) + private PermissibleBase origPerm; + + private @Getter String name; + + private @Getter OfflinePlayer basePlayer; + + @Getter + @Setter + private PermissibleBase perm; + + public void setOp(boolean value) { //CraftPlayer-compatible implementation + this.origPerm.setOp(value); + this.perm.recalculatePermissions(); + } + + public boolean isOp() { return this.origPerm.isOp(); } + + @Override + public EntityType getType() { + return EntityType.PLAYER; + } + + @Override + public String getCustomName() { + return user.getUsername(); + } + + @Override + public void setCustomName(String name) { + } + + @Override + public boolean isConversing() { + + return false; + } + + @Override + public void acceptConversationInput(String input) { + } + + @Override + public boolean beginConversation(Conversation conversation) { + return false; + } + + @Override + public void abandonConversation(Conversation conversation) { + } + + @Override + public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) { + } + + @Override + public boolean isOnline() { + return true;// Let's pretend + } + + @Override + public boolean isBanned() { + return false; + } + + @Override + public boolean isWhitelisted() { + return true; + } + + @Override + public void setWhitelisted(boolean value) { + } + + @Override + public Player getPlayer() { + return this; + } + + @Override + public long getFirstPlayed() { + return 0; + } + + @Override + public long getLastPlayed() { + return 0; + } + + @Override + public boolean hasPlayedBefore() { + return false; + } + + @Override + public Map serialize() { + return new HashMap<>(); + } + + @Override + public void sendPluginMessage(Plugin source, String channel, byte[] message) { + } + + @Override + public Set getListeningPluginChannels() { + return Collections.emptySet(); + } + + @Override + public String getDisplayName() { + return Objects.requireNonNull(user.asMember(DiscordPlugin.mainServer.getId()).block()).getDisplayName(); + } + + @Override + public void setDisplayName(String name) { + } + + @Override + public String getPlayerListName() { + return getName(); + } + + @Override + public void setPlayerListName(String name) { + } + + @Override + public void setCompassTarget(Location loc) { + } + + @Override + public Location getCompassTarget() { + return new Location(Bukkit.getWorlds().get(0), 0, 0, 0); + } + + @Override + public InetSocketAddress getAddress() { + return null; + } + + @Override + public void sendRawMessage(String message) { + sendMessage(message); + } + + @Override + public void kickPlayer(String message) { + } + + @Override + public void chat(String msg) { + Bukkit.getPluginManager() + .callEvent(new AsyncPlayerChatEvent(true, this, msg, new HashSet<>(Bukkit.getOnlinePlayers()))); + } + + @Override + public boolean performCommand(String command) { + return Bukkit.getServer().dispatchCommand(this, command); + } + + @Override + public boolean isSneaking() { + return false; + } + + @Override + public void setSneaking(boolean sneak) { + } + + @Override + public boolean isSprinting() { + return false; + } + + @Override + public void setSprinting(boolean sprinting) { + } + + @Override + public void saveData() { + } + + @Override + public void loadData() { + } + + @Override + public void setSleepingIgnored(boolean isSleeping) { + } + + @Override + public boolean isSleepingIgnored() { + return false; + } + + @Override + public void playNote(Location loc, byte instrument, byte note) { + } + + @Override + public void playNote(Location loc, Instrument instrument, Note note) { + } + + @Override + public void playSound(Location location, Sound sound, float volume, float pitch) { + } - @Override - public void playSound(Location location, String sound, float volume, float pitch) { - } + @Override + public void playSound(Location location, String sound, float volume, float pitch) { + } - @Override - public void playSound(Location location, Sound sound, SoundCategory category, float volume, float pitch) { - } + @Override + public void playSound(Location location, Sound sound, SoundCategory category, float volume, float pitch) { + } - @Override - public void playSound(Location location, String sound, SoundCategory category, float volume, float pitch) { - } + @Override + public void playSound(Location location, String sound, SoundCategory category, float volume, float pitch) { + } - @Override - public void stopSound(Sound sound) { - } + @Override + public void stopSound(Sound sound) { + } - @Override - public void stopSound(String sound) { - } + @Override + public void stopSound(String sound) { + } - @Override - public void stopSound(Sound sound, SoundCategory category) { - } + @Override + public void stopSound(Sound sound, SoundCategory category) { + } - @Override - public void stopSound(String sound, SoundCategory category) { - } + @Override + public void stopSound(String sound, SoundCategory category) { + } - @Override - public void playEffect(Location loc, Effect effect, int data) { - } + @Override + public void playEffect(Location loc, Effect effect, int data) { + } - @Override - public void playEffect(Location loc, Effect effect, T data) { - } + @Override + public void playEffect(Location loc, Effect effect, T data) { + } - @Override - public void sendBlockChange(Location loc, Material material, byte data) { - } + @Override + public void sendBlockChange(Location loc, Material material, byte data) { + } - @Override - public boolean sendChunkChange(Location loc, int sx, int sy, int sz, byte[] data) { - return false; - } + @Override + public boolean sendChunkChange(Location loc, int sx, int sy, int sz, byte[] data) { + return false; + } - @Override - public void sendBlockChange(Location loc, int material, byte data) { - } + @Override + public void sendBlockChange(Location loc, int material, byte data) { + } - @Override - public void sendSignChange(Location loc, String[] lines) throws IllegalArgumentException { - } + @Override + public void sendSignChange(Location loc, String[] lines) throws IllegalArgumentException { + } - @Override - public void sendMap(MapView map) { - } + @Override + public void sendMap(MapView map) { + } - @Override - public void updateInventory() { - } + @Override + public void updateInventory() { + } - @Override - public void awardAchievement(@SuppressWarnings("deprecation") Achievement achievement) { - } + @Override + public void awardAchievement(@SuppressWarnings("deprecation") Achievement achievement) { + } - @Override - public void removeAchievement(@SuppressWarnings("deprecation") Achievement achievement) { - } + @Override + public void removeAchievement(@SuppressWarnings("deprecation") Achievement achievement) { + } - @Override - public boolean hasAchievement(@SuppressWarnings("deprecation") Achievement achievement) { - return false; - } + @Override + public boolean hasAchievement(@SuppressWarnings("deprecation") Achievement achievement) { + return false; + } - @Override - public void incrementStatistic(Statistic statistic) throws IllegalArgumentException { - } + @Override + public void incrementStatistic(Statistic statistic) throws IllegalArgumentException { + } - @Override - public void decrementStatistic(Statistic statistic) throws IllegalArgumentException { - } + @Override + public void decrementStatistic(Statistic statistic) throws IllegalArgumentException { + } - @Override - public void incrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException { + @Override + public void incrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException { - } + } - @Override - public void decrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException { + @Override + public void decrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException { - } + } - @Override - public void setStatistic(Statistic statistic, int newValue) throws IllegalArgumentException { + @Override + public void setStatistic(Statistic statistic, int newValue) throws IllegalArgumentException { - } + } - @Override - public int getStatistic(Statistic statistic) throws IllegalArgumentException { + @Override + public int getStatistic(Statistic statistic) throws IllegalArgumentException { - return 0; - } + return 0; + } - @Override - public void incrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + @Override + public void incrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { - } + } - @Override - public void decrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + @Override + public void decrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { - } + } - @Override - public int getStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + @Override + public int getStatistic(Statistic statistic, Material material) throws IllegalArgumentException { - return 0; - } + return 0; + } - @Override - public void incrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException { + @Override + public void incrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException { - } + } - @Override - public void decrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException { + @Override + public void decrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException { - } + } - @Override - public void setStatistic(Statistic statistic, Material material, int newValue) throws IllegalArgumentException { + @Override + public void setStatistic(Statistic statistic, Material material, int newValue) throws IllegalArgumentException { - } + } - @Override - public void incrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + @Override + public void incrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { - } + } - @Override - public void decrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + @Override + public void decrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { - } + } - @Override - public int getStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + @Override + public int getStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { - return 0; - } + return 0; + } - @Override - public void incrementStatistic(Statistic statistic, EntityType entityType, int amount) - throws IllegalArgumentException { + @Override + public void incrementStatistic(Statistic statistic, EntityType entityType, int amount) + throws IllegalArgumentException { - } + } - @Override - public void decrementStatistic(Statistic statistic, EntityType entityType, int amount) { + @Override + public void decrementStatistic(Statistic statistic, EntityType entityType, int amount) { - } + } - @Override - public void setStatistic(Statistic statistic, EntityType entityType, int newValue) { + @Override + public void setStatistic(Statistic statistic, EntityType entityType, int newValue) { - } + } - @Override - public void setPlayerTime(long time, boolean relative) { + @Override + public void setPlayerTime(long time, boolean relative) { - } + } - @Override - public long getPlayerTime() { + @Override + public long getPlayerTime() { - return 0; - } + return 0; + } - @Override - public long getPlayerTimeOffset() { + @Override + public long getPlayerTimeOffset() { - return 0; - } + return 0; + } - @Override - public boolean isPlayerTimeRelative() { + @Override + public boolean isPlayerTimeRelative() { - return false; - } + return false; + } - @Override - public void resetPlayerTime() { + @Override + public void resetPlayerTime() { - } + } - @Override - public void setPlayerWeather(WeatherType type) { + @Override + public void setPlayerWeather(WeatherType type) { - } + } - @Override - public WeatherType getPlayerWeather() { + @Override + public WeatherType getPlayerWeather() { - return null; - } + return null; + } - @Override - public void resetPlayerWeather() { + @Override + public void resetPlayerWeather() { - } + } - @Override - public void giveExp(int amount) { + @Override + public void giveExp(int amount) { - } + } - @Override - public void giveExpLevels(int amount) { + @Override + public void giveExpLevels(int amount) { - } + } - @Override - public float getExp() { + @Override + public float getExp() { - return 0; - } + return 0; + } - @Override - public void setExp(float exp) { + @Override + public void setExp(float exp) { - } + } - @Override - public int getLevel() { + @Override + public int getLevel() { - return 0; - } + return 0; + } - @Override - public void setLevel(int level) { + @Override + public void setLevel(int level) { - } + } - @Override - public int getTotalExperience() { + @Override + public int getTotalExperience() { - return 0; - } + return 0; + } - @Override - public void setTotalExperience(int exp) { + @Override + public void setTotalExperience(int exp) { - } + } - @Override - public float getExhaustion() { + @Override + public float getExhaustion() { - return 0; - } + return 0; + } - @Override - public void setExhaustion(float value) { + @Override + public void setExhaustion(float value) { - } + } - @Override - public float getSaturation() { + @Override + public float getSaturation() { - return 0; - } + return 0; + } - @Override - public void setSaturation(float value) { + @Override + public void setSaturation(float value) { - } + } - @Override - public int getFoodLevel() { + @Override + public int getFoodLevel() { - return 0; - } + return 0; + } - @Override - public void setFoodLevel(int value) { + @Override + public void setFoodLevel(int value) { - } + } - @Override - public Location getBedSpawnLocation() { - return null; - } + @Override + public Location getBedSpawnLocation() { + return null; + } - @Override - public void setBedSpawnLocation(Location location) { - } + @Override + public void setBedSpawnLocation(Location location) { + } - @Override - public void setBedSpawnLocation(Location location, boolean force) { - } + @Override + public void setBedSpawnLocation(Location location, boolean force) { + } - @Override - public boolean getAllowFlight() { - return false; - } + @Override + public boolean getAllowFlight() { + return false; + } - @Override - public void setAllowFlight(boolean flight) { - } + @Override + public void setAllowFlight(boolean flight) { + } - @Override - public void hidePlayer(Player player) { - } + @Override + public void hidePlayer(Player player) { + } - @Override - public void showPlayer(Player player) { - } + @Override + public void showPlayer(Player player) { + } - @Override - public boolean canSee(Player player) { // Nobody can see them - return false; - } + @Override + public boolean canSee(Player player) { // Nobody can see them + return false; + } - @Override - public boolean isFlying() { - return false; - } + @Override + public boolean isFlying() { + return false; + } - @Override - public void setFlying(boolean value) { - } + @Override + public void setFlying(boolean value) { + } - @Override - public void setFlySpeed(float value) throws IllegalArgumentException { - } + @Override + public void setFlySpeed(float value) throws IllegalArgumentException { + } - @Override - public void setWalkSpeed(float value) throws IllegalArgumentException { - } + @Override + public void setWalkSpeed(float value) throws IllegalArgumentException { + } - @Override - public float getFlySpeed() { - return 0; - } + @Override + public float getFlySpeed() { + return 0; + } - @Override - public float getWalkSpeed() { - return 0; - } + @Override + public float getWalkSpeed() { + return 0; + } - @Override - public void setTexturePack(String url) { - } + @Override + public void setTexturePack(String url) { + } - @Override - public void setResourcePack(String url) { - } + @Override + public void setResourcePack(String url) { + } - @Override - public void setResourcePack(String url, byte[] hash) { - } + @Override + public void setResourcePack(String url, byte[] hash) { + } - @Override - public Scoreboard getScoreboard() { - return null; - } + @Override + public Scoreboard getScoreboard() { + return null; + } - @Override - public void setScoreboard(Scoreboard scoreboard) throws IllegalArgumentException, IllegalStateException { - } + @Override + public void setScoreboard(Scoreboard scoreboard) throws IllegalArgumentException, IllegalStateException { + } - @Override - public boolean isHealthScaled() { - return false; - } + @Override + public boolean isHealthScaled() { + return false; + } - @Override - public void setHealthScaled(boolean scale) { - } + @Override + public void setHealthScaled(boolean scale) { + } - @Override - public void setHealthScale(double scale) throws IllegalArgumentException { - } + @Override + public void setHealthScale(double scale) throws IllegalArgumentException { + } - @Override - public double getHealthScale() { - return 1; - } + @Override + public double getHealthScale() { + return 1; + } - @Override - public Entity getSpectatorTarget() { - return null; - } + @Override + public Entity getSpectatorTarget() { + return null; + } - @Override - public void setSpectatorTarget(Entity entity) { - } + @Override + public void setSpectatorTarget(Entity entity) { + } - @Override - public void sendTitle(String title, String subtitle) { - } + @Override + public void sendTitle(String title, String subtitle) { + } - @Override - public void sendTitle(String title, String subtitle, int fadeIn, int stay, int fadeOut) { - } + @Override + public void sendTitle(String title, String subtitle, int fadeIn, int stay, int fadeOut) { + } - @Override - public void resetTitle() { - } + @Override + public void resetTitle() { + } - @Override - public void spawnParticle(Particle particle, Location location, int count) { - } + @Override + public void spawnParticle(Particle particle, Location location, int count) { + } - @Override - public void spawnParticle(Particle particle, double x, double y, double z, int count) { + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count) { - } + } - @Override - public void spawnParticle(Particle particle, Location location, int count, T data) { + @Override + public void spawnParticle(Particle particle, Location location, int count, T data) { - } + } - @Override - public void spawnParticle(Particle particle, double x, double y, double z, int count, T data) { + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, T data) { - } + } - @Override - public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, - double offsetZ) { + @Override + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, + double offsetZ) { - } + } - @Override - public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, - double offsetY, double offsetZ) { + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, + double offsetY, double offsetZ) { - } + } - @Override - public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, - double offsetZ, T data) { + @Override + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, + double offsetZ, T data) { - } + } - @Override - public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, - double offsetY, double offsetZ, T data) { + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, + double offsetY, double offsetZ, T data) { - } + } - @Override - public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, - double offsetZ, double extra) { + @Override + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, + double offsetZ, double extra) { - } + } - @Override - public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, - double offsetY, double offsetZ, double extra) { + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, + double offsetY, double offsetZ, double extra) { - } + } - @Override - public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, - double offsetZ, double extra, T data) { + @Override + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, + double offsetZ, double extra, T data) { - } + } - @Override - public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, - double offsetY, double offsetZ, double extra, T data) { + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, + double offsetY, double offsetZ, double extra, T data) { - } + } - @Override - public AdvancementProgress getAdvancementProgress(Advancement advancement) { // TODO: Test - return null; - } + @Override + public AdvancementProgress getAdvancementProgress(Advancement advancement) { // TODO: Test + return null; + } - @Override - public String getLocale() { + @Override + public String getLocale() { - return null; - } + return null; + } - @Override - public Player.Spigot spigot() { - return new Player.Spigot(); - } + @Override + public Player.Spigot spigot() { + return new Player.Spigot(); + } } diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordHumanEntity.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordHumanEntity.java index c1522f1..4b8b32d 100755 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordHumanEntity.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordHumanEntity.java @@ -1,5 +1,8 @@ package buttondevteam.discordplugin.playerfaker; +import buttondevteam.discordplugin.mcchat.MinecraftChatModule; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.User; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; @@ -8,14 +11,12 @@ import org.bukkit.entity.HumanEntity; import org.bukkit.entity.Villager; import org.bukkit.inventory.*; import org.bukkit.inventory.InventoryView.Property; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IUser; import java.util.UUID; public abstract class DiscordHumanEntity extends DiscordLivingEntity implements HumanEntity { - protected DiscordHumanEntity(IUser user, IChannel channel, int entityId, UUID uuid) { - super(user, channel, entityId, uuid); + protected DiscordHumanEntity(User user, MessageChannel channel, int entityId, UUID uuid, MinecraftChatModule module) { + super(user, channel, entityId, uuid, module); } private PlayerInventory inv = new DiscordPlayerInventory(this); diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordLivingEntity.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordLivingEntity.java index f261de4..c561fbf 100755 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordLivingEntity.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordLivingEntity.java @@ -1,5 +1,8 @@ package buttondevteam.discordplugin.playerfaker; +import buttondevteam.discordplugin.mcchat.MinecraftChatModule; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.User; import lombok.Getter; import lombok.Setter; import org.bukkit.Location; @@ -16,15 +19,13 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.util.Vector; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IUser; import java.util.*; public abstract class DiscordLivingEntity extends DiscordEntity implements LivingEntity { - protected DiscordLivingEntity(IUser user, IChannel channel, int entityId, UUID uuid) { - super(user, channel, entityId, uuid); + protected DiscordLivingEntity(User user, MessageChannel channel, int entityId, UUID uuid, MinecraftChatModule module) { + super(user, channel, entityId, uuid, module); } private @Getter EntityEquipment equipment = new DiscordEntityEquipment(this); diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java b/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java new file mode 100644 index 0000000..de5a84b --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java @@ -0,0 +1,240 @@ +package buttondevteam.discordplugin.playerfaker.perm; + +import buttondevteam.core.MainPlugin; +import buttondevteam.discordplugin.mcchat.MCChatUtils; +import buttondevteam.discordplugin.playerfaker.DiscordFakePlayer; +import buttondevteam.lib.TBMCCoreAPI; +import me.lucko.luckperms.bukkit.LPBukkitBootstrap; +import me.lucko.luckperms.bukkit.LPBukkitPlugin; +import me.lucko.luckperms.bukkit.inject.dummy.DummyPermissibleBase; +import me.lucko.luckperms.bukkit.inject.permissible.LPPermissible; +import me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.locale.message.Message; +import me.lucko.luckperms.common.model.User; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.permissions.PermissibleBase; +import org.bukkit.permissions.PermissionAttachment; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class LPInjector implements Listener { //Disable login event for LuckPerms + private LPBukkitPlugin plugin; + private BukkitConnectionListener connectionListener; + private Set deniedLogin; + private Field detectedCraftBukkitOfflineMode; + private Method printCraftBukkitOfflineModeError; + private Field PERMISSIBLE_BASE_ATTACHMENTS_FIELD; + private Method convertAndAddAttachments; + private Method getActive; + private Method setOldPermissible; + private Method getOldPermissible; + + public LPInjector(MainPlugin mp) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException { + LPBukkitBootstrap bs = (LPBukkitBootstrap) Bukkit.getPluginManager().getPlugin("LuckPerms"); + Field field = LPBukkitBootstrap.class.getDeclaredField("plugin"); + field.setAccessible(true); + plugin = (LPBukkitPlugin) field.get(bs); + MCChatUtils.addStaticExcludedPlugin(PlayerLoginEvent.class, "LuckPerms"); + MCChatUtils.addStaticExcludedPlugin(PlayerQuitEvent.class, "LuckPerms"); + + field = LPBukkitPlugin.class.getDeclaredField("connectionListener"); + field.setAccessible(true); + connectionListener = (BukkitConnectionListener) field.get(plugin); + field = connectionListener.getClass().getDeclaredField("deniedLogin"); + field.setAccessible(true); + //noinspection unchecked + deniedLogin = (Set) field.get(connectionListener); + field = connectionListener.getClass().getDeclaredField("detectedCraftBukkitOfflineMode"); + field.setAccessible(true); + detectedCraftBukkitOfflineMode = field; + printCraftBukkitOfflineModeError = connectionListener.getClass().getDeclaredMethod("printCraftBukkitOfflineModeError"); + printCraftBukkitOfflineModeError.setAccessible(true); + + //PERMISSIBLE_FIELD = DiscordFakePlayer.class.getDeclaredField("perm"); + //PERMISSIBLE_FIELD.setAccessible(true); //Hacking my own plugin, while we're at it + PERMISSIBLE_BASE_ATTACHMENTS_FIELD = PermissibleBase.class.getDeclaredField("attachments"); + PERMISSIBLE_BASE_ATTACHMENTS_FIELD.setAccessible(true); + + convertAndAddAttachments = LPPermissible.class.getDeclaredMethod("convertAndAddAttachments", Collection.class); + convertAndAddAttachments.setAccessible(true); + getActive = LPPermissible.class.getDeclaredMethod("getActive"); + getActive.setAccessible(true); + setOldPermissible = LPPermissible.class.getDeclaredMethod("setOldPermissible", PermissibleBase.class); + setOldPermissible.setAccessible(true); + getOldPermissible = LPPermissible.class.getDeclaredMethod("getOldPermissible"); + getOldPermissible.setAccessible(true); + + TBMCCoreAPI.RegisterEventsForExceptions(this, mp); + } + + + //Code copied from LuckPerms - me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerLogin(PlayerLoginEvent e) { + /* Called when the player starts logging into the server. + At this point, the users data should be present and loaded. */ + + if (!(e.getPlayer() instanceof DiscordFakePlayer)) + return; //Normal players must be handled by the plugin + + final DiscordFakePlayer player = (DiscordFakePlayer) e.getPlayer(); + + if (plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + plugin.getLogger().info("Processing login for " + player.getUniqueId() + " - " + player.getName()); + } + + final User user = plugin.getUserManager().getIfLoaded(player.getUniqueId()); + + /* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */ + if (user == null) { + deniedLogin.add(player.getUniqueId()); + + if (!connectionListener.getUniqueConnections().contains(player.getUniqueId())) { + + plugin.getLogger().warn("User " + player.getUniqueId() + " - " + player.getName() + + " doesn't have data pre-loaded, they have never been processed during pre-login in this session." + + " - denying login."); + + try { + if ((Boolean) detectedCraftBukkitOfflineMode.get(connectionListener)) { + printCraftBukkitOfflineModeError.invoke(connectionListener); + e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_STATE_ERROR_CB_OFFLINE_MODE.asString(plugin.getLocaleManager())); + return; + } + } catch (IllegalAccessException | InvocationTargetException ex) { + ex.printStackTrace(); + } + + } else { + plugin.getLogger().warn("User " + player.getUniqueId() + " - " + player.getName() + + " doesn't currently have data pre-loaded, but they have been processed before in this session." + + " - denying login."); + } + + e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_STATE_ERROR.asString(plugin.getLocaleManager())); + return; + } + + // User instance is there, now we can inject our custom Permissible into the player. + // Care should be taken at this stage to ensure that async tasks which manipulate bukkit data check that the player is still online. + try { + // get the existing PermissibleBase held by the player + PermissibleBase oldPermissible = player.getPerm(); + + // Make a new permissible for the user + LPPermissible lpPermissible = new LPPermissible(player, user, plugin); + + // Inject into the player + inject(player, lpPermissible, oldPermissible); + + } catch (Throwable t) { + plugin.getLogger().warn("Exception thrown when setting up permissions for " + + player.getUniqueId() + " - " + player.getName() + " - denying login."); + t.printStackTrace(); + + e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_SETUP_ERROR.asString(plugin.getLocaleManager())); + return; + } + + plugin.refreshAutoOp(player, true); + } + + // Wait until the last priority to unload, so plugins can still perform permission checks on this event + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerQuit(PlayerQuitEvent e) { + if (!(e.getPlayer() instanceof DiscordFakePlayer)) + return; + + final DiscordFakePlayer player = (DiscordFakePlayer) e.getPlayer(); + + connectionListener.handleDisconnect(player.getUniqueId()); + + // perform unhooking from bukkit objects 1 tick later. + // this allows plugins listening after us on MONITOR to still have intact permissions data + this.plugin.getBootstrap().getServer().getScheduler().runTaskLaterAsynchronously(this.plugin.getBootstrap(), () -> { + // Remove the custom permissible + try { + uninject(player, true); + } catch (Exception ex) { + ex.printStackTrace(); + } + + // Handle auto op + if (this.plugin.getConfiguration().get(ConfigKeys.AUTO_OP)) { + player.setOp(false); + } + + // remove their contexts cache + this.plugin.getContextManager().onPlayerQuit(player); + }, 1L); + } + + //me.lucko.luckperms.bukkit.inject.permissible.PermissibleInjector + private void inject(DiscordFakePlayer player, LPPermissible newPermissible, PermissibleBase oldPermissible) throws IllegalAccessException, InvocationTargetException { + + // seems we have already injected into this player. + if (oldPermissible instanceof LPPermissible) { + throw new IllegalStateException("LPPermissible already injected into player " + player.toString()); + } + + // Move attachments over from the old permissible + + //noinspection unchecked + List attachments = (List) PERMISSIBLE_BASE_ATTACHMENTS_FIELD.get(oldPermissible); + + convertAndAddAttachments.invoke(newPermissible, attachments); + attachments.clear(); + oldPermissible.clearPermissions(); + + // Setup the new permissible + ((AtomicBoolean) getActive.invoke(newPermissible)).set(true); + setOldPermissible.invoke(newPermissible, oldPermissible); + + // inject the new instance + player.setPerm(newPermissible); + } + + private void uninject(DiscordFakePlayer player, boolean dummy) throws Exception { + + // gets the players current permissible. + PermissibleBase permissible = player.getPerm(); + + // only uninject if the permissible was a luckperms one. + if (permissible instanceof LPPermissible) { + LPPermissible lpPermissible = ((LPPermissible) permissible); + + // clear all permissions + lpPermissible.clearPermissions(); + + // set to inactive + ((AtomicBoolean) getActive.invoke(lpPermissible)).set(false); + + // handle the replacement permissible. + if (dummy) { + // just inject a dummy class. this is used when we know the player is about to quit the server. + player.setPerm(DummyPermissibleBase.INSTANCE); + + } else { + PermissibleBase newPb = (PermissibleBase) getOldPermissible.invoke(lpPermissible); + if (newPb == null) { + newPb = new PermissibleBase(player); + } + + player.setPerm(newPb); + } + } + } +} diff --git a/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.java b/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.java index 8806647..0ab0cac 100644 --- a/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.java +++ b/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.java @@ -4,17 +4,19 @@ import buttondevteam.core.ComponentManager; import buttondevteam.discordplugin.DPUtils; import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.architecture.ConfigData; +import buttondevteam.lib.architecture.ReadOnlyConfigData; +import discord4j.core.event.domain.role.RoleCreateEvent; +import discord4j.core.event.domain.role.RoleDeleteEvent; +import discord4j.core.event.domain.role.RoleEvent; +import discord4j.core.event.domain.role.RoleUpdateEvent; +import discord4j.core.object.entity.MessageChannel; +import discord4j.core.object.entity.Role; import lombok.val; import org.bukkit.Bukkit; -import sx.blah.discord.handle.impl.events.guild.role.RoleCreateEvent; -import sx.blah.discord.handle.impl.events.guild.role.RoleDeleteEvent; -import sx.blah.discord.handle.impl.events.guild.role.RoleEvent; -import sx.blah.discord.handle.impl.events.guild.role.RoleUpdateEvent; -import sx.blah.discord.handle.obj.IChannel; -import sx.blah.discord.handle.obj.IRole; +import reactor.core.publisher.Mono; import java.awt.*; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -24,7 +26,7 @@ public class GameRoleModule extends Component { @Override protected void enable() { getPlugin().getManager().registerCommand(new RoleCommand(this)); - GameRoles = DiscordPlugin.mainServer.getRoles().stream().filter(this::isGameRole).map(IRole::getName).collect(Collectors.toList()); + GameRoles = DiscordPlugin.mainServer.getRoles().filterWhen(this::isGameRole).map(Role::getName).collect(Collectors.toList()).block(); } @Override @@ -32,7 +34,7 @@ public class GameRoleModule extends Component { } - private ConfigData logChannel() { + private ReadOnlyConfigData> logChannel() { return DPUtils.channelData(getConfig(), "logChannel", 239519012529111040L); } @@ -43,41 +45,55 @@ public class GameRoleModule extends Component { val logChannel = grm.logChannel().get(); if (roleEvent instanceof RoleCreateEvent) { Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> { - if (roleEvent.getRole().isDeleted() || !grm.isGameRole(roleEvent.getRole())) - return; //Deleted or not a game role - GameRoles.add(roleEvent.getRole().getName()); - if (logChannel != null) - DiscordPlugin.sendMessageToChannel(logChannel, "Added " + roleEvent.getRole().getName() + " as game role. If you don't want this, change the role's color from the default."); + Role role=((RoleCreateEvent) roleEvent).getRole(); + grm.isGameRole(role).flatMap(b -> { + if (!b) + return Mono.empty(); //Deleted or not a game role + GameRoles.add(role.getName()); + if (logChannel != null) + return logChannel.flatMap(ch -> ch.createMessage("Added " + role.getName() + " as game role. If you don't want this, change the role's color from the default.")); + return Mono.empty(); + }).subscribe(); }, 100); } else if (roleEvent instanceof RoleDeleteEvent) { - if (GameRoles.remove(roleEvent.getRole().getName()) && logChannel != null) - DiscordPlugin.sendMessageToChannel(logChannel, "Removed " + roleEvent.getRole().getName() + " as a game role."); + Role role=((RoleDeleteEvent) roleEvent).getRole().orElse(null); + if(role==null) return; + if (GameRoles.remove(role.getName()) && logChannel != null) + logChannel.flatMap(ch -> ch.createMessage("Removed " + role.getName() + " as a game role.")).subscribe(); } else if (roleEvent instanceof RoleUpdateEvent) { val event = (RoleUpdateEvent) roleEvent; - if (!grm.isGameRole(event.getNewRole())) { - if (GameRoles.remove(event.getOldRole().getName()) && logChannel != null) - DiscordPlugin.sendMessageToChannel(logChannel, "Removed " + event.getOldRole().getName() + " as a game role because it's color changed."); - } else { - if (GameRoles.contains(event.getOldRole().getName()) && event.getOldRole().getName().equals(event.getNewRole().getName())) - return; - boolean removed = GameRoles.remove(event.getOldRole().getName()); //Regardless of whether it was a game role - GameRoles.add(event.getNewRole().getName()); //Add it because it has no color - if (logChannel != null) { - if (removed) - DiscordPlugin.sendMessageToChannel(logChannel, "Changed game role from " + event.getOldRole().getName() + " to " + event.getNewRole().getName() + "."); - else - DiscordPlugin.sendMessageToChannel(logChannel, "Added " + event.getNewRole().getName() + " as game role because it has the default color."); - } + if(!event.getOld().isPresent()) { + DPUtils.getLogger().warning("Old role not stored, cannot update game role!"); + return; } + Role or=event.getOld().get(); + grm.isGameRole(event.getCurrent()).flatMap(b -> { + if (!b) { + if (GameRoles.remove(or.getName()) && logChannel != null) + return logChannel.flatMap(ch -> ch.createMessage("Removed " + or.getName() + " as a game role because it's color changed.")); + } else { + if (GameRoles.contains(or.getName()) && or.getName().equals(event.getCurrent().getName())) + return Mono.empty(); + boolean removed = GameRoles.remove(or.getName()); //Regardless of whether it was a game role + GameRoles.add(event.getCurrent().getName()); //Add it because it has no color + if (logChannel != null) { + if (removed) + return logChannel.flatMap(ch -> ch.createMessage("Changed game role from " + or.getName() + " to " + event.getCurrent().getName() + ".")); + else + return logChannel.flatMap(ch -> ch.createMessage("Added " + event.getCurrent().getName() + " as game role because it has the default color.")); + } + } + return Mono.empty(); + }).subscribe(); } } - private boolean isGameRole(IRole r) { - if (r.getGuild().getLongID() != DiscordPlugin.mainServer.getLongID()) - return false; //Only allow on the main server + private Mono isGameRole(Role r) { + if (r.getGuildId().asLong() != DiscordPlugin.mainServer.getId().asLong()) + return Mono.just(false); //Only allow on the main server val rc = new Color(149, 165, 166, 0); - return r.getColor().equals(rc) - && DiscordPlugin.dc.getOurUser().getRolesForGuild(DiscordPlugin.mainServer) - .stream().anyMatch(or -> r.getPosition() < or.getPosition()); //Below one of our roles + return Mono.just(r.getColor().equals(rc)).filter(b -> b).flatMap(b -> + DiscordPlugin.dc.getSelf().flatMap(u -> u.asMember(DiscordPlugin.mainServer.getId())).flatMap(m -> m.hasHigherRoles(Collections.singleton(r)))) //Below one of our roles + .defaultIfEmpty(false); } } diff --git a/src/main/java/buttondevteam/discordplugin/role/RoleCommand.java b/src/main/java/buttondevteam/discordplugin/role/RoleCommand.java index 3381ae4..eb93eb6 100755 --- a/src/main/java/buttondevteam/discordplugin/role/RoleCommand.java +++ b/src/main/java/buttondevteam/discordplugin/role/RoleCommand.java @@ -1,14 +1,13 @@ package buttondevteam.discordplugin.role; -import buttondevteam.discordplugin.DPUtils; import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.commands.Command2DCSender; import buttondevteam.discordplugin.commands.ICommand2DC; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.CommandClass; +import discord4j.core.object.entity.Role; import lombok.val; -import sx.blah.discord.handle.obj.IRole; import java.util.List; import java.util.stream.Collectors; @@ -27,12 +26,12 @@ public class RoleCommand extends ICommand2DC { "This command adds a role to your account." }) public boolean add(Command2DCSender sender, @Command2.TextArg String rolename) { - final IRole role = checkAndGetRole(sender, rolename); + final Role role = checkAndGetRole(sender, rolename); if (role == null) return true; try { - DPUtils.perform(() -> sender.getMessage().getAuthor().addRole(role)); - sender.sendMessage("added role."); + sender.getMessage().getAuthorAsMember() + .subscribe(m -> m.addRole(role.getId()).subscribe(r -> sender.sendMessage("added role."))); } catch (Exception e) { TBMCCoreAPI.SendException("Error while adding role!", e); sender.sendMessage("an error occured while adding the role."); @@ -45,12 +44,12 @@ public class RoleCommand extends ICommand2DC { "This command removes a role from your account." }) public boolean remove(Command2DCSender sender, @Command2.TextArg String rolename) { - final IRole role = checkAndGetRole(sender, rolename); + final Role role = checkAndGetRole(sender, rolename); if (role == null) return true; try { - DPUtils.perform(() -> sender.getMessage().getAuthor().removeRole(role)); - sender.sendMessage("removed role."); + sender.getMessage().getAuthorAsMember() + .subscribe(m -> m.removeRole(role.getId()).subscribe(r -> sender.sendMessage("removed role."))); } catch (Exception e) { TBMCCoreAPI.SendException("Error while removing role!", e); sender.sendMessage("an error occured while removing the role."); @@ -61,9 +60,9 @@ public class RoleCommand extends ICommand2DC { @Command2.Subcommand public void list(Command2DCSender sender) { sender.sendMessage("list of roles:\n" + grm.GameRoles.stream().sorted().collect(Collectors.joining("\n"))); - } + } - private IRole checkAndGetRole(Command2DCSender sender, String rolename) { + private Role checkAndGetRole(Command2DCSender sender, String rolename) { String rname = rolename; if (!grm.GameRoles.contains(rolename)) { //If not found as-is, correct case val orn = grm.GameRoles.stream().filter(r -> r.equalsIgnoreCase(rolename)).findAny(); @@ -73,18 +72,23 @@ public class RoleCommand extends ICommand2DC { return null; } rname = orn.get(); - } - final List roles = DiscordPlugin.mainServer.getRolesByName(rname); - if (roles.size() == 0) { - sender.sendMessage("the specified role cannot be found on Discord! Removing from the list."); - grm.GameRoles.remove(rolename); - return null; - } - if (roles.size() > 1) { - sender.sendMessage("there are multiple roles with this name. Why are there multiple roles with this name?"); - return null; - } - return roles.get(0); - } + } + val frname = rname; + final List roles = DiscordPlugin.mainServer.getRoles().filter(r -> r.getName().equals(frname)).collectList().block(); + if (roles == null) { + sender.sendMessage("an error occured."); + return null; + } + if (roles.size() == 0) { + sender.sendMessage("the specified role cannot be found on Discord! Removing from the list."); + grm.GameRoles.remove(rolename); + return null; + } + if (roles.size() > 1) { + sender.sendMessage("there are multiple roles with this name. Why are there multiple roles with this name?"); + return null; + } + return roles.get(0); + } } diff --git a/src/main/java/buttondevteam/discordplugin/util/Timings.java b/src/main/java/buttondevteam/discordplugin/util/Timings.java new file mode 100644 index 0000000..12c12f2 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/util/Timings.java @@ -0,0 +1,16 @@ +package buttondevteam.discordplugin.util; + +import buttondevteam.discordplugin.listeners.CommonListeners; + +public class Timings { + private long start; + + public Timings() { + start = System.nanoTime(); + } + + public void printElapsed(String message) { + CommonListeners.debug(message + " (" + (System.nanoTime() - start) / 1000000L + ")"); + start = System.nanoTime(); + } +}