From c57ac26b2d2b1e6d70e473c38331348c855f9f23 Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Mon, 1 Mar 2021 02:07:40 +0100 Subject: [PATCH] All classes converted that I wanted --- .../discordplugin/BukkitLogWatcher.java | 23 -- .../discordplugin/BukkitLogWatcher.scala | 17 + .../discordplugin/ChannelconBroadcast.java | 15 - .../discordplugin/ChannelconBroadcast.scala | 6 + .../discordplugin/ChromaBot.scala | 1 + .../buttondevteam/discordplugin/DPUtils.java | 222 ------------ .../buttondevteam/discordplugin/DPUtils.scala | 209 +++++++++++ .../discordplugin/DiscordConnectedPlayer.java | 324 ------------------ .../DiscordConnectedPlayer.scala | 251 ++++++++++++++ .../discordplugin/DiscordPlayer.java | 29 -- .../discordplugin/DiscordPlayer.scala | 20 ++ .../discordplugin/DiscordPlayerSender.java | 42 --- .../discordplugin/DiscordPlayerSender.scala | 41 +++ .../discordplugin/DiscordSender.java | 117 ------- .../discordplugin/DiscordSender.scala | 64 ++++ .../discordplugin/DiscordSenderBase.java | 75 ---- .../discordplugin/DiscordSenderBase.scala | 64 ++++ .../discordplugin/IMCPlayer.java | 8 - .../discordplugin/IMCPlayer.scala | 8 + .../discordplugin/mcchat/MCChatUtils.scala | 11 +- .../mccommands/DiscordMCCommand.java | 144 -------- .../mccommands/DiscordMCCommand.scala | 128 +++++++ .../playerfaker/DelegatingMockMaker.java | 20 -- .../playerfaker/DelegatingMockMaker.scala | 49 +++ .../playerfaker/ServerWatcher.java | 96 ------ .../playerfaker/ServerWatcher.scala | 91 +++++ .../playerfaker/VCMDWrapper.java | 1 + .../playerfaker/perm/LPInjector.java | 1 + .../discordplugin/role/GameRoleModule.java | 126 ------- .../discordplugin/role/GameRoleModule.scala | 107 ++++++ .../discordplugin/role/RoleCommand.java | 107 ------ .../discordplugin/role/RoleCommand.scala | 84 +++++ .../discordplugin/util/DPState.java | 24 -- .../discordplugin/util/DPState.scala | 31 ++ .../discordplugin/util/Timings.java | 14 - .../discordplugin/util/Timings.scala | 12 + src/main/scala/Test.scala | 5 - 37 files changed, 1191 insertions(+), 1396 deletions(-) delete mode 100644 src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.java create mode 100644 src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.scala delete mode 100644 src/main/java/buttondevteam/discordplugin/ChannelconBroadcast.java create mode 100644 src/main/java/buttondevteam/discordplugin/ChannelconBroadcast.scala rename src/main/{scala => java}/buttondevteam/discordplugin/ChromaBot.scala (94%) delete mode 100755 src/main/java/buttondevteam/discordplugin/DPUtils.java create mode 100644 src/main/java/buttondevteam/discordplugin/DPUtils.scala delete mode 100644 src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java create mode 100644 src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.scala delete mode 100755 src/main/java/buttondevteam/discordplugin/DiscordPlayer.java create mode 100644 src/main/java/buttondevteam/discordplugin/DiscordPlayer.scala delete mode 100755 src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java create mode 100644 src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.scala delete mode 100755 src/main/java/buttondevteam/discordplugin/DiscordSender.java create mode 100644 src/main/java/buttondevteam/discordplugin/DiscordSender.scala delete mode 100755 src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java create mode 100644 src/main/java/buttondevteam/discordplugin/DiscordSenderBase.scala delete mode 100755 src/main/java/buttondevteam/discordplugin/IMCPlayer.java create mode 100644 src/main/java/buttondevteam/discordplugin/IMCPlayer.scala delete mode 100644 src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java create mode 100644 src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.scala delete mode 100644 src/main/java/buttondevteam/discordplugin/playerfaker/DelegatingMockMaker.java create mode 100644 src/main/java/buttondevteam/discordplugin/playerfaker/DelegatingMockMaker.scala delete mode 100644 src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java create mode 100644 src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.scala delete mode 100644 src/main/java/buttondevteam/discordplugin/role/GameRoleModule.java create mode 100644 src/main/java/buttondevteam/discordplugin/role/GameRoleModule.scala delete mode 100755 src/main/java/buttondevteam/discordplugin/role/RoleCommand.java create mode 100644 src/main/java/buttondevteam/discordplugin/role/RoleCommand.scala delete mode 100644 src/main/java/buttondevteam/discordplugin/util/DPState.java create mode 100644 src/main/java/buttondevteam/discordplugin/util/DPState.scala delete mode 100644 src/main/java/buttondevteam/discordplugin/util/Timings.java create mode 100644 src/main/java/buttondevteam/discordplugin/util/Timings.scala delete mode 100644 src/main/scala/Test.scala diff --git a/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.java b/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.java deleted file mode 100644 index c51471b..0000000 --- a/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.java +++ /dev/null @@ -1,23 +0,0 @@ -package buttondevteam.discordplugin; - -import buttondevteam.discordplugin.util.DPState; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.core.Filter; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.appender.AbstractAppender; -import org.apache.logging.log4j.core.filter.LevelRangeFilter; -import org.apache.logging.log4j.core.layout.PatternLayout; - -public class BukkitLogWatcher extends AbstractAppender { - protected BukkitLogWatcher() { - super("ChromaDiscord", - LevelRangeFilter.createFilter(Level.INFO, Level.INFO, Filter.Result.ACCEPT, Filter.Result.DENY), - PatternLayout.createDefaultLayout()); - } - - @Override - public void append(LogEvent logEvent) { - if (logEvent.getMessage().getFormattedMessage().contains("Attempting to restart with ")) - MinecraftChatModule.state = DPState.RESTARTING_SERVER; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.scala b/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.scala new file mode 100644 index 0000000..7fee830 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.scala @@ -0,0 +1,17 @@ +package buttondevteam.discordplugin + +import buttondevteam.discordplugin.mcchat.MinecraftChatModule +import buttondevteam.discordplugin.util.DPState +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.core.{Filter, LogEvent} +import org.apache.logging.log4j.core.appender.AbstractAppender +import org.apache.logging.log4j.core.filter.LevelRangeFilter +import org.apache.logging.log4j.core.layout.PatternLayout + +class BukkitLogWatcher private[discordplugin]() extends AbstractAppender("ChromaDiscord", + LevelRangeFilter.createFilter(Level.INFO, Level.INFO, Filter.Result.ACCEPT, Filter.Result.DENY), + PatternLayout.createDefaultLayout) { + override def append(logEvent: LogEvent): Unit = + if (logEvent.getMessage.getFormattedMessage.contains("Attempting to restart with ")) + MinecraftChatModule.state = DPState.RESTARTING_SERVER +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/ChannelconBroadcast.java b/src/main/java/buttondevteam/discordplugin/ChannelconBroadcast.java deleted file mode 100644 index 994c8ed..0000000 --- a/src/main/java/buttondevteam/discordplugin/ChannelconBroadcast.java +++ /dev/null @@ -1,15 +0,0 @@ -package buttondevteam.discordplugin; - -public enum ChannelconBroadcast { - JOINLEAVE, - AFK, - RESTART, - DEATH, - BROADCAST; - - public final int flag; - - ChannelconBroadcast() { - this.flag = 1 << this.ordinal(); - } -} diff --git a/src/main/java/buttondevteam/discordplugin/ChannelconBroadcast.scala b/src/main/java/buttondevteam/discordplugin/ChannelconBroadcast.scala new file mode 100644 index 0000000..317a759 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/ChannelconBroadcast.scala @@ -0,0 +1,6 @@ +package buttondevteam.discordplugin + +object ChannelconBroadcast extends Enumeration { + type ChannelconBroadcast = Value + val JOINLEAVE, AFK, RESTART, DEATH, BROADCAST = Value +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/ChromaBot.scala b/src/main/java/buttondevteam/discordplugin/ChromaBot.scala similarity index 94% rename from src/main/scala/buttondevteam/discordplugin/ChromaBot.scala rename to src/main/java/buttondevteam/discordplugin/ChromaBot.scala index 6a8b7e0..187c7f8 100644 --- a/src/main/scala/buttondevteam/discordplugin/ChromaBot.scala +++ b/src/main/java/buttondevteam/discordplugin/ChromaBot.scala @@ -1,5 +1,6 @@ package buttondevteam.discordplugin +import buttondevteam.discordplugin.ChannelconBroadcast.ChannelconBroadcast import buttondevteam.discordplugin.mcchat.MCChatUtils import discord4j.core.`object`.entity.Message import discord4j.core.`object`.entity.channel.MessageChannel diff --git a/src/main/java/buttondevteam/discordplugin/DPUtils.java b/src/main/java/buttondevteam/discordplugin/DPUtils.java deleted file mode 100755 index 747c21b..0000000 --- a/src/main/java/buttondevteam/discordplugin/DPUtils.java +++ /dev/null @@ -1,222 +0,0 @@ -package buttondevteam.discordplugin; - -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.common.util.Snowflake; -import discord4j.core.object.entity.Guild; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.Role; -import discord4j.core.object.entity.channel.MessageChannel; -import discord4j.core.spec.EmbedCreateSpec; -import lombok.val; -import reactor.core.publisher.Mono; - -import javax.annotation.Nullable; -import java.util.Comparator; -import java.util.Optional; -import java.util.TreeSet; -import java.util.logging.Logger; -import java.util.regex.Pattern; - -public final class DPUtils { - - private static final Pattern URL_PATTERN = Pattern.compile("https?://\\S*"); - private static final Pattern FORMAT_PATTERN = Pattern.compile("[*_~]"); - - 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) { - 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(); - } - - private static String escape(String message) { - //var ts = new TreeSet<>(); - var ts = new TreeSet(Comparator.comparingInt(a -> a[0])); //Compare the start, then check the end - var matcher = URL_PATTERN.matcher(message); - while (matcher.find()) - ts.add(new int[]{matcher.start(), matcher.end()}); - matcher = FORMAT_PATTERN.matcher(message); - /*Function aFunctionalInterface = result -> - Optional.ofNullable(ts.floor(new int[]{result.start(), 0})).map(a -> a[1]).orElse(0) < result.start() - ? "\\\\" + result.group() : result.group(); - return matcher.replaceAll(aFunctionalInterface); //Find nearest URL match and if it's not reaching to the char then escape*/ - StringBuffer sb = new StringBuffer(); - while (matcher.find()) { - matcher.appendReplacement(sb, Optional.ofNullable(ts.floor(new int[]{matcher.start(), 0})) //Find a URL start <= our start - .map(a -> a[1]).orElse(-1) < matcher.start() //Check if URL end < our start - ? "\\\\" + matcher.group() : matcher.group()); - } - matcher.appendTail(sb); - return sb.toString(); - } - - public static Logger getLogger() { - if (DiscordPlugin.plugin == null || DiscordPlugin.plugin.getLogger() == null) - return Logger.getLogger("DiscordPlugin"); - return DiscordPlugin.plugin.getLogger(); - } - - public static ReadOnlyConfigData> channelData(IHaveConfig config, String key) { - return config.getReadOnlyDataPrimDef(key, 0L, id -> getMessageChannel(key, Snowflake.of((Long) id)), ch -> 0L); //We can afford to search for the channel in the cache once (instead of using mainServer) - } - - public static ReadOnlyConfigData> roleData(IHaveConfig config, String key, String defName) { - return roleData(config, key, defName, Mono.just(DiscordPlugin.mainServer)); - } - - /** - * 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) || ((String) name).length() == 0) return Mono.empty(); - return guild.flatMapMany(Guild::getRoles).filter(r -> r.getName().equals(name)).onErrorResume(e -> { - getLogger().warning("Failed to get role data for " + key + "=" + name + " - " + e.getMessage()); - return Mono.empty(); - }).next(); - }, r -> defName); - } - - public static ReadOnlyConfigData snowflakeData(IHaveConfig config, String key, long defID) { - return config.getReadOnlyDataPrimDef(key, defID, id -> Snowflake.of((long) id), Snowflake::asLong); - } - - /** - * Mentions the bot channel. Useful for help texts. - * - * @return The string for mentioning the channel - */ - public static String botmention() { - if (DiscordPlugin.plugin == null) return "#bot"; - return channelMention(DiscordPlugin.plugin.commandChannel.get()); - } - - /** - * 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 configs The configs to check for null - * @return Whether the component got disabled and a warning logged - */ - public static boolean disableIfConfigError(@Nullable Component component, ConfigData... configs) { - for (val config : configs) { - 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) { - if (component != null) - TBMCCoreAPI.SendException("Failed to disable component after config error!", e, component); - else - TBMCCoreAPI.SendException("Failed to disable component after config error!", e, DiscordPlugin.plugin); - } - 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; - } - - /** - * Send a response in the form of "@User, message". Use Mono.empty() if you don't have a channel object. - * - * @param original The original message to reply to - * @param channel The channel to send the message in, defaults to the original - * @param message The message to send - * @return A mono to send the message - */ - 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 reply(original, ch, message); - } - - /** - * @see #reply(Message, MessageChannel, String) - */ - public static Mono reply(Message original, Mono ch, String message) { - 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() + ">"; - } - - /** - * Gets a message channel for a config. Returns empty for ID 0. - * - * @param key The config key - * @param id The channel ID - * @return A message channel - */ - public static Mono getMessageChannel(String key, Snowflake id) { - if (id.asLong() == 0L) return Mono.empty(); - 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()); - } - - public static Mono ignoreError(Mono mono) { - return mono.onErrorResume(t -> Mono.empty()); - } - -} diff --git a/src/main/java/buttondevteam/discordplugin/DPUtils.scala b/src/main/java/buttondevteam/discordplugin/DPUtils.scala new file mode 100644 index 0000000..f69db27 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/DPUtils.scala @@ -0,0 +1,209 @@ +package buttondevteam.discordplugin + +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.architecture.{Component, ConfigData, IHaveConfig, ReadOnlyConfigData} +import discord4j.common.util.Snowflake +import discord4j.core.`object`.entity.channel.MessageChannel +import discord4j.core.`object`.entity.{Guild, Message, Role} +import discord4j.core.spec.EmbedCreateSpec +import reactor.core.publisher.Mono + +import java.util +import java.util.{Comparator, Optional} +import java.util.logging.Logger +import java.util.regex.Pattern +import javax.annotation.Nullable + +object DPUtils { + private val URL_PATTERN = Pattern.compile("https?://\\S*") + private val FORMAT_PATTERN = Pattern.compile("[*_~]") + + def embedWithHead(ecs: EmbedCreateSpec, displayname: String, playername: String, profileUrl: String): EmbedCreateSpec = + 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) + */ + def sanitizeString(string: String): String = escape(sanitizeStringNoEscape(string)) + + /** + * Removes §[char] colour codes from strings + */ + def sanitizeStringNoEscape(string: String): String = { + val sanitizedString = new StringBuilder + var random = false + var i = 0 + while ( { + i < string.length + }) { + if (string.charAt(i) == '§') { + i += 1 // 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)) + } + i += 1 + } + sanitizedString.toString + } + + private def escape(message: String) = { //var ts = new TreeSet<>(); + val ts = new util.TreeSet[Array[Int]](Comparator.comparingInt((a: Array[Int]) => a(0)): Comparator[Array[Int]]) //Compare the start, then check the end + var matcher = URL_PATTERN.matcher(message) + while ( { + matcher.find + }) ts.add(Array[Int](matcher.start, matcher.end)) + matcher = FORMAT_PATTERN.matcher(message) + /*Function aFunctionalInterface = result -> + Optional.ofNullable(ts.floor(new int[]{result.start(), 0})).map(a -> a[1]).orElse(0) < result.start() + ? "\\\\" + result.group() : result.group(); + return matcher.replaceAll(aFunctionalInterface); //Find nearest URL match and if it's not reaching to the char then escape*/ val sb = new StringBuffer + while ( { + matcher.find + }) matcher.appendReplacement(sb, if (Optional.ofNullable(ts.floor(Array[Int](matcher.start, 0))).map( //Find a URL start <= our start + (a: Array[Int]) => a(1)).orElse(-1) < matcher.start //Check if URL end < our start + ) "\\\\" + matcher.group else matcher.group) + matcher.appendTail(sb) + sb.toString + } + + def getLogger: Logger = { + if (DiscordPlugin.plugin == null || DiscordPlugin.plugin.getLogger == null) return Logger.getLogger("DiscordPlugin") + DiscordPlugin.plugin.getLogger + } + + def channelData(config: IHaveConfig, key: String): ReadOnlyConfigData[Mono[MessageChannel]] = + config.getReadOnlyDataPrimDef(key, 0L, (id: Any) => + getMessageChannel(key, Snowflake.of(id.asInstanceOf[Long])), (_: Mono[MessageChannel]) => 0L) //We can afford to search for the channel in the cache once (instead of using mainServer) + def roleData(config: IHaveConfig, key: String, defName: String): ReadOnlyConfigData[Mono[Role]] = + roleData(config, key, defName, Mono.just(DiscordPlugin.mainServer)) + + /** + * Needs to be a {@link ConfigData} for checking if it's set + */ + def roleData(config: IHaveConfig, key: String, defName: String, guild: Mono[Guild]): ReadOnlyConfigData[Mono[Role]] = config.getReadOnlyDataPrimDef(key, defName, (name: Any) => { + def foo(name: Any): Mono[Role] = { + if (!name.isInstanceOf[String] || name.asInstanceOf[String].isEmpty) return Mono.empty[Role] + guild.flatMapMany(_.getRoles).filter((r: Role) => r.getName == name).onErrorResume((e: Throwable) => { + def foo(e: Throwable): Mono[Role] = { + getLogger.warning("Failed to get role data for " + key + "=" + name + " - " + e.getMessage) + Mono.empty[Role] + } + + foo(e) + }).next + } + + foo(name) + }, (_: Mono[Role]) => defName) + + def snowflakeData(config: IHaveConfig, key: String, defID: Long): ReadOnlyConfigData[Snowflake] = + config.getReadOnlyDataPrimDef(key, defID, (id: Any) => Snowflake.of(id.asInstanceOf[Long]), _.asLong) + + /** + * Mentions the bot channel. Useful for help texts. + * + * @return The string for mentioning the channel + */ + def botmention: String = { + if (DiscordPlugin.plugin == null) return "#bot" + channelMention(DiscordPlugin.plugin.commandChannel.get) + } + + /** + * 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 configs The configs to check for null + * @return Whether the component got disabled and a warning logged + */ + def disableIfConfigError(@Nullable component: Component[DiscordPlugin], configs: ConfigData[_]*): Boolean = { + for (config <- configs) { + val v = config.get + if (disableIfConfigErrorRes(component, config, v)) return true + } + 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 + */ + def disableIfConfigErrorRes(@Nullable component: Component[DiscordPlugin], config: ConfigData[_], result: Any): Boolean = { + //noinspection ConstantConditions + if (result == null || (result.isInstanceOf[Mono[_]] && !result.asInstanceOf[Mono[_]].hasElement.block)) { + var path: String = null + try { + if (component != null) Component.setComponentEnabled(component, false) + path = config.getPath + } catch { + case e: Exception => + if (component != null) TBMCCoreAPI.SendException("Failed to disable component after config error!", e, component) + else TBMCCoreAPI.SendException("Failed to disable component after config error!", e, DiscordPlugin.plugin) + } + getLogger.warning("The config value " + path + " isn't set correctly " + (if (component == null) "in global settings!" + else "for component " + component.getClass.getSimpleName + "!")) + getLogger.warning("Set the correct ID in the config" + (if (component == null) "" + else " or disable this component") + " to remove this message.") + return true + } + false + } + + /** + * Send a response in the form of "@User, message". Use Mono.empty() if you don't have a channel object. + * + * @param original The original message to reply to + * @param channel The channel to send the message in, defaults to the original + * @param message The message to send + * @return A mono to send the message + */ + def reply(original: Message, @Nullable channel: MessageChannel, message: String): Mono[Message] = { + val ch = if (channel == null) original.getChannel + else Mono.just(channel) + reply(original, ch, message) + } + + /** + * @see #reply(Message, MessageChannel, String) + */ + def reply(original: Message, ch: Mono[MessageChannel], message: String): Mono[Message] = + ch.flatMap(_.createMessage((if (original.getAuthor.isPresent) + original.getAuthor.get.getMention + ", " + else "") + message)) + + def nickMention(userId: Snowflake): String = "<@!" + userId.asString + ">" + + def channelMention(channelId: Snowflake): String = "<#" + channelId.asString + ">" + + /** + * Gets a message channel for a config. Returns empty for ID 0. + * + * @param key The config key + * @param id The channel ID + * @return A message channel + */ + def getMessageChannel(key: String, id: Snowflake): Mono[MessageChannel] = { + if (id.asLong == 0L) return Mono.empty[MessageChannel] + + DiscordPlugin.dc.getChannelById(id).onErrorResume(e => { + def foo(e: Throwable) = { + getLogger.warning("Failed to get channel data for " + key + "=" + id + " - " + e.getMessage) + Mono.empty + } + + foo(e) + }).filter(ch => ch.isInstanceOf[MessageChannel]).cast(classOf[MessageChannel]) + } + + def getMessageChannel(config: ConfigData[Snowflake]): Mono[MessageChannel] = + getMessageChannel(config.getPath, config.get) + + def ignoreError[T](mono: Mono[T]): Mono[T] = mono.onErrorResume((_: Throwable) => Mono.empty) +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java deleted file mode 100644 index 714d347..0000000 --- a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java +++ /dev/null @@ -1,324 +0,0 @@ -package buttondevteam.discordplugin; - -import buttondevteam.discordplugin.playerfaker.DiscordInventory; -import buttondevteam.discordplugin.playerfaker.VCMDWrapper; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.MessageChannel; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Delegate; -import net.md_5.bungee.api.ChatMessageType; -import net.md_5.bungee.api.chat.BaseComponent; -import org.bukkit.*; -import org.bukkit.attribute.Attribute; -import org.bukkit.attribute.AttributeInstance; -import org.bukkit.attribute.AttributeModifier; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.event.player.AsyncPlayerChatEvent; -import org.bukkit.event.player.PlayerTeleportEvent; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.PlayerInventory; -import org.bukkit.permissions.PermissibleBase; -import org.bukkit.permissions.ServerOperator; -import org.mockito.MockSettings; -import org.mockito.Mockito; - -import java.lang.reflect.Modifier; -import java.net.InetSocketAddress; -import java.util.*; - -import static org.mockito.Answers.RETURNS_DEFAULTS; - -public abstract class DiscordConnectedPlayer extends DiscordSenderBase implements IMCPlayer { - private @Getter VCMDWrapper vanillaCmdListener; - @Getter - @Setter - private boolean loggedIn = false; - - @Delegate(excludes = ServerOperator.class) - private PermissibleBase origPerm; - - private @Getter String name; - - private @Getter OfflinePlayer basePlayer; - - @Getter - @Setter - private PermissibleBase perm; - - private Location location; - - private final MinecraftChatModule module; - - @Getter - private final UUID uniqueId; - - /** - * The parameters must match with {@link #create(User, MessageChannel, UUID, String, MinecraftChatModule)} - */ - protected DiscordConnectedPlayer(User user, MessageChannel channel, UUID uuid, String mcname, - MinecraftChatModule module) { - super(user, channel); - location = Bukkit.getWorlds().get(0).getSpawnLocation(); - origPerm = perm = new PermissibleBase(basePlayer = Bukkit.getOfflinePlayer(uuid)); - name = mcname; - this.module = module; - uniqueId = uuid; - displayName = mcname; - vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this, module)); - } - - /** - * For testing - */ - protected DiscordConnectedPlayer(User user, MessageChannel channel) { - super(user, channel); - module = null; - uniqueId = UUID.randomUUID(); - } - - public void setOp(boolean value) { //CraftPlayer-compatible implementation - this.origPerm.setOp(value); - this.perm.recalculatePermissions(); - } - - public boolean isOp() { return this.origPerm.isOp(); } - - @Override - public boolean teleport(Location location) { - if (module.allowFakePlayerTeleports.get()) - this.location = location; - return true; - } - - @Override - public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause) { - if (module.allowFakePlayerTeleports.get()) - this.location = location; - return true; - } - - @Override - public boolean teleport(Entity destination) { - if (module.allowFakePlayerTeleports.get()) - this.location = destination.getLocation(); - return true; - } - - @Override - public boolean teleport(Entity destination, PlayerTeleportEvent.TeleportCause cause) { - if (module.allowFakePlayerTeleports.get()) - this.location = destination.getLocation(); - return true; - } - - @Override - public Location getLocation(Location loc) { - if (loc != null) { - loc.setWorld(getWorld()); - loc.setX(location.getX()); - loc.setY(location.getY()); - loc.setZ(location.getZ()); - loc.setYaw(location.getYaw()); - loc.setPitch(location.getPitch()); - } - - return loc; - } - - @Override - public Server getServer() { - return Bukkit.getServer(); - } - - @Override - public void sendRawMessage(String message) { - sendMessage(message); - } - - @Override - public void chat(String msg) { - Bukkit.getPluginManager() - .callEvent(new AsyncPlayerChatEvent(true, this, msg, new HashSet<>(Bukkit.getOnlinePlayers()))); - } - - @Override - public World getWorld() { - return Bukkit.getWorlds().get(0); - } - - @Override - public boolean isOnline() { - return true; - } - - @Override - public Location getLocation() { - return new Location(getWorld(), location.getX(), location.getY(), location.getZ(), - location.getYaw(), location.getPitch()); - } - - @Override - public Location getEyeLocation() { - return getLocation(); - } - - @Override - @Deprecated - public double getMaxHealth() { - return 20; - } - - @Override - public Player getPlayer() { - return this; - } - - @Getter - @Setter - private String displayName; - - @Override - public AttributeInstance getAttribute(Attribute attribute) { - return new AttributeInstance() { - @Override - public Attribute getAttribute() { - return attribute; - } - - @Override - public double getBaseValue() { - return getDefaultValue(); - } - - @Override - public void setBaseValue(double value) { - } - - @Override - public Collection getModifiers() { - return Collections.emptyList(); - } - - @Override - public void addModifier(AttributeModifier modifier) { - } - - @Override - public void removeModifier(AttributeModifier modifier) { - } - - @Override - public double getValue() { - return getDefaultValue(); - } - - @Override - public double getDefaultValue() { - return 20; //Works for max health, should be okay for the rest - } - }; - } - - @Override - public GameMode getGameMode() { - return GameMode.SPECTATOR; - } - - @SuppressWarnings("deprecation") - private final Player.Spigot spigot = new Player.Spigot() { - @Override - public InetSocketAddress getRawAddress() { - return null; - } - - @Override - public void playEffect(Location location, Effect effect, int id, int data, float offsetX, float offsetY, float offsetZ, float speed, int particleCount, int radius) { - } - - @Override - public boolean getCollidesWithEntities() { - return false; - } - - @Override - public void setCollidesWithEntities(boolean collides) { - } - - @Override - public void respawn() { - } - - @Override - public String getLocale() { - return "en_us"; - } - - @Override - public Set getHiddenPlayers() { - return Collections.emptySet(); - } - - @Override - public void sendMessage(BaseComponent component) { - DiscordConnectedPlayer.super.sendMessage(component.toPlainText()); - } - - @Override - public void sendMessage(BaseComponent... components) { - for (var component : components) - sendMessage(component); - } - - @Override - public void sendMessage(ChatMessageType position, BaseComponent component) { - sendMessage(component); //Ignore position - } - - @Override - public void sendMessage(ChatMessageType position, BaseComponent... components) { - sendMessage(components); //Ignore position - } - - @Override - public boolean isInvulnerable() { - return true; - } - }; - - @Override - public Player.Spigot spigot() { - return spigot; - } - - public static DiscordConnectedPlayer create(User user, MessageChannel channel, UUID uuid, String mcname, - MinecraftChatModule module) { - return Mockito.mock(DiscordConnectedPlayer.class, - getSettings().useConstructor(user, channel, uuid, mcname, module)); - } - - public static DiscordConnectedPlayer createTest() { - return Mockito.mock(DiscordConnectedPlayer.class, getSettings().useConstructor(null, null)); - } - - private static MockSettings getSettings() { - return Mockito.withSettings() - .defaultAnswer(invocation -> { - try { - if (!Modifier.isAbstract(invocation.getMethod().getModifiers())) - return invocation.callRealMethod(); - if (PlayerInventory.class.isAssignableFrom(invocation.getMethod().getReturnType())) - return Mockito.mock(DiscordInventory.class, Mockito.withSettings().extraInterfaces(PlayerInventory.class)); - if (Inventory.class.isAssignableFrom(invocation.getMethod().getReturnType())) - return new DiscordInventory(); - return RETURNS_DEFAULTS.answer(invocation); - } catch (Exception e) { - System.err.println("Error in mocked player!"); - e.printStackTrace(); - return RETURNS_DEFAULTS.answer(invocation); - } - }) - .stubOnly(); - } -} diff --git a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.scala b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.scala new file mode 100644 index 0000000..e627049 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.scala @@ -0,0 +1,251 @@ +package buttondevteam.discordplugin + +import buttondevteam.discordplugin.mcchat.MinecraftChatModule +import buttondevteam.discordplugin.playerfaker.{DiscordInventory, VCMDWrapper} +import discord4j.core.`object`.entity.User +import discord4j.core.`object`.entity.channel.MessageChannel +import net.md_5.bungee.api.ChatMessageType +import net.md_5.bungee.api.chat.BaseComponent +import org.bukkit._ +import org.bukkit.attribute.{Attribute, AttributeInstance, AttributeModifier} +import org.bukkit.entity.Player.Spigot +import org.bukkit.entity.{Entity, Player} +import org.bukkit.event.player.{AsyncPlayerChatEvent, PlayerTeleportEvent} +import org.bukkit.inventory.{Inventory, PlayerInventory} +import org.bukkit.permissions.{PermissibleBase, Permission, PermissionAttachment, PermissionAttachmentInfo} +import org.bukkit.plugin.Plugin +import org.mockito.Answers.RETURNS_DEFAULTS +import org.mockito.{MockSettings, Mockito} +import org.mockito.invocation.InvocationOnMock + +import java.lang.reflect.Modifier +import java.net.InetSocketAddress +import java.util +import java.util._ + +object DiscordConnectedPlayer { + def create(user: User, channel: MessageChannel, uuid: UUID, mcname: String, module: MinecraftChatModule): DiscordConnectedPlayer = + Mockito.mock(classOf[DiscordConnectedPlayer], getSettings.useConstructor(user, channel, uuid, mcname, module)) + + def createTest: DiscordConnectedPlayer = + Mockito.mock(classOf[DiscordConnectedPlayer], getSettings.useConstructor(null, null)) + + private def getSettings: MockSettings = Mockito.withSettings.defaultAnswer((invocation: InvocationOnMock) => { + def foo(invocation: InvocationOnMock): AnyRef = + try { + if (!Modifier.isAbstract(invocation.getMethod.getModifiers)) + invocation.callRealMethod + else if (classOf[PlayerInventory].isAssignableFrom(invocation.getMethod.getReturnType)) + Mockito.mock(classOf[DiscordInventory], Mockito.withSettings.extraInterfaces(classOf[PlayerInventory])) + else if (classOf[Inventory].isAssignableFrom(invocation.getMethod.getReturnType)) + new DiscordInventory + else + RETURNS_DEFAULTS.answer(invocation) + } catch { + case e: Exception => + System.err.println("Error in mocked player!") + e.printStackTrace() + RETURNS_DEFAULTS.answer(invocation) + } + + foo(invocation) + }).stubOnly +} + +abstract class DiscordConnectedPlayer(user: User, channel: MessageChannel) extends DiscordSenderBase(user, channel) with IMCPlayer[DiscordConnectedPlayer] { + override def isPermissionSet(name: String): Boolean = this.origPerm.isPermissionSet(name) + + override def isPermissionSet(perm: Permission): Boolean = this.origPerm.isPermissionSet(perm) + + override def hasPermission(inName: String): Boolean = this.origPerm.hasPermission(inName) + + override def hasPermission(perm: Permission): Boolean = this.origPerm.hasPermission(perm) + + override def addAttachment(plugin: Plugin, name: String, value: Boolean): PermissionAttachment = this.origPerm.addAttachment(plugin, name, value) + + override def addAttachment(plugin: Plugin): PermissionAttachment = this.origPerm.addAttachment(plugin) + + override def removeAttachment(attachment: PermissionAttachment): Unit = this.origPerm.removeAttachment(attachment) + + override def recalculatePermissions(): Unit = this.origPerm.recalculatePermissions() + + def clearPermissions(): Unit = this.origPerm.clearPermissions() + + override def addAttachment(plugin: Plugin, name: String, value: Boolean, ticks: Int): PermissionAttachment = + this.origPerm.addAttachment(plugin, name, value, ticks) + + override def addAttachment(plugin: Plugin, ticks: Int): PermissionAttachment = this.origPerm.addAttachment(plugin, ticks) + + override def getEffectivePermissions: util.Set[PermissionAttachmentInfo] = this.origPerm.getEffectivePermissions + + def setLoggedIn(loggedIn: Boolean): Unit = this.loggedIn = loggedIn + + def setPerm(perm: PermissibleBase): Unit = this.perm = perm + + override def setDisplayName(displayName: String): Unit = this.displayName = displayName + + override def getVanillaCmdListener: VCMDWrapper = this.vanillaCmdListener + + def isLoggedIn: Boolean = this.loggedIn + + override def getName: String = this.name + + def getBasePlayer: OfflinePlayer = this.basePlayer + + def getPerm: PermissibleBase = this.perm + + override def getUniqueId: UUID = this.uniqueId + + override def getDisplayName: String = this.displayName + + private var vanillaCmdListener: VCMDWrapper = null + private var loggedIn = false + private var origPerm: PermissibleBase = null + private var name: String = null + private var basePlayer: OfflinePlayer = null + private var perm: PermissibleBase = null + private var location: Location = null + final private var module: MinecraftChatModule = null + final private var uniqueId: UUID = null + final private var displayName: String = null + + /** + * The parameters must match with {@link #create ( User, MessageChannel, UUID, String, MinecraftChatModule)} + */ + def this(user: User, channel: MessageChannel, uuid: UUID, mcname: String, module: MinecraftChatModule) { + this(user, channel) + location = Bukkit.getWorlds.get(0).getSpawnLocation + perm = new PermissibleBase(basePlayer = Bukkit.getOfflinePlayer(uuid)) + origPerm = perm + name = mcname + this.module = module + uniqueId = uuid + displayName = mcname + vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this, module)) + } + + /** + * For testing + */ + def this(user: User, channel: MessageChannel) { + this(user, channel) + module = null + uniqueId = UUID.randomUUID + } + + override def setOp(value: Boolean): Unit = { //CraftPlayer-compatible implementation + this.origPerm.setOp(value) + this.perm.recalculatePermissions() + } + + override def isOp: Boolean = this.origPerm.isOp + + override def teleport(location: Location): Boolean = { + if (module.allowFakePlayerTeleports.get) this.location = location + true + } + + def teleport(location: Location, cause: PlayerTeleportEvent.TeleportCause): Boolean = { + if (module.allowFakePlayerTeleports.get) this.location = location + true + } + + override def teleport(destination: Entity): Boolean = { + if (module.allowFakePlayerTeleports.get) this.location = destination.getLocation + true + } + + def teleport(destination: Entity, cause: PlayerTeleportEvent.TeleportCause): Boolean = { + if (module.allowFakePlayerTeleports.get) this.location = destination.getLocation + true + } + + override def getLocation(loc: Location): Location = { + if (loc != null) { + loc.setWorld(getWorld) + loc.setX(location.getX) + loc.setY(location.getY) + loc.setZ(location.getZ) + loc.setYaw(location.getYaw) + loc.setPitch(location.getPitch) + } + loc + } + + override def getServer: Server = Bukkit.getServer + + override def sendRawMessage(message: String): Unit = sendMessage(message) + + override def chat(msg: String): Unit = Bukkit.getPluginManager.callEvent(new AsyncPlayerChatEvent(true, this, msg, new util.HashSet[Player](Bukkit.getOnlinePlayers))) + + override def getWorld: World = Bukkit.getWorlds.get(0) + + override def isOnline = true + + override def getLocation = new Location(getWorld, location.getX, location.getY, location.getZ, location.getYaw, location.getPitch) + + override def getEyeLocation: Location = getLocation + + @deprecated override def getMaxHealth = 20 + + override def getPlayer: DiscordConnectedPlayer = this + + override def getAttribute(attribute: Attribute): AttributeInstance = new AttributeInstance() { + override def getAttribute: Attribute = attribute + + override def getBaseValue: Double = getDefaultValue + + override def setBaseValue(value: Double): Unit = { + } + + override def getModifiers: util.Collection[AttributeModifier] = Collections.emptyList + + override def addModifier(modifier: AttributeModifier): Unit = { + } + + override def removeModifier(modifier: AttributeModifier): Unit = { + } + + override def getValue: Double = getDefaultValue + + override def getDefaultValue: Double = 20 //Works for max health, should be okay for the rest + } + + override def getGameMode = GameMode.SPECTATOR + + //noinspection ScalaDeprecation + @SuppressWarnings(Array("deprecation")) final private val spigot: Spigot = new Spigot() { + override def getRawAddress: InetSocketAddress = null + + override def playEffect(location: Location, effect: Effect, id: Int, data: Int, offsetX: Float, offsetY: Float, offsetZ: Float, speed: Float, particleCount: Int, radius: Int): Unit = { + } + + override def getCollidesWithEntities = false + + override def setCollidesWithEntities(collides: Boolean): Unit = { + } + + override def respawn(): Unit = { + } + + override def getLocale = "en_us" + + override def getHiddenPlayers: util.Set[Player] = Collections.emptySet + + override def sendMessage(component: BaseComponent): Unit = + DiscordConnectedPlayer.super.sendMessage(component.toPlainText) + + override def sendMessage(components: BaseComponent*): Unit = + for (component <- components) + sendMessage(component) + + override def sendMessage(position: ChatMessageType, component: BaseComponent): Unit = + sendMessage(component) //Ignore position + override def sendMessage(position: ChatMessageType, components: BaseComponent*) = + sendMessage(components) + + override def isInvulnerable = true + } + + override def spigot: Spigot = spigot +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java b/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java deleted file mode 100755 index 40ce273..0000000 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java +++ /dev/null @@ -1,29 +0,0 @@ -package buttondevteam.discordplugin; - -import buttondevteam.lib.player.ChromaGamerBase; -import buttondevteam.lib.player.UserClass; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.MessageChannel; - -@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 = getFileName(); - return did; - } - - /** - * Returns true if player has the private Minecraft chat enabled. For setting the value, see - * {@link MCChatPrivate#privateMCChat(MessageChannel, boolean, User, DiscordPlayer)} - */ - public boolean isMinecraftChatEnabled() { - return MCChatPrivate.isMinecraftChatEnabled(this); - } -} diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlayer.scala b/src/main/java/buttondevteam/discordplugin/DiscordPlayer.scala new file mode 100644 index 0000000..017e12a --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlayer.scala @@ -0,0 +1,20 @@ +package buttondevteam.discordplugin + +import buttondevteam.discordplugin.mcchat.MCChatPrivate +import buttondevteam.lib.player.{ChromaGamerBase, UserClass} + +@UserClass(foldername = "discord") class DiscordPlayer() extends ChromaGamerBase { + private var did: String = null + + // private @Getter @Setter boolean minecraftChatEnabled; + def getDiscordID: String = { + if (did == null) did = getFileName + did + } + + /** + * Returns true if player has the private Minecraft chat enabled. For setting the value, see + * {@link MCChatPrivate# privateMCChat ( MessageChannel, boolean, User, DiscordPlayer)} + */ + def isMinecraftChatEnabled: Boolean = MCChatPrivate.isMinecraftChatEnabled(this) +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java deleted file mode 100755 index b9e7f86..0000000 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java +++ /dev/null @@ -1,42 +0,0 @@ -package buttondevteam.discordplugin; - -import buttondevteam.discordplugin.playerfaker.VCMDWrapper; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.MessageChannel; -import lombok.Getter; -import org.bukkit.entity.Player; -import org.mockito.Mockito; - -import java.lang.reflect.Modifier; - -public abstract class DiscordPlayerSender extends DiscordSenderBase implements IMCPlayer { - - protected Player player; - private @Getter final VCMDWrapper vanillaCmdListener; - - public DiscordPlayerSender(User user, MessageChannel channel, Player player, MinecraftChatModule module) { - super(user, channel); - this.player = player; - vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this, player, module)); - } - - @Override - public void sendMessage(String message) { - player.sendMessage(message); - super.sendMessage(message); - } - - @Override - public void sendMessage(String[] messages) { - player.sendMessage(messages); - super.sendMessage(messages); - } - - public static DiscordPlayerSender create(User user, MessageChannel channel, Player player, MinecraftChatModule module) { - return Mockito.mock(DiscordPlayerSender.class, Mockito.withSettings().stubOnly().defaultAnswer(invocation -> { - if (!Modifier.isAbstract(invocation.getMethod().getModifiers())) - return invocation.callRealMethod(); - return invocation.getMethod().invoke(((DiscordPlayerSender) invocation.getMock()).player, invocation.getArguments()); - }).useConstructor(user, channel, player, module)); - } -} diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.scala b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.scala new file mode 100644 index 0000000..5994da7 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.scala @@ -0,0 +1,41 @@ +package buttondevteam.discordplugin + +import buttondevteam.discordplugin.mcchat.MinecraftChatModule +import buttondevteam.discordplugin.playerfaker.VCMDWrapper +import discord4j.core.`object`.entity.User +import discord4j.core.`object`.entity.channel.MessageChannel +import org.bukkit.entity.Player +import org.mockito.Mockito +import org.mockito.invocation.InvocationOnMock + +import java.lang.reflect.Modifier + +object DiscordPlayerSender { + def create(user: User, channel: MessageChannel, player: Player, module: MinecraftChatModule): DiscordPlayerSender = + Mockito.mock(classOf[DiscordPlayerSender], Mockito.withSettings.stubOnly.defaultAnswer((invocation: InvocationOnMock) => { + def foo(invocation: InvocationOnMock): AnyRef = { + if (!Modifier.isAbstract(invocation.getMethod.getModifiers)) + invocation.callRealMethod + else + invocation.getMethod.invoke(invocation.getMock.asInstanceOf[DiscordPlayerSender].player, invocation.getArguments) + } + + foo(invocation) + }).useConstructor(user, channel, player, module)) +} + +abstract class DiscordPlayerSender(val user: User, val channel: MessageChannel, var player: Player, val module: Nothing) extends DiscordSenderBase(user, channel) with IMCPlayer[DiscordPlayerSender] { + val vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this, player, module)) + + override def getVanillaCmdListener: VCMDWrapper = this.vanillaCmdListener + + override def sendMessage(message: String): Unit = { + player.sendMessage(message) + super.sendMessage(message) + } + + override def sendMessage(messages: Array[String]): Unit = { + player.sendMessage(messages) + super.sendMessage(messages) + } +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSender.java b/src/main/java/buttondevteam/discordplugin/DiscordSender.java deleted file mode 100755 index 0ef62a9..0000000 --- a/src/main/java/buttondevteam/discordplugin/DiscordSender.java +++ /dev/null @@ -1,117 +0,0 @@ -package buttondevteam.discordplugin; - -import discord4j.core.object.entity.Member; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.MessageChannel; -import lombok.val; -import org.bukkit.Bukkit; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.bukkit.permissions.PermissibleBase; -import org.bukkit.permissions.Permission; -import org.bukkit.permissions.PermissionAttachment; -import org.bukkit.permissions.PermissionAttachmentInfo; -import org.bukkit.plugin.Plugin; -import reactor.core.publisher.Mono; - -import java.util.Set; - -public class DiscordSender extends DiscordSenderBase implements CommandSender { - private PermissibleBase perm = new PermissibleBase(this); - - private String name; - - public DiscordSender(User user, MessageChannel channel) { - super(user, channel); - val def = "Discord user"; - name = user == null ? def : user.asMember(DiscordPlugin.mainServer.getId()) - .onErrorResume(t -> Mono.empty()).blockOptional().map(Member::getDisplayName).orElse(def); - } - - public DiscordSender(User user, MessageChannel channel, String name) { - super(user, channel); - this.name = name; - } - - @Override - public boolean isPermissionSet(String name) { - return perm.isPermissionSet(name); - } - - @Override - public boolean isPermissionSet(Permission perm) { - return this.perm.isPermissionSet(perm); - } - - @Override - public boolean hasPermission(String name) { - if (name.contains("essentials") && !name.equals("essentials.list")) - return false; - return perm.hasPermission(name); - } - - @Override - public boolean hasPermission(Permission perm) { - return this.perm.hasPermission(perm); - } - - @Override - public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) { - return perm.addAttachment(plugin, name, value); - } - - @Override - public PermissionAttachment addAttachment(Plugin plugin) { - return perm.addAttachment(plugin); - } - - @Override - public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) { - return perm.addAttachment(plugin, name, value, ticks); - } - - @Override - public PermissionAttachment addAttachment(Plugin plugin, int ticks) { - return perm.addAttachment(plugin, ticks); - } - - @Override - public void removeAttachment(PermissionAttachment attachment) { - perm.removeAttachment(attachment); - } - - @Override - public void recalculatePermissions() { - perm.recalculatePermissions(); - } - - @Override - public Set getEffectivePermissions() { - return perm.getEffectivePermissions(); - } - - @Override - public boolean isOp() { - return false; - } - - @Override - public void setOp(boolean value) { - } - - @Override - public Server getServer() { - return Bukkit.getServer(); - } - - @Override - public String getName() { - return name; - } - - @Override - public Spigot spigot() { - return new CommandSender.Spigot(); - } - -} diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSender.scala b/src/main/java/buttondevteam/discordplugin/DiscordSender.scala new file mode 100644 index 0000000..5e9c038 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/DiscordSender.scala @@ -0,0 +1,64 @@ +package buttondevteam.discordplugin + +import discord4j.core.`object`.entity.User +import discord4j.core.`object`.entity.channel.MessageChannel +import org.bukkit.{Bukkit, Server} +import org.bukkit.command.CommandSender +import org.bukkit.permissions.{PermissibleBase, Permission, PermissionAttachment, PermissionAttachmentInfo} +import org.bukkit.plugin.Plugin +import reactor.core.publisher.Mono + +import java.util + +class DiscordSender(user: User, channel: MessageChannel) extends DiscordSenderBase(user, channel) with CommandSender { + private val perm = new PermissibleBase(this) + private var name: String = null + + def this(user: User, channel: MessageChannel) { + this(user, channel) + val `def` = "Discord user" + name = if (user == null) `def` + else user.asMember(DiscordPlugin.mainServer.getId).onErrorResume((_: Throwable) => Mono.empty).blockOptional.map(_.getDisplayName).orElse(`def`) + } + + def this(user: User, channel: MessageChannel, name: String) { + this(user, channel) + this.name = name + } + + override def isPermissionSet(name: String): Boolean = perm.isPermissionSet(name) + + override def isPermissionSet(perm: Permission): Boolean = this.perm.isPermissionSet(perm) + + override def hasPermission(name: String): Boolean = { + if (name.contains("essentials") && !(name == "essentials.list")) return false + perm.hasPermission(name) + } + + override def hasPermission(perm: Permission): Boolean = this.perm.hasPermission(perm) + + override def addAttachment(plugin: Plugin, name: String, value: Boolean): PermissionAttachment = perm.addAttachment(plugin, name, value) + + override def addAttachment(plugin: Plugin): PermissionAttachment = perm.addAttachment(plugin) + + override def addAttachment(plugin: Plugin, name: String, value: Boolean, ticks: Int): PermissionAttachment = perm.addAttachment(plugin, name, value, ticks) + + override def addAttachment(plugin: Plugin, ticks: Int): PermissionAttachment = perm.addAttachment(plugin, ticks) + + override def removeAttachment(attachment: PermissionAttachment): Unit = perm.removeAttachment(attachment) + + override def recalculatePermissions(): Unit = perm.recalculatePermissions() + + override def getEffectivePermissions: util.Set[PermissionAttachmentInfo] = perm.getEffectivePermissions + + override def isOp = false + + override def setOp(value: Boolean): Unit = { + } + + override def getServer: Server = Bukkit.getServer + + override def getName: String = name + + override def spigot = new CommandSender.Spigot +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java b/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java deleted file mode 100755 index 7ada97f..0000000 --- a/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java +++ /dev/null @@ -1,75 +0,0 @@ -package buttondevteam.discordplugin; - -import buttondevteam.lib.TBMCCoreAPI; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.MessageChannel; -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.scheduler.BukkitTask; - -public abstract class DiscordSenderBase implements CommandSender { - /** - * May be null. - */ - protected User user; - protected MessageChannel channel; - - protected DiscordSenderBase(User user, MessageChannel channel) { - this.user = user; - this.channel = channel; - } - - private volatile String msgtosend = ""; - private volatile BukkitTask sendtask; - - /** - * Returns the user. May be null. - * - * @return The user or null. - */ - public User getUser() { - return user; - } - - public MessageChannel getChannel() { - return channel; - } - - private DiscordPlayer chromaUser; - - /** - * Loads the user data on first query. - * - * @return A Chroma user of Discord or a Discord user of Chroma - */ - public DiscordPlayer getChromaUser() { - if (chromaUser == null) chromaUser = DiscordPlayer.getUser(user.getId().asString(), DiscordPlayer.class); - return chromaUser; - } - - @Override - public void sendMessage(String message) { - try { - final boolean broadcast = new Exception().getStackTrace()[2].getMethodName().contains("broadcast"); - if (broadcast) //We're catching broadcasts using the Bukkit event - return; - final String sendmsg = DPUtils.sanitizeString(message); - synchronized (this) { - msgtosend += "\n" + sendmsg; - if (sendtask == null) - sendtask = Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> { - channel.createMessage((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 - } - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while sending message to DiscordSender", e, DiscordPlugin.plugin); - } - } - - @Override - public void sendMessage(String[] messages) { - sendMessage(String.join("\n", messages)); - } -} diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.scala b/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.scala new file mode 100644 index 0000000..83121aa --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.scala @@ -0,0 +1,64 @@ +package buttondevteam.discordplugin + +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.player.ChromaGamerBase +import discord4j.core.`object`.entity.User +import discord4j.core.`object`.entity.channel.MessageChannel +import org.bukkit.Bukkit +import org.bukkit.command.CommandSender +import org.bukkit.scheduler.BukkitTask + +/** + * + * @param user May be null. + * @param channel May not be null. + */ +abstract class DiscordSenderBase protected(var user: User, var channel: MessageChannel) extends CommandSender { + private var msgtosend = "" + private var sendtask: BukkitTask = null + + /** + * Returns the user. May be null. + * + * @return The user or null. + */ + def getUser: User = user + + def getChannel: MessageChannel = channel + + private var chromaUser: DiscordPlayer = null + + /** + * Loads the user data on first query. + * + * @return A Chroma user of Discord or a Discord user of Chroma + */ + def getChromaUser: DiscordPlayer = { + if (chromaUser == null) chromaUser = ChromaGamerBase.getUser(user.getId.asString, classOf[DiscordPlayer]) + chromaUser + } + + override def sendMessage(message: String): Unit = try { + val broadcast = new Exception().getStackTrace()(2).getMethodName.contains("broadcast") + if (broadcast) { //We're catching broadcasts using the Bukkit event + return + } + val sendmsg = DPUtils.sanitizeString(message) + this synchronized msgtosend += "\n" + sendmsg + if (sendtask == null) sendtask = Bukkit.getScheduler.runTaskLaterAsynchronously(DiscordPlugin.plugin, () => { + def foo(): Unit = { + channel.createMessage((if (user != null) user.getMention + "\n" + else "") + msgtosend.trim).subscribe + sendtask = null + msgtosend = "" + } + + foo() + }, 4) // Waits a 0.2 second to gather all/most of the different messages + } catch { + case e: Exception => + TBMCCoreAPI.SendException("An error occured while sending message to DiscordSender", e, DiscordPlugin.plugin) + } + + override def sendMessage(messages: Array[String]): Unit = sendMessage(String.join("\n", messages)) +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/IMCPlayer.java b/src/main/java/buttondevteam/discordplugin/IMCPlayer.java deleted file mode 100755 index c2ee28e..0000000 --- a/src/main/java/buttondevteam/discordplugin/IMCPlayer.java +++ /dev/null @@ -1,8 +0,0 @@ -package buttondevteam.discordplugin; - -import buttondevteam.discordplugin.playerfaker.VCMDWrapper; -import org.bukkit.entity.Player; - -public interface IMCPlayer extends Player { - VCMDWrapper getVanillaCmdListener(); -} diff --git a/src/main/java/buttondevteam/discordplugin/IMCPlayer.scala b/src/main/java/buttondevteam/discordplugin/IMCPlayer.scala new file mode 100644 index 0000000..70f1a5d --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/IMCPlayer.scala @@ -0,0 +1,8 @@ +package buttondevteam.discordplugin + +import buttondevteam.discordplugin.playerfaker.VCMDWrapper +import org.bukkit.entity.Player + +trait IMCPlayer[T] extends Player { + def getVanillaCmdListener: VCMDWrapper +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.scala b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.scala index e06a34d..7eaae1e 100644 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.scala +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.scala @@ -1,6 +1,7 @@ package buttondevteam.discordplugin.mcchat import buttondevteam.core.{ComponentManager, MainPlugin, component} +import buttondevteam.discordplugin.ChannelconBroadcast.ChannelconBroadcast import buttondevteam.discordplugin._ import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule import buttondevteam.discordplugin.mcchat.MCChatCustom.CustomLMD @@ -105,14 +106,14 @@ object MCChatUtils { def getSender[T <: DiscordSenderBase](senders: ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, T]], channel: Snowflake, user: User): T = { val map = senders.get(user.getId.asString) - if (map != null) return map.get(channel) - null + if (map != null) map.get(channel) + else null.asInstanceOf } def removeSender[T <: DiscordSenderBase](senders: ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, T]], channel: Snowflake, user: User): T = { val map = senders.get(user.getId.asString) - if (map != null) return map.remove(channel) - null + if (map != null) map.remove(channel) + else null.asInstanceOf } def forPublicPrivateChat(action: Mono[MessageChannel] => Mono[_]): Mono[_] = { @@ -154,7 +155,7 @@ object MCChatUtils { if (notEnabled) return Mono.empty val st = MCChatCustom.lastmsgCustom.stream.filter((clmd) => { def foo(clmd: CustomLMD): Boolean = { //new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple - if (toggle != null && ((clmd.toggles & toggle.flag) eq 0)) return false //If null then allow + if (toggle != null && ((clmd.toggles & (1 << toggle.id)) eq 0)) return false //If null then allow if (sender == null) return true clmd.groupID.equals(clmd.mcchannel.getGroupID(sender)) } diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java deleted file mode 100644 index e4cd17d..0000000 --- a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java +++ /dev/null @@ -1,144 +0,0 @@ -package buttondevteam.discordplugin.mccommands; - -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.DiscordSenderBase; -import buttondevteam.discordplugin.util.DPState; -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) { - if (checkSafeMode(player)) return true; - 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); - 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) { - if (checkSafeMode(player)) return true; - 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 restart." - }) - public void reload(CommandSender sender) { - if (DiscordPlugin.plugin.tryReloadConfig()) - sender.sendMessage("§bConfig reloaded."); - else - sender.sendMessage("§cFailed to reload config."); - } - - @Command2.Subcommand(permGroup = Command2.Subcommand.MOD_GROUP, helpText = { - "Restart the plugin", // - "This command disables and then enables the plugin." // - }) - public void restart(CommandSender sender) { - Runnable task = () -> { - if (!DiscordPlugin.plugin.tryReloadConfig()) { - sender.sendMessage("§cFailed to reload config so not restarting. Check the console."); - return; - } - MinecraftChatModule.state = DPState.RESTARTING_PLUGIN; //Reset in MinecraftChatModule - 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("§bRestart finished!"); - }; - if (!Bukkit.getName().equals("Paper")) { - getPlugin().getLogger().warning("Async plugin events are not supported by the server, running on main thread"); - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, task); - } else - Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, task); - } - - @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) { - if (checkSafeMode(sender)) return; - 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); - } - } - - private boolean checkSafeMode(CommandSender sender) { - if (DiscordPlugin.SafeMode) { - sender.sendMessage("§cThe plugin isn't initialized. Check console for details."); - return true; - } - return false; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.scala b/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.scala new file mode 100644 index 0000000..76cb49d --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.scala @@ -0,0 +1,128 @@ +package buttondevteam.discordplugin.mccommands + +import buttondevteam.discordplugin.{DPUtils, DiscordPlayer, DiscordPlugin, DiscordSenderBase} +import buttondevteam.discordplugin.commands.{ConnectCommand, VersionCommand} +import buttondevteam.discordplugin.mcchat.{MCChatUtils, MinecraftChatModule} +import buttondevteam.discordplugin.util.DPState +import buttondevteam.lib.chat.{Command2, CommandClass, ICommand2MC} +import buttondevteam.lib.player.{ChromaGamerBase, TBMCPlayer, TBMCPlayerBase} +import discord4j.core.`object`.ExtendedInvite +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 = Array(Array( + "Discord", + "This command allows performing Discord-related actions." +))) class DiscordMCCommand extends ICommand2MC { + @Command2.Subcommand def accept(player: Player): Boolean = { + if (checkSafeMode(player)) return true + val did = ConnectCommand.WaitingToConnect.get(player.getName) + if (did == null) { + player.sendMessage("§cYou don't have a pending connection to Discord.") + return true + } + val dp = ChromaGamerBase.getUser(did, classOf[DiscordPlayer]) + val mcp = TBMCPlayerBase.getPlayer(player.getUniqueId, classOf[TBMCPlayer]) + dp.connectWith(mcp) + ConnectCommand.WaitingToConnect.remove(player.getName) + MCChatUtils.UnconnectedSenders.remove(did) //Remove all unconnected, will be recreated where needed + player.sendMessage("§bAccounts connected.") + true + } + + @Command2.Subcommand def decline(player: Player): Boolean = { + if (checkSafeMode(player)) return true + val 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.") + true + } + + @Command2.Subcommand(permGroup = Command2.Subcommand.MOD_GROUP, helpText = Array(Array( + "Reload Discord plugin", + "Reloads the config. To apply some changes, you may need to also run /discord restart." + ))) def reload(sender: CommandSender): Unit = + if (DiscordPlugin.plugin.tryReloadConfig) sender.sendMessage("§bConfig reloaded.") + else sender.sendMessage("§cFailed to reload config.") + + @Command2.Subcommand(permGroup = Command2.Subcommand.MOD_GROUP, helpText = Array(Array( + "Restart the plugin", // + "This command disables and then enables the plugin." // + ))) def restart(sender: CommandSender): Unit = { + val task: Runnable = () => { + def foo(): Unit = { + if (!DiscordPlugin.plugin.tryReloadConfig) { + sender.sendMessage("§cFailed to reload config so not restarting. Check the console.") + return + } + MinecraftChatModule.state = DPState.RESTARTING_PLUGIN //Reset in MinecraftChatModule + sender.sendMessage("§bDisabling DiscordPlugin...") + Bukkit.getPluginManager.disablePlugin(DiscordPlugin.plugin) + if (!sender.isInstanceOf[DiscordSenderBase]) { //Sending to Discord errors + sender.sendMessage("§bEnabling DiscordPlugin...") + } + Bukkit.getPluginManager.enablePlugin(DiscordPlugin.plugin) + if (!sender.isInstanceOf[DiscordSenderBase]) sender.sendMessage("§bRestart finished!") + } + + foo() + } + + if (!(Bukkit.getName == "Paper")) { + getPlugin.getLogger.warning("Async plugin events are not supported by the server, running on main thread") + Bukkit.getScheduler.runTask(DiscordPlugin.plugin, task) + } + else { + Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, task) + } + } + + @Command2.Subcommand(helpText = Array(Array( + "Version command", + "Prints the plugin version"))) def version(sender: CommandSender): Unit = { + sender.sendMessage(VersionCommand.getVersion) + } + + @Command2.Subcommand(helpText = Array(Array( + "Invite", + "Shows an invite link to the server" + ))) def invite(sender: CommandSender): Unit = { + if (checkSafeMode(sender)) { + return + } + val invi: String = DiscordPlugin.plugin.inviteLink.get + if (invi.nonEmpty) { + sender.sendMessage("§bInvite link: " + invi) + return + } + DiscordPlugin.mainServer.getInvites.limitRequest(1) + .switchIfEmpty(Mono.fromRunnable(() => sender.sendMessage("§cNo invites found for the server."))) + .subscribe((inv: ExtendedInvite) => sender.sendMessage("§bInvite link: https://discord.gg/" + inv.getCode), _ => sender.sendMessage("§cThe invite link is not set and the bot has no permission to get it.")) + } + + override def getHelpText(method: Method, ann: Command2.Subcommand): Array[String] = { + method.getName match { + case "accept" => + Array[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" => + Array[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") + case _ => + super.getHelpText(method, ann) + } + } + + private def checkSafeMode(sender: CommandSender): Boolean = { + if (DiscordPlugin.SafeMode) { + sender.sendMessage("§cThe plugin isn't initialized. Check console for details.") + true + } + else false + } +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DelegatingMockMaker.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DelegatingMockMaker.java deleted file mode 100644 index 116cade..0000000 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/DelegatingMockMaker.java +++ /dev/null @@ -1,20 +0,0 @@ -package buttondevteam.discordplugin.playerfaker; - -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Delegate; -import org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker; -import org.mockito.plugins.MockMaker; - -public class DelegatingMockMaker implements MockMaker { - @Getter - @Setter - @Delegate - private MockMaker mockMaker = new SubclassByteBuddyMockMaker(); - @Getter - private static DelegatingMockMaker instance; - - public DelegatingMockMaker() { - instance = this; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DelegatingMockMaker.scala b/src/main/java/buttondevteam/discordplugin/playerfaker/DelegatingMockMaker.scala new file mode 100644 index 0000000..0e14a89 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DelegatingMockMaker.scala @@ -0,0 +1,49 @@ +package buttondevteam.discordplugin.playerfaker + +import org.mockito.MockedConstruction +import org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker +import org.mockito.invocation.MockHandler +import org.mockito.mock.MockCreationSettings +import org.mockito.plugins.MockMaker + +import java.util.Optional + +object DelegatingMockMaker { + def getInstance: DelegatingMockMaker = DelegatingMockMaker.instance + + private var instance: DelegatingMockMaker = null +} + +class DelegatingMockMaker() extends MockMaker { + DelegatingMockMaker.instance = this + + override def createMock[T](settings: MockCreationSettings[T], handler: MockHandler[_]): T = + this.mockMaker.createMock(settings, handler) + + override def createSpy[T](settings: MockCreationSettings[T], handler: MockHandler[_], instance: T): Optional[T] = + this.mockMaker.createSpy(settings, handler, instance) + + override def getHandler(mock: Any): MockHandler[_] = + this.mockMaker.getHandler(mock) + + override def resetMock(mock: Any, newHandler: MockHandler[_], settings: MockCreationSettings[_]): Unit = { + this.mockMaker.resetMock(mock, newHandler, settings) + } + + override def isTypeMockable(`type`: Class[_]): MockMaker.TypeMockability = + this.mockMaker.isTypeMockable(`type`) + + override def createStaticMock[T](`type`: Class[T], settings: MockCreationSettings[T], handler: MockHandler[_]): MockMaker.StaticMockControl[T] = + this.mockMaker.createStaticMock(`type`, settings, handler) + + override def createConstructionMock[T](`type`: Class[T], settingsFactory: Function[MockedConstruction.Context, MockCreationSettings[T]], handlerFactory: Function[MockedConstruction.Context, MockHandler[T]], mockInitializer: MockedConstruction.MockInitializer[T]): MockMaker.ConstructionMockControl[T] = + this.mockMaker.createConstructionMock[T](`type`, settingsFactory: Function[MockedConstruction.Context, MockCreationSettings[T]], handlerFactory, mockInitializer) + + def setMockMaker(mockMaker: MockMaker): Unit = { + this.mockMaker = mockMaker + } + + def getMockMaker: MockMaker = this.mockMaker + + private var mockMaker: MockMaker = new SubclassByteBuddyMockMaker +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java b/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java deleted file mode 100644 index bf3051e..0000000 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java +++ /dev/null @@ -1,96 +0,0 @@ -package buttondevteam.discordplugin.playerfaker; - -import com.destroystokyo.paper.profile.CraftPlayerProfile; -import lombok.RequiredArgsConstructor; -import net.bytebuddy.implementation.bind.annotation.IgnoreForBinding; -import org.bukkit.Bukkit; -import org.bukkit.Server; -import org.bukkit.entity.Player; -import org.mockito.Mockito; -import org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker; - -import java.lang.reflect.Modifier; -import java.util.*; - -public class ServerWatcher { - private List playerList; - public final List fakePlayers = new ArrayList<>(); - private Server origServer; - - @IgnoreForBinding - public void enableDisable(boolean enable) throws Exception { - var serverField = Bukkit.class.getDeclaredField("server"); - serverField.setAccessible(true); - if (enable) { - var serverClass = Bukkit.getServer().getClass(); - var originalServer = serverField.get(null); - DelegatingMockMaker.getInstance().setMockMaker(new InlineByteBuddyMockMaker()); - var settings = Mockito.withSettings().stubOnly() - .defaultAnswer(invocation -> { - var method = invocation.getMethod(); - int pc = method.getParameterCount(); - Player player = null; - switch (method.getName()) { - case "getPlayer": - if (pc == 1 && method.getParameterTypes()[0] == UUID.class) - player = MCChatUtils.LoggedInPlayers.get(invocation.getArgument(0)); - break; - case "getPlayerExact": - if (pc == 1) { - final String argument = invocation.getArgument(0); - player = MCChatUtils.LoggedInPlayers.values().stream() - .filter(dcp -> dcp.getName().equalsIgnoreCase(argument)).findAny().orElse(null); - } - break; - /*case "getOnlinePlayers": - if (playerList == null) { - @SuppressWarnings("unchecked") var list = (List) method.invoke(origServer, invocation.getArguments()); - playerList = new AppendListView<>(list, fakePlayers); - } - Your scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should. - return playerList;*/ - case "createProfile": //Paper's method, casts the player to a CraftPlayer - if (pc == 2) { - UUID uuid = invocation.getArgument(0); - String name = invocation.getArgument(1); - player = uuid != null ? MCChatUtils.LoggedInPlayers.get(uuid) : null; - if (player == null && name != null) - player = MCChatUtils.LoggedInPlayers.values().stream() - .filter(dcp -> dcp.getName().equalsIgnoreCase(name)).findAny().orElse(null); - if (player != null) - return new CraftPlayerProfile(player.getUniqueId(), player.getName()); - } - break; - } - if (player != null) - return player; - return method.invoke(origServer, invocation.getArguments()); - }); - //var mock = mockMaker.createMock(settings, MockHandlerFactory.createMockHandler(settings)); - //thread.setContextClassLoader(cl); - var mock = Mockito.mock(serverClass, settings); - for (var field : serverClass.getFields()) //Copy public fields, private fields aren't accessible directly anyways - if (!Modifier.isFinal(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())) - field.set(mock, field.get(originalServer)); - serverField.set(null, mock); - origServer = (Server) originalServer; - } else if (origServer != null) - serverField.set(null, origServer); - } - - @RequiredArgsConstructor - public static class AppendListView extends AbstractSequentialList { - private final List originalList; - private final List additionalList; - - @Override - public ListIterator listIterator(int i) { - int os = originalList.size(); - return i < os ? originalList.listIterator(i) : additionalList.listIterator(i - os); - } - - @Override - public int size() { - return originalList.size() + additionalList.size(); - } - } -} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.scala b/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.scala new file mode 100644 index 0000000..ceb01b8 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.scala @@ -0,0 +1,91 @@ +package buttondevteam.discordplugin.playerfaker + +import buttondevteam.discordplugin.DiscordConnectedPlayer +import buttondevteam.discordplugin.mcchat.MCChatUtils +import com.destroystokyo.paper.profile.CraftPlayerProfile +import net.bytebuddy.implementation.bind.annotation.IgnoreForBinding +import org.bukkit.{Bukkit, Server} +import org.bukkit.entity.Player +import org.mockito.Mockito +import org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker +import org.mockito.invocation.InvocationOnMock + +import java.lang.reflect.Modifier +import java.util +import java.util._ + +object ServerWatcher { + + class AppendListView[T](private val originalList: java.util.List[T], private val additionalList: java.util.List[T]) extends java.util.AbstractSequentialList[T] { + + override def listIterator(i: Int): util.ListIterator[T] = { + val os = originalList.size + if (i < os) originalList.listIterator(i) + else additionalList.listIterator(i - os) + } + + override def size: Int = originalList.size + additionalList.size + } + +} + +class ServerWatcher { + final val fakePlayers = new util.ArrayList[Player] + private var origServer: Server = null + + @IgnoreForBinding + @throws[Exception] + def enableDisable(enable: Boolean): Unit = { + val serverField = classOf[Bukkit].getDeclaredField("server") + serverField.setAccessible(true) + if (enable) { + val serverClass = Bukkit.getServer.getClass + val originalServer = serverField.get(null) + DelegatingMockMaker.getInstance.setMockMaker(new InlineByteBuddyMockMaker) + val settings = Mockito.withSettings.stubOnly.defaultAnswer((invocation: InvocationOnMock) => { + def foo(invocation: InvocationOnMock): AnyRef = { + val method = invocation.getMethod + val pc = method.getParameterCount + var player: DiscordConnectedPlayer = null + method.getName match { + case "getPlayer" => + if (pc == 1 && (method.getParameterTypes()(0) eq classOf[UUID])) player = MCChatUtils.LoggedInPlayers.get(invocation.getArgument[UUID](0)) + case "getPlayerExact" => + if (pc == 1) { + val argument = invocation.getArgument(0) + player = MCChatUtils.LoggedInPlayers.values.stream.filter((dcp) => dcp.getName.equalsIgnoreCase(argument)).findAny.orElse(null) + } + + /*case "getOnlinePlayers": + if (playerList == null) { + @SuppressWarnings("unchecked") var list = (List) method.invoke(origServer, invocation.getArguments()); + playerList = new AppendListView<>(list, fakePlayers); + } - Your scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should. + return playerList;*/ case "createProfile" => //Paper's method, casts the player to a CraftPlayer + if (pc == 2) { + val uuid = invocation.getArgument(0) + val name = invocation.getArgument(1) + player = if (uuid != null) MCChatUtils.LoggedInPlayers.get(uuid) + else null + if (player == null && name != null) player = MCChatUtils.LoggedInPlayers.values.stream.filter((dcp) => dcp.getName.equalsIgnoreCase(name)).findAny.orElse(null) + if (player != null) return new CraftPlayerProfile(player.getUniqueId, player.getName) + } + } + if (player != null) return player + method.invoke(origServer, invocation.getArguments) + } + + foo(invocation) + }) + //var mock = mockMaker.createMock(settings, MockHandlerFactory.createMockHandler(settings)); + //thread.setContextClassLoader(cl); + val mock = Mockito.mock(serverClass, settings) + for (field <- serverClass.getFields) { //Copy public fields, private fields aren't accessible directly anyways + if (!Modifier.isFinal(field.getModifiers) && !Modifier.isStatic(field.getModifiers)) field.set(mock, field.get(originalServer)) + } + serverField.set(null, mock) + origServer = originalServer.asInstanceOf[Server] + } + else if (origServer != null) serverField.set(null, origServer) + } +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java b/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java index 7727673..6ff856b 100644 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java @@ -2,6 +2,7 @@ package buttondevteam.discordplugin.playerfaker; import buttondevteam.discordplugin.DiscordSenderBase; import buttondevteam.discordplugin.IMCPlayer; +import buttondevteam.discordplugin.mcchat.MinecraftChatModule; import buttondevteam.lib.TBMCCoreAPI; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java b/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java index c13b7dd..055c5e8 100644 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java @@ -2,6 +2,7 @@ package buttondevteam.discordplugin.playerfaker.perm; import buttondevteam.discordplugin.DiscordConnectedPlayer; import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.discordplugin.mcchat.MCChatUtils; import buttondevteam.lib.TBMCCoreAPI; import me.lucko.luckperms.bukkit.LPBukkitBootstrap; import me.lucko.luckperms.bukkit.LPBukkitPlugin; diff --git a/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.java b/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.java deleted file mode 100644 index c1a4079..0000000 --- a/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.java +++ /dev/null @@ -1,126 +0,0 @@ -package buttondevteam.discordplugin.role; - -import buttondevteam.core.ComponentManager; -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.architecture.ComponentMetadata; -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.Role; -import discord4j.core.object.entity.channel.MessageChannel; -import discord4j.rest.util.Color; -import lombok.val; -import org.bukkit.Bukkit; -import reactor.core.publisher.Mono; - -import java.util.Collections; -import java.util.List; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** - * Automatically collects roles with a certain color. - * Users can add these roles to themselves using the /role Discord command. - */ -@ComponentMetadata(enabledByDefault = false) -public class GameRoleModule extends Component { - public List GameRoles; - - private final RoleCommand command = new RoleCommand(this); - - @Override - protected void enable() { - getPlugin().getManager().registerCommand(command); - GameRoles = DiscordPlugin.mainServer.getRoles().filterWhen(this::isGameRole).map(Role::getName).collect(Collectors.toList()).block(); - } - - @Override - protected void disable() { - getPlugin().getManager().unregisterCommand(command); - } - - /** - * The channel where the bot logs when it detects a role change that results in a new game role or one being removed. - */ - private final ReadOnlyConfigData> logChannel = DPUtils.channelData(getConfig(), "logChannel"); - - /** - * The role color that is used by game roles. - * Defaults to the second to last in the upper row - #95a5a6. - */ - private final ReadOnlyConfigData roleColor = getConfig().getConfig("roleColor") - .def(Color.of(149, 165, 166)) - .getter(rgb -> Color.of(Integer.parseInt(((String) rgb).substring(1), 16))) - .setter(color -> String.format("#%08x", color.getRGB())).buildReadOnly(); - - public static void handleRoleEvent(RoleEvent roleEvent) { - val grm = ComponentManager.getIfEnabled(GameRoleModule.class); - if (grm == null) return; - val GameRoles = grm.GameRoles; - val logChannel = grm.logChannel.get(); - Predicate notMainServer = r -> r.getGuildId().asLong() != DiscordPlugin.mainServer.getId().asLong(); - if (roleEvent instanceof RoleCreateEvent) { - Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> { - Role role = ((RoleCreateEvent) roleEvent).getRole(); - if (notMainServer.test(role)) - return; - 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 game role color.")); - return Mono.empty(); - }).subscribe(); - }, 100); - } else if (roleEvent instanceof RoleDeleteEvent) { - Role role = ((RoleDeleteEvent) roleEvent).getRole().orElse(null); - if (role == null) return; - if (notMainServer.test(role)) - 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 (!event.getOld().isPresent()) { - grm.logWarn("Old role not stored, cannot update game role!"); - return; - } - Role or = event.getOld().get(); - if (notMainServer.test(or)) - return; - 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 its 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 color of one.")); - } - } - return Mono.empty(); - }).subscribe(); - } - } - - 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 = roleColor.get(); - 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.getId())))) //Below one of our roles - .defaultIfEmpty(false); - } -} diff --git a/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.scala b/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.scala new file mode 100644 index 0000000..95648ec --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.scala @@ -0,0 +1,107 @@ +package buttondevteam.discordplugin.role + +import buttondevteam.core.ComponentManager +import buttondevteam.discordplugin.{DPUtils, DiscordPlugin} +import buttondevteam.lib.architecture.{Component, ComponentMetadata} +import discord4j.core.`object`.entity.Role +import discord4j.core.`object`.entity.channel.MessageChannel +import discord4j.core.event.domain.role.{RoleCreateEvent, RoleDeleteEvent, RoleEvent, RoleUpdateEvent} +import discord4j.rest.util.Color +import org.bukkit.Bukkit +import reactor.core.publisher.Mono + +import java.util.Collections +import java.util.stream.Collectors + +/** + * Automatically collects roles with a certain color. + * Users can add these roles to themselves using the /role Discord command. + */ +@ComponentMetadata(enabledByDefault = false) object GameRoleModule { + def handleRoleEvent(roleEvent: RoleEvent): Unit = { + val grm = ComponentManager.getIfEnabled(classOf[GameRoleModule]) + if (grm == null) return + val GameRoles = grm.GameRoles + val logChannel = grm.logChannel.get + val notMainServer = (r: Role) => r.getGuildId.asLong != DiscordPlugin.mainServer.getId.asLong + roleEvent match { + case roleCreateEvent: RoleCreateEvent => Bukkit.getScheduler.runTaskLaterAsynchronously(DiscordPlugin.plugin, () => { + def foo(): Unit = { + val role = roleCreateEvent.getRole + if (notMainServer(role)) return + grm.isGameRole(role).flatMap((b: Boolean) => { + def foo(b: Boolean): Mono[_] = { + if (!b) return Mono.empty //Deleted or not a game role + GameRoles.add(role.getName) + if (logChannel != null) return logChannel.flatMap((ch: MessageChannel) => ch.createMessage("Added " + role.getName + " as game role. If you don't want this, change the role's color from the game role color.")) + Mono.empty + } + + foo(b) + }).subscribe + } + + foo() + }, 100) + case roleDeleteEvent: RoleDeleteEvent => + val role = roleDeleteEvent.getRole.orElse(null) + if (role == null) return + if (notMainServer(role)) return + if (GameRoles.remove(role.getName) && logChannel != null) logChannel.flatMap((ch: MessageChannel) => ch.createMessage("Removed " + role.getName + " as a game role.")).subscribe + case roleUpdateEvent: RoleUpdateEvent => + if (!roleUpdateEvent.getOld.isPresent) { + grm.logWarn("Old role not stored, cannot update game role!") + return + } + val or = roleUpdateEvent.getOld.get + if (notMainServer(or)) return + val cr = roleUpdateEvent.getCurrent + grm.isGameRole(cr).flatMap((b: Boolean) => { + def foo(b: Boolean): Mono[_] = { + if (!b) if (GameRoles.remove(or.getName) && logChannel != null) return logChannel.flatMap((ch: MessageChannel) => ch.createMessage("Removed " + or.getName + " as a game role because its color changed.")) + else { + if (GameRoles.contains(or.getName) && or.getName == cr.getName) return Mono.empty + val removed = GameRoles.remove(or.getName) //Regardless of whether it was a game role + GameRoles.add(cr.getName) //Add it because it has no color + if (logChannel != null) if (removed) return logChannel.flatMap((ch: MessageChannel) => ch.createMessage("Changed game role from " + or.getName + " to " + cr.getName + ".")) + else return logChannel.flatMap((ch: MessageChannel) => ch.createMessage("Added " + cr.getName + " as game role because it has the color of one.")) + } + Mono.empty + } + + foo(b) + }).subscribe + case _ => + } + } +} + +@ComponentMetadata(enabledByDefault = false) class GameRoleModule extends Component[DiscordPlugin] { + var GameRoles: java.util.List[String] = null + final private val command = new RoleCommand(this) + + override protected def enable(): Unit = { + getPlugin.manager.registerCommand(command) + GameRoles = DiscordPlugin.mainServer.getRoles.filterWhen(this.isGameRole _).map(_.getName).collect(Collectors.toList).block + } + + override protected def disable(): Unit = getPlugin.manager.unregisterCommand(command) + + /** + * The channel where the bot logs when it detects a role change that results in a new game role or one being removed. + */ + final private val logChannel = DPUtils.channelData(getConfig, "logChannel") + /** + * The role color that is used by game roles. + * Defaults to the second to last in the upper row - #95a5a6. + */ + final private val roleColor = getConfig.getConfig[Color]("roleColor").`def`(Color.of(149, 165, 166)).getter((rgb: Any) => Color.of(Integer.parseInt(rgb.asInstanceOf[String].substring(1), 16))).setter((color: Color) => String.format("#%08x", color.getRGB)).buildReadOnly + + private def isGameRole(r: Role): Mono[Boolean] = { + if (r.getGuildId.asLong != DiscordPlugin.mainServer.getId.asLong) return Mono.just(false) //Only allow on the main server + val rc = roleColor.get + if (r.getColor equals rc) + DiscordPlugin.dc.getSelf.flatMap((u) => u.asMember(DiscordPlugin.mainServer.getId)).flatMap((m) => m.hasHigherRoles(Collections.singleton(r.getId))).defaultIfEmpty(false) //Below one of our roles + else Mono.just(false) + } +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/role/RoleCommand.java b/src/main/java/buttondevteam/discordplugin/role/RoleCommand.java deleted file mode 100755 index aac0aae..0000000 --- a/src/main/java/buttondevteam/discordplugin/role/RoleCommand.java +++ /dev/null @@ -1,107 +0,0 @@ -package buttondevteam.discordplugin.role; - -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -import discord4j.core.object.entity.Role; -import lombok.val; -import reactor.core.publisher.Mono; - -import java.util.List; - -@CommandClass -public class RoleCommand extends ICommand2DC { - - private GameRoleModule grm; - - RoleCommand(GameRoleModule grm) { - this.grm = grm; - } - - @Command2.Subcommand(helpText = { - "Add role", - "This command adds a role to your account." - }) - public boolean add(Command2DCSender sender, @Command2.TextArg String rolename) { - final Role role = checkAndGetRole(sender, rolename); - if (role == null) - return true; - try { - sender.getMessage().getAuthorAsMember() - .flatMap(m -> m.addRole(role.getId()).switchIfEmpty(Mono.fromRunnable(() -> sender.sendMessage("added role.")))) - .subscribe(); - } catch (Exception e) { - TBMCCoreAPI.SendException("Error while adding role!", e, grm); - sender.sendMessage("an error occured while adding the role."); - } - return true; - } - - @Command2.Subcommand(helpText = { - "Remove role", - "This command removes a role from your account." - }) - public boolean remove(Command2DCSender sender, @Command2.TextArg String rolename) { - final Role role = checkAndGetRole(sender, rolename); - if (role == null) - return true; - try { - sender.getMessage().getAuthorAsMember() - .flatMap(m -> m.removeRole(role.getId()).switchIfEmpty(Mono.fromRunnable(() -> sender.sendMessage("removed role.")))) - .subscribe(); - } catch (Exception e) { - TBMCCoreAPI.SendException("Error while removing role!", e, grm); - sender.sendMessage("an error occured while removing the role."); - } - return true; - } - - @Command2.Subcommand - public void list(Command2DCSender sender) { - var sb = new StringBuilder(); - boolean b = false; - for (String role : (Iterable) grm.GameRoles.stream().sorted()::iterator) { - sb.append(role); - if (!b) - for (int j = 0; j < Math.max(1, 20 - role.length()); j++) - sb.append(" "); - else - sb.append("\n"); - b = !b; - } - if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '\n') - sb.append('\n'); - sender.sendMessage("list of roles:\n```\n" + sb + "```"); - } - - 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(); - if (!orn.isPresent()) { - sender.sendMessage("that role cannot be found."); - list(sender); - return null; - } - rname = orn.get(); - } - 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/role/RoleCommand.scala b/src/main/java/buttondevteam/discordplugin/role/RoleCommand.scala new file mode 100644 index 0000000..08db583 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/role/RoleCommand.scala @@ -0,0 +1,84 @@ +package buttondevteam.discordplugin.role + +import buttondevteam.discordplugin.DiscordPlugin +import buttondevteam.discordplugin.commands.{Command2DCSender, ICommand2DC} +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.chat.{Command2, CommandClass} +import discord4j.core.`object`.entity.Role +import reactor.core.publisher.Mono + +@CommandClass class RoleCommand private[role](var grm: GameRoleModule) extends ICommand2DC { + @Command2.Subcommand(helpText = Array(Array( + "Add role", + "This command adds a role to your account." + ))) def add(sender: Command2DCSender, @Command2.TextArg rolename: String): Boolean = { + val role = checkAndGetRole(sender, rolename) + if (role == null) return true + try sender.getMessage.getAuthorAsMember.flatMap(m => m.addRole(role.getId).switchIfEmpty(Mono.fromRunnable(() => sender.sendMessage("added role.")))).subscribe + catch { + case e: Exception => + TBMCCoreAPI.SendException("Error while adding role!", e, grm) + sender.sendMessage("an error occured while adding the role.") + } + true + } + + @Command2.Subcommand(helpText = Array(Array( + "Remove role", + "This command removes a role from your account." + ))) def remove(sender: Command2DCSender, @Command2.TextArg rolename: String): Boolean = { + val role = checkAndGetRole(sender, rolename) + if (role == null) return true + try sender.getMessage.getAuthorAsMember.flatMap(m => m.removeRole(role.getId).switchIfEmpty(Mono.fromRunnable(() => sender.sendMessage("removed role.")))).subscribe + catch { + case e: Exception => + TBMCCoreAPI.SendException("Error while removing role!", e, grm) + sender.sendMessage("an error occured while removing the role.") + } + true + } + + @Command2.Subcommand def list(sender: Command2DCSender): Unit = { + val sb = new StringBuilder + var b = false + for (role <- grm.GameRoles.stream.sorted.iterator.asInstanceOf[Iterable[String]]) { + sb.append(role) + if (!b) for (_ <- 0 until Math.max(1, 20 - role.length)) { + sb.append(" ") + } + else sb.append("\n") + b = !b + } + if (sb.nonEmpty && sb.charAt(sb.length - 1) != '\n') sb.append('\n') + sender.sendMessage("list of roles:\n```\n" + sb + "```") + } + + private def checkAndGetRole(sender: Command2DCSender, rolename: String): Role = { + var 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 + if (!orn.isPresent) { + sender.sendMessage("that role cannot be found.") + list(sender) + return null + } + rname = orn.get + } + val frname = rname + val 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 + } + roles.get(0) + } +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/util/DPState.java b/src/main/java/buttondevteam/discordplugin/util/DPState.java deleted file mode 100644 index b83d4ac..0000000 --- a/src/main/java/buttondevteam/discordplugin/util/DPState.java +++ /dev/null @@ -1,24 +0,0 @@ -package buttondevteam.discordplugin.util; - -public enum DPState { - /** - * Used from server start until anything else happens - */ - RUNNING, - /** - * Used when /restart is detected - */ - RESTARTING_SERVER, - /** - * Used when the plugin is disabled by outside forces - */ - STOPPING_SERVER, - /** - * Used when /discord restart is run - */ - RESTARTING_PLUGIN, - /** - * Used when the plugin is in the RUNNING state when the chat is disabled - */ - DISABLED_MCCHAT -} diff --git a/src/main/java/buttondevteam/discordplugin/util/DPState.scala b/src/main/java/buttondevteam/discordplugin/util/DPState.scala new file mode 100644 index 0000000..34bde89 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/util/DPState.scala @@ -0,0 +1,31 @@ +package buttondevteam.discordplugin.util + +object DPState extends Enumeration { + type DPState = Value + val + + /** + * Used from server start until anything else happens + */ + RUNNING, + + /** + * Used when /restart is detected + */ + RESTARTING_SERVER, + + /** + * Used when the plugin is disabled by outside forces + */ + STOPPING_SERVER, + + /** + * Used when /discord restart is run + */ + RESTARTING_PLUGIN, + + /** + * Used when the plugin is in the RUNNING state when the chat is disabled + */ + DISABLED_MCCHAT = Value +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/util/Timings.java b/src/main/java/buttondevteam/discordplugin/util/Timings.java deleted file mode 100644 index af91b0f..0000000 --- a/src/main/java/buttondevteam/discordplugin/util/Timings.java +++ /dev/null @@ -1,14 +0,0 @@ -package buttondevteam.discordplugin.util; - -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(); - } -} diff --git a/src/main/java/buttondevteam/discordplugin/util/Timings.scala b/src/main/java/buttondevteam/discordplugin/util/Timings.scala new file mode 100644 index 0000000..58702a0 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/util/Timings.scala @@ -0,0 +1,12 @@ +package buttondevteam.discordplugin.util + +import buttondevteam.discordplugin.listeners.CommonListeners + +class Timings() { + private var start = System.nanoTime + + def printElapsed(message: String): Unit = { + CommonListeners.debug(message + " (" + (System.nanoTime - start) / 1000000L + ")") + start = System.nanoTime + } +} \ No newline at end of file diff --git a/src/main/scala/Test.scala b/src/main/scala/Test.scala deleted file mode 100644 index 743d12a..0000000 --- a/src/main/scala/Test.scala +++ /dev/null @@ -1,5 +0,0 @@ -import buttondevteam.discordplugin.DiscordPlugin - -object Test extends App { - println(DiscordPlugin.plugin) -} \ No newline at end of file