diff --git a/.gitignore b/.gitignore index 1d227f7..9ab5846 100755 --- a/.gitignore +++ b/.gitignore @@ -1,225 +1,226 @@ -################# -## Eclipse -################# - -*.pydevproject -.metadata/ -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.classpath -.settings/ -.loadpath -target/ -.project - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# CDT-specific -.cproject - -# PDT-specific -.buildpath - - -################# -## Visual Studio -################# - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.sln.docstates - -# Build results - -[Dd]ebug/ -[Rr]elease/ -x64/ -build/ -[Bb]in/ -[Oo]bj/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -*_i.c -*_p.c -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.log -*.scc - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -*.ncrunch* -.*crunch*.local.xml - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.Publish.xml -*.pubxml -*.publishproj - -# NuGet Packages Directory -## TO!DO: If you have NuGet Package Restore enabled, uncomment the next line -#packages/ - -# Windows Azure Build Output -csx -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -sql/ -*.Cache -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.[Pp]ublish.xml -*.pfx -*.publishsettings - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file to a newer -# Visual Studio version. Backup files are not needed, because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -App_Data/*.mdf -App_Data/*.ldf - -############# -## Windows detritus -############# - -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Mac crap -.DS_Store - - -############# -## Python -############# - -*.py[cod] - -# Packages -*.egg -*.egg-info -dist/ -build/ -eggs/ -parts/ -var/ -sdist/ -develop-eggs/ -.installed.cfg - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox - -#Translations -*.mo - -#Mr Developer -.mr.developer.cfg -.metadata/* -TheButtonAutoFlair/out/artifacts/Autoflair/Autoflair.jar -*.iml -*.name -.idea/compiler.xml -*.xml - -Token.txt +################# +## Eclipse +################# + +*.pydevproject +.metadata/ +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath +target/ +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + + +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +build/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml +*.publishproj + +# NuGet Packages Directory +## TO!DO: If you have NuGet Package Restore enabled, uncomment the next line +#packages/ + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.pfx +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +############# +## Windows detritus +############# + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac crap +.DS_Store + + +############# +## Python +############# + +*.py[cod] + +# Packages +*.egg +*.egg-info +dist/ +build/ +eggs/ +parts/ +var/ +sdist/ +develop-eggs/ +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg +.metadata/* +TheButtonAutoFlair/out/artifacts/Autoflair/Autoflair.jar +*.iml +*.name +.idea/compiler.xml +*.xml + +Token.txt +.bsp diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..19c60c3 --- /dev/null +++ b/build.sbt @@ -0,0 +1,141 @@ +import org.bukkit.configuration.file.YamlConfiguration + +import java.util.regex.Pattern +import scala.io.Source +import scala.util.Using + +name := "Chroma-Discord" + +version := "1.1" + +scalaVersion := "3.0.0" + +resolvers += "spigot-repo" at "https://hub.spigotmc.org/nexus/content/repositories/snapshots/" +resolvers += "jitpack.io" at "https://jitpack.io" +resolvers += Resolver.mavenLocal + +libraryDependencies ++= Seq( + "org.spigotmc" % "spigot-api" % "1.12.2-R0.1-SNAPSHOT" % Provided, + "org.spigotmc" % "spigot" % "1.12.2-R0.1-SNAPSHOT" % Provided, + "org.spigotmc." % "spigot" % "1.14.4-R0.1-SNAPSHOT" % Provided, + "com.destroystokyo.paper" % "paper" % "1.16.3-R0.1-SNAPSHOT" % Provided, + + "com.discord4j" % "discord4j-core" % "3.2.1", + "org.slf4j" % "slf4j-jdk14" % "1.7.32", + "com.vdurmont" % "emoji-java" % "5.1.1", + "org.mockito" % "mockito-core" % "4.2.0", + "io.projectreactor" % "reactor-scala-extensions_2.13" % "0.8.0", + // https://mvnrepository.com/artifact/org.immutables/value + "org.immutables" % "value" % "2.8.8" % "provided", + + "com.github.TBMCPlugins.ChromaCore" % "Chroma-Core" % "v1.0.0" % Provided, + "net.ess3" % "EssentialsX" % "2.17.1" % Provided, + "net.luckperms" % "api" % "5.3" % Provided, +) + +assembly / assemblyJarName := "Chroma-Discord.jar" +assembly / assemblyShadeRules := Seq( + "io.netty", "com.fasterxml", "org.mockito", "org.slf4j" +).map { p => + ShadeRule.rename(s"$p.**" -> "btndvtm.dp.@0").inAll +} + +assembly / assemblyMergeStrategy := { + case PathList("META-INF", "io.netty.versions.properties") => MergeStrategy.concat + // https://stackoverflow.com/a/55557287/457612 + case "module-info.class" => MergeStrategy.discard + case x => (assembly / assemblyMergeStrategy).value(x) +} + +val saveConfigComments = TaskKey[Seq[File]]("saveConfigComments") +saveConfigComments := { + val sv = (Compile / sources).value + val cdataRegex = Pattern.compile("(?:def|val|var) (\\w+)(?::[^=]+)? = (?:getI?Config|DPUtils.\\w+Data)") //Hack: DPUtils + val clRegex = Pattern.compile("class (\\w+).* extends ((?:\\w|\\d)+)") + val objRegex = Pattern.compile("object (\\w+)") + val subRegex = Pattern.compile("def `?(\\w+)`?") + val subParamRegex = Pattern.compile("((?:\\w|\\d)+): ((?:\\w|\\d)+)") + val configConfig = new YamlConfiguration() + val commandConfig = new YamlConfiguration() + + def getConfigComments(line: String, clKey: String, comment: String, justCommented: Boolean): (String, String, Boolean) = { + val clMatcher = clRegex.matcher(line) + if (clKey == null && clMatcher.find()) { //First occurrence + (if (clMatcher.group(2).contains("Component")) + "components." + clMatcher.group(1) + else "global", comment, justCommented) + } else if (line.contains("/**")) { + (clKey, "", false) + } else if (line.contains("*/") && comment != null) + (clKey, comment, true) + else if (comment != null) { + if (justCommented) { + if (clKey != null) { + val matcher = cdataRegex.matcher(line) + if (matcher.find()) + configConfig.set(s"$clKey.${matcher.group(1)}", comment.trim) + } + else { + val matcher = objRegex.matcher(line) + if (matcher.find()) { + val clName = matcher.group(1) + val compKey = if (clName.contains("Module")) s"component.$clName" + else if (clName.contains("Plugin")) "global" + else null + if (compKey != null) + configConfig.set(s"$compKey.generalDescriptionInsteadOfAConfig", comment.trim) + } + } + (clKey, null, false) + } + else (clKey, comment + line.replaceFirst("^\\s*\\*\\s+", "") + "\n", justCommented) + } + else (clKey, comment, justCommented) + } + + for (file <- sv) { + Using(Source.fromFile(file)) { src => + var clKey: String = null + var comment: String = null + var justCommented: Boolean = false + + var subCommand = false + var pkg: String = null + var clName: String = null + for (line <- src.getLines) { + val (clk, c, jc) = getConfigComments(line, clKey, comment, justCommented) + clKey = clk; comment = c; justCommented = jc + + val objMatcher = objRegex.matcher(line) + val clMatcher = clRegex.matcher(line) + if (pkg == null && line.startsWith("package ")) + pkg = line.substring("package ".length) + else if (clName == null && objMatcher.find()) + clName = objMatcher.group(1) + else if (clName == null && clMatcher.find()) + clName = clMatcher.group(1) + val subMatcher = subRegex.matcher(line) + val subParamMatcher = subParamRegex.matcher(line) + val sub = line.contains("@Subcommand") || line.contains("@Command2.Subcommand") + if (sub) subCommand = true + else if (line.contains("}")) subCommand = false + if (subCommand && subMatcher.find()) { + /*val groups = (2 to subMatcher.groupCount()).map(subMatcher.group) + val pairs = for (i <- groups.indices by 2) yield (groups(i), groups(i + 1))*/ + val mname = subMatcher.group(1) + val params = Iterator.continually(()).takeWhile(_ => subParamMatcher.find()) + .map(_ => subParamMatcher.group(1)).drop(1) + val section = commandConfig.createSection(s"$pkg.$clName.$mname") + section.set("method", s"$mname()") + section.set("params", params.mkString(" ")) + subCommand = false + } + } + configConfig.save("target/configHelp.yml") + commandConfig.save("target/commands.yml") + }.recover[Unit]({ case t => t.printStackTrace() }) + } + Seq(file("target/configHelp.yml"), file("target/commands.yml")) +} + +Compile / resourceGenerators += saveConfigComments diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 6d69e8d..0000000 --- a/pom.xml +++ /dev/null @@ -1,226 +0,0 @@ - - 4.0.0 - - - com.github.TBMCPlugins.ChromaCore - CorePOM - master-SNAPSHOT - - - com.github.TBMCPlugins - Chroma-Discord - v${noprefix.version}-SNAPSHOT - jar - - Chroma-Discord - http://maven.apache.org - - - - src/main/java - - - src/main/resources - true - - - Chroma-Discord - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.1 - - - package - - shade - - - - - - - - - io.netty - btndvtm.dp.io.netty - - - - - com.fasterxml - btndvtm.dp.com.fasterxml - - - org.mockito - btndvtm.dp.org.mockito - - - org.slf4j - btndvtm.dp.org.slf4j - - - - - - - - maven-surefire-plugin - 2.4.2 - - false - - - - - - - - UTF-8 - 1.0.0 - - - - - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots/ - - - jcenter - http://jcenter.bintray.com - - - jitpack.io - https://jitpack.io - - - - Essentials - https://ci.ender.zone/plugin/repository/everything/ - - - projectlombok.org - http://projectlombok.org/mavenrepo - - - - - papermc - https://papermc.io/repo/repository/maven-public/ - - - - - - junit - junit - 4.13.1 - test - - - org.spigotmc - spigot-api - 1.12.2-R0.1-SNAPSHOT - provided - - - org.spigotmc - spigot - 1.12.2-R0.1-SNAPSHOT - provided - - - org.spigotmc. - spigot - 1.14.4-R0.1-SNAPSHOT - provided - - - com.destroystokyo.paper - paper - 1.16.3-R0.1-SNAPSHOT - provided - - - - com.discord4j - discord4j-core - 3.1.3 - - - - org.slf4j - slf4j-jdk14 - 1.7.21 - - - com.github.TBMCPlugins.ChromaCore - Chroma-Core - v1.0.0 - provided - - - net.ess3 - EssentialsX - 2.17.1 - provided - - - - org.projectlombok - lombok - 1.18.10 - provided - - - - com.vdurmont - emoji-java - 4.0.0 - - - - - com.github.lucko.LuckPerms - bukkit - master-SNAPSHOT - provided - - - - org.mockito - mockito-core - 3.5.13 - - - diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..7cde0be --- /dev/null +++ b/project/build.properties @@ -0,0 +1,2 @@ +sbt.version=1.5.8 +scala.version=3.0.0 \ No newline at end of file diff --git a/project/build.sbt b/project/build.sbt new file mode 100644 index 0000000..c98cb35 --- /dev/null +++ b/project/build.sbt @@ -0,0 +1,6 @@ +//lazy val commenter = RootProject(file("../commenter")) +//lazy val root = (project in file(".")).dependsOn(commenter) + +resolvers += Resolver.mavenLocal + +libraryDependencies += "org.spigotmc" % "spigot-api" % "1.12.2-R0.1-SNAPSHOT" diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..72477a2 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0") diff --git a/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.java b/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.java deleted file mode 100644 index cc52f5e..0000000 --- a/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.java +++ /dev/null @@ -1,24 +0,0 @@ -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; -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/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/ChromaBot.java b/src/main/java/buttondevteam/discordplugin/ChromaBot.java deleted file mode 100755 index 92f0e9d..0000000 --- a/src/main/java/buttondevteam/discordplugin/ChromaBot.java +++ /dev/null @@ -1,52 +0,0 @@ -package buttondevteam.discordplugin; - -import buttondevteam.discordplugin.mcchat.MCChatUtils; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.channel.MessageChannel; -import lombok.Getter; -import org.bukkit.scheduler.BukkitScheduler; -import reactor.core.publisher.Mono; - -import javax.annotation.Nullable; -import java.util.function.Function; - -public class ChromaBot { - /** - * May be null if it's not initialized. Initialization happens after the server is done loading (using {@link BukkitScheduler#runTaskAsynchronously(org.bukkit.plugin.Plugin, Runnable)}) - */ - private static @Getter ChromaBot instance; - - /** - * This will set the instance field. - */ - ChromaBot() { - instance = this; - } - - static void delete() { - instance = null; - } - - /** - * Send a message to the chat channels and private chats. - * - * @param message The message to send, duh (use {@link MessageChannel#createMessage(String)}) - */ - public void sendMessage(Function, Mono> message) { - MCChatUtils.forPublicPrivateChat(message::apply).subscribe(); - } - - /** - * Send a message to the chat channels, private chats and custom chats. - * - * @param message The message to send, duh - * @param toggle The toggle type for channelcon - */ - public void sendMessageCustomAsWell(Function, Mono> message, @Nullable ChannelconBroadcast toggle) { - MCChatUtils.forCustomAndAllMCChat(message::apply, toggle, false).subscribe(); - } - - public void updatePlayerList() { - MCChatUtils.updatePlayerList(); - } -} 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/DiscordConnectedPlayer.java b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java deleted file mode 100644 index fab1c02..0000000 --- a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java +++ /dev/null @@ -1,325 +0,0 @@ -package buttondevteam.discordplugin; - -import buttondevteam.discordplugin.mcchat.MinecraftChatModule; -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/DiscordPlayer.java b/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java deleted file mode 100755 index b6b8d99..0000000 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java +++ /dev/null @@ -1,30 +0,0 @@ -package buttondevteam.discordplugin; - -import buttondevteam.discordplugin.mcchat.MCChatPrivate; -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/DiscordPlayerSender.java b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java deleted file mode 100755 index ac3ae1c..0000000 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java +++ /dev/null @@ -1,43 +0,0 @@ -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 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/DiscordPlugin.java b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java deleted file mode 100755 index 74022e6..0000000 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java +++ /dev/null @@ -1,292 +0,0 @@ -package buttondevteam.discordplugin; - -import buttondevteam.discordplugin.announcer.AnnouncerModule; -import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule; -import buttondevteam.discordplugin.commands.*; -import buttondevteam.discordplugin.exceptions.ExceptionListenerModule; -import buttondevteam.discordplugin.fun.FunModule; -import buttondevteam.discordplugin.listeners.CommonListeners; -import buttondevteam.discordplugin.listeners.MCListener; -import buttondevteam.discordplugin.mcchat.MinecraftChatModule; -import buttondevteam.discordplugin.mccommands.DiscordMCCommand; -import buttondevteam.discordplugin.role.GameRoleModule; -import buttondevteam.discordplugin.util.DPState; -import buttondevteam.discordplugin.util.Timings; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.architecture.ButtonPlugin; -import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.architecture.ConfigData; -import buttondevteam.lib.architecture.IHaveConfig; -import buttondevteam.lib.player.ChromaGamerBase; -import com.google.common.io.Files; -import discord4j.common.util.Snowflake; -import discord4j.core.DiscordClientBuilder; -import discord4j.core.GatewayDiscordClient; -import discord4j.core.event.domain.guild.GuildCreateEvent; -import discord4j.core.event.domain.lifecycle.ReadyEvent; -import discord4j.core.object.entity.Guild; -import discord4j.core.object.entity.Role; -import discord4j.core.object.presence.Activity; -import discord4j.core.object.presence.Presence; -import discord4j.core.object.reaction.ReactionEmoji; -import discord4j.store.jdk.JdkStoreService; -import lombok.Getter; -import lombok.val; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.core.Logger; -import org.bukkit.configuration.file.YamlConfiguration; -import org.mockito.internal.util.MockUtil; -import reactor.core.publisher.Mono; - -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Optional; - -@ButtonPlugin.ConfigOpts(disableConfigGen = true) -public class DiscordPlugin extends ButtonPlugin { - public static GatewayDiscordClient dc; - public static DiscordPlugin plugin; - public static boolean SafeMode = true; - @Getter - private Command2DC manager; - private boolean starting; - private BukkitLogWatcher logWatcher; - - /** - * The prefix to use with Discord commands like /role. It only works in the bot channel. - */ - private final ConfigData prefix = getIConfig().getData("prefix", '/', str -> ((String) str).charAt(0), Object::toString); - - public static char getPrefix() { - if (plugin == null) return '/'; - return plugin.prefix.get(); - } - - /** - * The main server where the roles and other information is pulled from. It's automatically set to the first server the bot's invited to. - */ - private ConfigData> mainServer() { - return getIConfig().getDataPrimDef("mainServer", 0L, - id -> { - //It attempts to get the default as well - if ((long) id == 0L) - return Optional.empty(); //Hack? - return dc.getGuildById(Snowflake.of((long) id)) - .onErrorResume(t -> Mono.fromRunnable(() -> getLogger().warning("Failed to get guild: " + t.getMessage()))).blockOptional(); - }, - g -> g.map(gg -> gg.getId().asLong()).orElse(0L)); - } - - /** - * The (bot) channel to use for Discord commands like /role. - */ - public ConfigData commandChannel = DPUtils.snowflakeData(getIConfig(), "commandChannel", 0L); - - /** - * The role that allows using mod-only Discord commands. - * If empty (''), then it will only allow for the owner. - */ - public ConfigData> modRole; - - /** - * The invite link to show by /discord invite. If empty, it defaults to the first invite if the bot has access. - */ - public ConfigData inviteLink = getIConfig().getData("inviteLink", ""); - - private void setupConfig() { - modRole = DPUtils.roleData(getIConfig(), "modRole", "Moderator"); - } - - @Override - public void onLoad() { //Needed by ServerWatcher - var thread = Thread.currentThread(); - var cl = thread.getContextClassLoader(); - thread.setContextClassLoader(getClassLoader()); - MockUtil.isMock(null); //Load MockUtil to load Mockito plugins - thread.setContextClassLoader(cl); - getLogger().info("Load complete"); - } - - @Override - public void pluginEnable() { - try { - getLogger().info("Initializing..."); - plugin = this; - manager = new Command2DC(); - registerCommand(new DiscordMCCommand()); //Register so that the restart command works - String token; - File tokenFile = new File("TBMC", "Token.txt"); - if (tokenFile.exists()) //Legacy support - //noinspection UnstableApiUsage - token = Files.readFirstLine(tokenFile, StandardCharsets.UTF_8); - else { - File privateFile = new File(getDataFolder(), "private.yml"); - val conf = YamlConfiguration.loadConfiguration(privateFile); - token = conf.getString("token"); - if (token == null || token.equalsIgnoreCase("Token goes here")) { - conf.set("token", "Token goes here"); - conf.save(privateFile); - - getLogger().severe("Token not found! Please set it in private.yml then do /discord restart"); - getLogger().severe("You need to have a bot account to use with your server."); - getLogger().severe("If you don't have one, go to https://discordapp.com/developers/applications/ and create an application, then create a bot for it and copy the bot token."); - return; - } - } - starting = true; - //System.out.println("This line should show up for sure"); - val cb = DiscordClientBuilder.create(token).build().gateway(); - //System.out.println("Got gateway bootstrap"); - cb.setInitialStatus(si -> Presence.doNotDisturb(Activity.playing("booting"))); - cb.setStoreService(new JdkStoreService()); //The default doesn't work for some reason - it's waaay faster now - //System.out.println("Initial status and store service set"); - cb.login().doOnError(t -> { - stopStarting(); - //System.out.println("Got this error: " + t); t.printStackTrace(); - }).subscribe(dc -> { - //System.out.println("Login successful, got dc: " + dc); - DiscordPlugin.dc = dc; //Set to gateway client - dc.on(ReadyEvent.class) // Listen for ReadyEvent(s) - .map(event -> event.getGuilds().size()) // Get how many guilds the bot is in - .flatMap(size -> dc - .on(GuildCreateEvent.class) // Listen for GuildCreateEvent(s) - .take(size) // Take only the first `size` GuildCreateEvent(s) to be received - .doOnError(t -> { - stopStarting(); - //System.out.println("Got error: " + t); - t.printStackTrace(); - }) - .collectList()).doOnError(t -> stopStarting()).subscribe(this::handleReady); // Take all received GuildCreateEvents and make it a List - }); /* All guilds have been received, client is fully connected */ - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to enable the Discord plugin!", e, this); - getLogger().severe("You may be able to restart the plugin using /discord restart"); - stopStarting(); - } - } - - private void stopStarting() { - synchronized (this) { - starting = false; - notifyAll(); - } - } - - public static Guild mainServer; - - private void handleReady(List event) { - //System.out.println("Got ready event"); - try { - if (mainServer != null) { //This is not the first ready event - getLogger().info("Ready event already handled"); //TODO: It should probably handle disconnections - dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe(); //Update from the initial presence - return; - } - mainServer = mainServer().get().orElse(null); //Shouldn't change afterwards - if (mainServer == null) { - if (event.size() == 0) { - getLogger().severe("Main server not found! Invite the bot and do /discord restart"); - dc.getApplicationInfo().subscribe(info -> - getLogger().severe("Click here: https://discordapp.com/oauth2/authorize?client_id=" + info.getId().asString() + "&scope=bot&permissions=268509264")); - saveConfig(); //Put default there - return; //We should have all guilds by now, no need to retry - } - mainServer = event.get(0).getGuild(); - getLogger().warning("Main server set to first one: " + mainServer.getName()); - mainServer().set(Optional.of(mainServer)); //Save in config - } - SafeMode = false; - setupConfig(); - DPUtils.disableIfConfigErrorRes(null, commandChannel, DPUtils.getMessageChannel(commandChannel)); - //Won't disable, just prints the warning here - - if (MinecraftChatModule.state == DPState.STOPPING_SERVER) { - stopStarting(); - return; //Reusing that field to check if stopping while still initializing - } - CommonListeners.register(dc.getEventDispatcher()); - TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this); - TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class, DiscordPlayer::new); - ChromaGamerBase.addConverter(sender -> Optional.ofNullable(sender instanceof DiscordSenderBase - ? ((DiscordSenderBase) sender).getChromaUser() : null)); - - IHaveConfig.pregenConfig(this, null); - - var cb = new ChromaBot(); //Initialize ChromaBot - Component.registerComponent(this, new GeneralEventBroadcasterModule()); - Component.registerComponent(this, new MinecraftChatModule()); - Component.registerComponent(this, new ExceptionListenerModule()); - Component.registerComponent(this, new GameRoleModule()); //Needs the mainServer to be set - Component.registerComponent(this, new AnnouncerModule()); - Component.registerComponent(this, new FunModule()); - cb.updatePlayerList(); //The MCChatModule is tested to be enabled - - getManager().registerCommand(new VersionCommand()); - getManager().registerCommand(new UserinfoCommand()); - getManager().registerCommand(new HelpCommand()); - getManager().registerCommand(new DebugCommand()); - getManager().registerCommand(new ConnectCommand()); - - TBMCCoreAPI.SendUnsentExceptions(); - TBMCCoreAPI.SendUnsentDebugMessages(); - - var blw = new BukkitLogWatcher(); - blw.start(); - ((Logger) LogManager.getRootLogger()).addAppender(blw); - logWatcher = blw; - - if (!TBMCCoreAPI.IsTestServer()) { - dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe(); - } else { - dc.updatePresence(Presence.online(Activity.playing("testing"))).subscribe(); - } - getLogger().info("Loaded!"); - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occurred while enabling DiscordPlugin!", e, this); - } - stopStarting(); - } - - @Override - public void pluginPreDisable() { - if (MinecraftChatModule.state == DPState.RUNNING) - MinecraftChatModule.state = DPState.STOPPING_SERVER; - synchronized (this) { - if (starting) { - try { - wait(10000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - if (ChromaBot.getInstance() == null) return; //Failed to load - Timings timings = new Timings(); - timings.printElapsed("Disable start"); - timings.printElapsed("Updating player list"); - ChromaBot.getInstance().updatePlayerList(); - timings.printElapsed("Done"); - } - - @Override - public void pluginDisable() { - Timings timings = new Timings(); - timings.printElapsed("Actual disable start (logout)"); - if (ChromaBot.getInstance() == null) return; //Failed to load - - try { - SafeMode = true; // Stop interacting with Discord - ChromaBot.delete(); - ((Logger) LogManager.getRootLogger()).removeAppender(logWatcher); - timings.printElapsed("Logging out..."); - dc.logout().block(); - mainServer = null; //Allow ReadyEvent again - //Configs are emptied so channels and servers are fetched again - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e, this); - } - } - - public static final ReactionEmoji DELIVERED_REACTION = ReactionEmoji.unicode("✅"); -} 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/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/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/announcer/AnnouncerModule.java b/src/main/java/buttondevteam/discordplugin/announcer/AnnouncerModule.java deleted file mode 100644 index e27a30f..0000000 --- a/src/main/java/buttondevteam/discordplugin/announcer/AnnouncerModule.java +++ /dev/null @@ -1,135 +0,0 @@ -package buttondevteam.discordplugin.announcer; - -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.architecture.ComponentMetadata; -import buttondevteam.lib.architecture.ConfigData; -import buttondevteam.lib.architecture.ReadOnlyConfigData; -import buttondevteam.lib.player.ChromaGamerBase; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.channel.MessageChannel; -import lombok.val; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** - * Posts new posts from Reddit to the specified channel(s). It will pin the regular posts (not the mod posts). - */ -@ComponentMetadata(enabledByDefault = false) -public class AnnouncerModule extends Component { - /** - * Channel to post new posts. - */ - public final ReadOnlyConfigData> channel = DPUtils.channelData(getConfig(), "channel"); - - /** - * Channel where distinguished (moderator) posts go. - */ - private final ReadOnlyConfigData> modChannel = DPUtils.channelData(getConfig(), "modChannel"); - - /** - * Automatically unpins all messages except the last few. Set to 0 or >50 to disable - */ - private final ConfigData keepPinned = getConfig().getData("keepPinned", (short) 40); - - private final ConfigData lastAnnouncementTime = getConfig().getData("lastAnnouncementTime", 0L); - - private final ConfigData lastSeenTime = getConfig().getData("lastSeenTime", 0L); - - /** - * The subreddit to pull the posts from - */ - private final ConfigData subredditURL = getConfig().getData("subredditURL", "https://www.reddit.com/r/ChromaGamers"); - - private static boolean stop = false; - - @Override - protected void enable() { - if (DPUtils.disableIfConfigError(this, channel, modChannel)) return; - stop = false; //If not the first time - val kp = keepPinned.get(); - if (kp == 0) return; - Flux msgs = channel.get().flatMapMany(MessageChannel::getPinnedMessages).takeLast(kp); - msgs.subscribe(Message::unpin); - new Thread(this::AnnouncementGetterThreadMethod).start(); - } - - @Override - protected void disable() { - stop = true; - } - - private void AnnouncementGetterThreadMethod() { - while (!stop) { - try { - if (!isEnabled()) { - //noinspection BusyWait - Thread.sleep(10000); - continue; - } - String body = TBMCCoreAPI.DownloadString(subredditURL.get() + "/new/.json?limit=10"); - JsonArray json = new JsonParser().parse(body).getAsJsonObject().get("data").getAsJsonObject() - .get("children").getAsJsonArray(); - StringBuilder msgsb = new StringBuilder(); - StringBuilder modmsgsb = new StringBuilder(); - long lastanntime = lastAnnouncementTime.get(); - for (int i = json.size() - 1; i >= 0; i--) { - JsonObject item = json.get(i).getAsJsonObject(); - final JsonObject data = item.get("data").getAsJsonObject(); - String author = data.get("author").getAsString(); - JsonElement distinguishedjson = data.get("distinguished"); - String distinguished; - if (distinguishedjson.isJsonNull()) - distinguished = null; - else - distinguished = distinguishedjson.getAsString(); - String permalink = "https://www.reddit.com" + data.get("permalink").getAsString(); - long date = data.get("created_utc").getAsLong(); - if (date > lastSeenTime.get()) - lastSeenTime.set(date); - else if (date > lastAnnouncementTime.get()) { - //noinspection ConstantConditions - do { - val reddituserclass = ChromaGamerBase.getTypeForFolder("reddit"); - if (reddituserclass == null) - break; - val user = ChromaGamerBase.getUser(author, reddituserclass); - String id = user.getConnectedID(DiscordPlayer.class); - if (id != null) - author = "<@" + id + ">"; - } while (false); - if (!author.startsWith("<")) - author = "/u/" + author; - (distinguished != null && distinguished.equals("moderator") ? modmsgsb : msgsb) - .append("A new post was submitted to the subreddit by ").append(author).append("\n") - .append(permalink).append("\n"); - lastanntime = date; - } - } - if (msgsb.length() > 0) - channel.get().flatMap(ch -> ch.createMessage(msgsb.toString())) - .flatMap(Message::pin).subscribe(); - if (modmsgsb.length() > 0) - modChannel.get().flatMap(ch -> ch.createMessage(modmsgsb.toString())) - .flatMap(Message::pin).subscribe(); - if (lastAnnouncementTime.get() != lastanntime) - lastAnnouncementTime.set(lastanntime); // If sending succeeded - } catch (Exception e) { - e.printStackTrace(); - } - try { - //noinspection BusyWait - Thread.sleep(10000); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } - } -} diff --git a/src/main/java/buttondevteam/discordplugin/broadcaster/GeneralEventBroadcasterModule.java b/src/main/java/buttondevteam/discordplugin/broadcaster/GeneralEventBroadcasterModule.java deleted file mode 100644 index a6ef5e2..0000000 --- a/src/main/java/buttondevteam/discordplugin/broadcaster/GeneralEventBroadcasterModule.java +++ /dev/null @@ -1,45 +0,0 @@ -package buttondevteam.discordplugin.broadcaster; - -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.architecture.ComponentMetadata; -import lombok.Getter; - -/** - * Uses a bit of a hacky method of getting all broadcasted messages, including advancements and any other message that's for everyone. - * If this component is enabled then these messages will show up on Discord. - */ -@ComponentMetadata(enabledByDefault = false) -public class GeneralEventBroadcasterModule extends Component { - private static @Getter boolean hooked = false; - - @Override - protected void enable() { - try { - PlayerListWatcher.hookUpDown(true, this); - log("Finished hooking into the player list"); - hooked = true; - } catch (Exception e) { - TBMCCoreAPI.SendException("Error while hacking the player list! Disable this module if you're on an incompatible version.", e, this); - } catch (NoClassDefFoundError e) { - logWarn("Error while hacking the player list! Disable this module if you're on an incompatible version."); - } - - } - - @Override - protected void disable() { - try { - if (!hooked) return; - if (PlayerListWatcher.hookUpDown(false, this)) - log("Finished unhooking the player list!"); - else - log("Didn't have the player list hooked."); - hooked = false; - } catch (Exception e) { - TBMCCoreAPI.SendException("Error while hacking the player list!", e, this); - } catch (NoClassDefFoundError ignored) { - } - } -} diff --git a/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java b/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java deleted file mode 100755 index 5fd86d7..0000000 --- a/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java +++ /dev/null @@ -1,176 +0,0 @@ -package buttondevteam.discordplugin.broadcaster; - -import buttondevteam.discordplugin.mcchat.MCChatUtils; -import buttondevteam.discordplugin.playerfaker.DelegatingMockMaker; -import buttondevteam.lib.TBMCCoreAPI; -import lombok.val; -import org.bukkit.Bukkit; -import org.mockito.Mockito; -import org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.UUID; - -public class PlayerListWatcher { - private static Object plist; - private static Object mock; - private static MethodHandle fHandle; //Handle for PlayerList.f(EntityPlayer) - Only needed for 1.16 - - static boolean hookUpDown(boolean up, GeneralEventBroadcasterModule module) throws Exception { - val csc = Bukkit.getServer().getClass(); - Field conf = csc.getDeclaredField("console"); - conf.setAccessible(true); - val server = conf.get(Bukkit.getServer()); - val nms = server.getClass().getPackage().getName(); - val dplc = Class.forName(nms + ".DedicatedPlayerList"); - val currentPL = server.getClass().getMethod("getPlayerList").invoke(server); - if (up) { - if (currentPL == mock) { - module.logWarn("Player list already mocked!"); - return false; - } - DelegatingMockMaker.getInstance().setMockMaker(new SubclassByteBuddyMockMaker()); - val icbcl = Class.forName(nms + ".IChatBaseComponent"); - Method sendMessageTemp; - try { - sendMessageTemp = server.getClass().getMethod("sendMessage", icbcl, UUID.class); - } catch (NoSuchMethodException e) { - sendMessageTemp = server.getClass().getMethod("sendMessage", icbcl); - } - val sendMessage = sendMessageTemp; - val cmtcl = Class.forName(nms + ".ChatMessageType"); - val systemType = cmtcl.getDeclaredField("SYSTEM").get(null); - val chatType = cmtcl.getDeclaredField("CHAT").get(null); - - val obc = csc.getPackage().getName(); - val ccmcl = Class.forName(obc + ".util.CraftChatMessage"); - val fixComponent = ccmcl.getMethod("fixComponent", icbcl); - val ppoc = Class.forName(nms + ".PacketPlayOutChat"); - Constructor ppocCTemp; - try { - ppocCTemp = ppoc.getConstructor(icbcl, cmtcl, UUID.class); - } catch (Exception e) { - ppocCTemp = ppoc.getConstructor(icbcl, cmtcl); - } - val ppocC = ppocCTemp; - val sendAll = dplc.getMethod("sendAll", Class.forName(nms + ".Packet")); - Method tpt; - try { - tpt = icbcl.getMethod("toPlainText"); - } catch (NoSuchMethodException e) { - tpt = icbcl.getMethod("getString"); - } - val toPlainText = tpt; - val sysb = Class.forName(nms + ".SystemUtils").getField("b"); - - //Find the original method without overrides - Constructor lookupConstructor; - if (nms.contains("1_16")) { - lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class); - lookupConstructor.setAccessible(true); //Create lookup with a given class instead of caller - } else lookupConstructor = null; - mock = Mockito.mock(dplc, Mockito.withSettings().defaultAnswer(new Answer<>() { // Cannot call super constructor - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - final Method method = invocation.getMethod(); - if (!method.getName().equals("sendMessage")) { - if (method.getName().equals("sendAll")) { - sendAll(invocation.getArgument(0)); - return null; - } - //In 1.16 it passes a reference to the player list to advancement data for each player - if (nms.contains("1_16") && method.getName().equals("f") && method.getParameterCount() > 0 && method.getParameterTypes()[0].getSimpleName().equals("EntityPlayer")) { - method.setAccessible(true); - if (fHandle == null) { - assert lookupConstructor != null; - var lookup = lookupConstructor.newInstance(mock.getClass()); - fHandle = lookup.unreflectSpecial(method, mock.getClass()); //Special: super.method() - } - return fHandle.invoke(mock, invocation.getArgument(0)); //Invoke with our instance, so it passes that to advancement data, we have the fields as well - } - return method.invoke(plist, invocation.getArguments()); - } - val args = invocation.getArguments(); - val params = method.getParameterTypes(); - if (params.length == 0) { - TBMCCoreAPI.SendException("Found a strange method", - new Exception("Found a sendMessage() method without arguments."), module); - return null; - } - if (params[0].getSimpleName().equals("IChatBaseComponent[]")) - for (val arg : (Object[]) args[0]) - sendMessage(arg, true); - else if (params[0].getSimpleName().equals("IChatBaseComponent")) - if (params.length > 1 && params[1].getSimpleName().equalsIgnoreCase("boolean")) - sendMessage(args[0], (Boolean) args[1]); - else - sendMessage(args[0], true); - else - TBMCCoreAPI.SendException("Found a method with interesting params", - new Exception("Found a sendMessage(" + params[0].getSimpleName() + ") method"), module); - return null; - } - - private void sendMessage(Object chatComponent, boolean system) { - try { //Converted to use reflection - if (sendMessage.getParameterCount() == 2) - sendMessage.invoke(server, chatComponent, sysb.get(null)); - else - sendMessage.invoke(server, chatComponent); - Object chatmessagetype = system ? systemType : chatType; - - // CraftBukkit start - we run this through our processor first so we can get web links etc - var comp = fixComponent.invoke(null, chatComponent); - var packet = ppocC.getParameterCount() == 3 - ? ppocC.newInstance(comp, chatmessagetype, sysb.get(null)) - : ppocC.newInstance(comp, chatmessagetype); - this.sendAll(packet); - // CraftBukkit end - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occurred while passing a vanilla message through the player list", e, module); - } - } - - private void sendAll(Object packet) { - try { // Some messages get sent by directly constructing a packet - sendAll.invoke(plist, packet); - if (packet.getClass() == ppoc) { - Field msgf = ppoc.getDeclaredField("a"); - msgf.setAccessible(true); - MCChatUtils.forPublicPrivateChat(MCChatUtils.send((String) toPlainText.invoke(msgf.get(packet)))).subscribe(); - } - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to broadcast message sent to all players - hacking failed.", e, module); - } - } - }).stubOnly()); - plist = currentPL; - for (var plc = dplc; plc != null; plc = plc.getSuperclass()) { //Set all fields - for (var f : plc.getDeclaredFields()) { - f.setAccessible(true); - Field modf = f.getClass().getDeclaredField("modifiers"); - modf.setAccessible(true); - modf.set(f, f.getModifiers() & ~Modifier.FINAL); - f.set(mock, f.get(plist)); - } - } - } - try { - server.getClass().getMethod("a", dplc).invoke(server, up ? mock : plist); - } catch (NoSuchMethodException e) { - server.getClass().getMethod("a", Class.forName(server.getClass().getPackage().getName() + ".PlayerList")) - .invoke(server, up ? mock : plist); - } - Field pllf = csc.getDeclaredField("playerList"); - pllf.setAccessible(true); - pllf.set(Bukkit.getServer(), up ? mock : plist); - return true; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/commands/Command2DC.java b/src/main/java/buttondevteam/discordplugin/commands/Command2DC.java deleted file mode 100644 index ab56eb8..0000000 --- a/src/main/java/buttondevteam/discordplugin/commands/Command2DC.java +++ /dev/null @@ -1,19 +0,0 @@ -package buttondevteam.discordplugin.commands; - -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.chat.Command2; - -import java.lang.reflect.Method; - -public class Command2DC extends Command2 { - @Override - public void registerCommand(ICommand2DC command) { - super.registerCommand(command, DiscordPlugin.getPrefix()); //Needs to be configurable for the helps - } - - @Override - public boolean hasPermission(Command2DCSender sender, ICommand2DC command, Method method) { - //return !command.isModOnly() || sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.modRole().get()); //TODO: modRole may be null; more customisable way? - return true; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/commands/Command2DCSender.java b/src/main/java/buttondevteam/discordplugin/commands/Command2DCSender.java deleted file mode 100644 index ab22e37..0000000 --- a/src/main/java/buttondevteam/discordplugin/commands/Command2DCSender.java +++ /dev/null @@ -1,39 +0,0 @@ -package buttondevteam.discordplugin.commands; - -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.lib.chat.Command2Sender; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.User; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.val; - -@RequiredArgsConstructor -public class Command2DCSender implements Command2Sender { - private final @Getter - Message message; - - @Override - public void sendMessage(String message) { - if (message.length() == 0) return; - message = DPUtils.sanitizeString(message); - message = Character.toLowerCase(message.charAt(0)) + message.substring(1); - val msg = message; - /*this.message.getAuthorAsMember().flatMap(author -> - this.message.getChannel().flatMap(ch -> - ch.createMessage(author.getNicknameMention() + ", " + msg))).subscribe();*/ - this.message.getChannel().flatMap(ch -> - ch.createMessage(this.message.getAuthor().map(u -> DPUtils.nickMention(u.getId()) + ", ").orElse("") - + msg)).subscribe(); - } - - @Override - public void sendMessage(String[] message) { - sendMessage(String.join("\n", message)); - } - - @Override - public String getName() { - return message.getAuthor().map(User::getUsername).orElse("Discord"); - } -} diff --git a/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java b/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java deleted file mode 100755 index f0d7fe9..0000000 --- a/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java +++ /dev/null @@ -1,59 +0,0 @@ -package buttondevteam.discordplugin.commands; - -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -import buttondevteam.lib.player.TBMCPlayer; -import buttondevteam.lib.player.TBMCPlayerBase; -import com.google.common.collect.HashBiMap; -import lombok.val; -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.entity.Player; - -@CommandClass(helpText = { - "Connect command", // - "This command lets you connect your account with a Minecraft account. This allows using the private Minecraft chat and other things.", // -}) -public class ConnectCommand extends ICommand2DC { - - /** - * Key: Minecraft name
- * Value: Discord ID - */ - public static HashBiMap WaitingToConnect = HashBiMap.create(); - - @Command2.Subcommand - public boolean def(Command2DCSender sender, String Minecraftname) { - val message = sender.getMessage(); - val channel = message.getChannel().block(); - val author = message.getAuthor().orElse(null); - if (author == null || channel == null) return true; - if (WaitingToConnect.inverse().containsKey(author.getId().asString())) { - channel.createMessage( - "Replacing " + WaitingToConnect.inverse().get(author.getId().asString()) + " with " + Minecraftname).subscribe(); - WaitingToConnect.inverse().remove(author.getId().asString()); - } - @SuppressWarnings("deprecation") - OfflinePlayer p = Bukkit.getOfflinePlayer(Minecraftname); - if (p == null) { - channel.createMessage("The specified Minecraft player cannot be found").subscribe(); - return true; - } - TBMCPlayer pl = TBMCPlayerBase.getPlayer(p.getUniqueId(), TBMCPlayer.class); - DiscordPlayer dp = pl.getAs(DiscordPlayer.class); - if (dp != null && author.getId().asString().equals(dp.getDiscordID())) { - channel.createMessage("You already have this account connected.").subscribe(); - return true; - } - WaitingToConnect.put(p.getName(), author.getId().asString()); - channel.createMessage( - "Alright! Now accept the connection in Minecraft from the account " + Minecraftname - + " before the next server restart. You can also adjust the Minecraft name you want to connect to by running this command again.").subscribe(); - if (p.isOnline()) - ((Player) p).sendMessage("§bTo connect with the Discord account " + author.getUsername() + "#" - + author.getDiscriminator() + " do /discord accept"); - return true; - } - -} diff --git a/src/main/java/buttondevteam/discordplugin/commands/DebugCommand.java b/src/main/java/buttondevteam/discordplugin/commands/DebugCommand.java deleted file mode 100644 index 951cc71..0000000 --- a/src/main/java/buttondevteam/discordplugin/commands/DebugCommand.java +++ /dev/null @@ -1,30 +0,0 @@ -package buttondevteam.discordplugin.commands; - -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.listeners.CommonListeners; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -import reactor.core.publisher.Mono; - -@CommandClass(helpText = { - "Switches debug mode." -}) -public class DebugCommand extends ICommand2DC { - @Command2.Subcommand - public boolean def(Command2DCSender sender) { - sender.getMessage().getAuthorAsMember() - .switchIfEmpty(sender.getMessage().getAuthor() //Support DMs - .map(u -> u.asMember(DiscordPlugin.mainServer.getId())) - .orElse(Mono.empty())) - .flatMap(m -> DiscordPlugin.plugin.modRole.get() - .map(mr -> m.getRoleIds().stream().anyMatch(r -> r.equals(mr.getId()))) - .switchIfEmpty(Mono.fromSupplier(() -> DiscordPlugin.mainServer.getOwnerId().asLong() == m.getId().asLong()))) //Role not found - .onErrorReturn(false).subscribe(success -> { - if (success) - sender.sendMessage("debug " + (CommonListeners.debug() ? "enabled" : "disabled")); - else - sender.sendMessage("you need to be a moderator to use this command."); - }); - return true; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/commands/HelpCommand.java b/src/main/java/buttondevteam/discordplugin/commands/HelpCommand.java deleted file mode 100755 index 546d4ee..0000000 --- a/src/main/java/buttondevteam/discordplugin/commands/HelpCommand.java +++ /dev/null @@ -1,24 +0,0 @@ -package buttondevteam.discordplugin.commands; - -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; - -@CommandClass(helpText = { - "Help command", // - "Shows some info about a command or lists the available commands.", // -}) -public class HelpCommand extends ICommand2DC { - @Command2.Subcommand - public boolean def(Command2DCSender sender, @Command2.TextArg @Command2.OptionalArg String args) { - if (args == null || args.length() == 0) - sender.sendMessage(getManager().getCommandsText()); - else { - String[] ht = getManager().getHelpText(args); - if (ht == null) - sender.sendMessage("Command not found: " + args); - else - sender.sendMessage(ht); - } - return true; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/commands/ICommand2DC.java b/src/main/java/buttondevteam/discordplugin/commands/ICommand2DC.java deleted file mode 100644 index 6aae802..0000000 --- a/src/main/java/buttondevteam/discordplugin/commands/ICommand2DC.java +++ /dev/null @@ -1,20 +0,0 @@ -package buttondevteam.discordplugin.commands; - -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.chat.CommandClass; -import buttondevteam.lib.chat.ICommand2; -import lombok.Getter; -import lombok.val; - -public abstract class ICommand2DC extends ICommand2 { - public ICommand2DC() { - super(DiscordPlugin.plugin.getManager()); - val ann = getClass().getAnnotation(CommandClass.class); - if (ann == null) - modOnly = false; - else - modOnly = ann.modOnly(); - } - - private final @Getter boolean modOnly; -} diff --git a/src/main/java/buttondevteam/discordplugin/commands/UserinfoCommand.java b/src/main/java/buttondevteam/discordplugin/commands/UserinfoCommand.java deleted file mode 100755 index 46a06ce..0000000 --- a/src/main/java/buttondevteam/discordplugin/commands/UserinfoCommand.java +++ /dev/null @@ -1,88 +0,0 @@ -package buttondevteam.discordplugin.commands; - -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -import buttondevteam.lib.player.ChromaGamerBase; -import buttondevteam.lib.player.ChromaGamerBase.InfoTarget; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.User; -import lombok.val; - -import java.util.List; - -@CommandClass(helpText = { - "User information", // - "Shows some information about users, from Discord, from Minecraft or from Reddit if they have these accounts connected.", // - "If used without args, shows your info.", // -}) -public class UserinfoCommand extends ICommand2DC { - @Command2.Subcommand - public boolean def(Command2DCSender sender, @Command2.OptionalArg @Command2.TextArg String user) { - val message = sender.getMessage(); - User target = null; - val channel = message.getChannel().block(); - assert channel != null; - if (user == null || user.length() == 0) - target = message.getAuthor().orElse(null); - else { - final User firstmention = message.getUserMentions() - .filter(m -> !m.getId().asString().equals(DiscordPlugin.dc.getSelfId().asString())).blockFirst(); - if (firstmention != null) - target = firstmention; - else if (user.contains("#")) { - String[] targettag = user.split("#"); - final List targets = getUsers(message, targettag[0]); - if (targets.size() == 0) { - channel.createMessage("The user cannot be found (by name): " + user).subscribe(); - return true; - } - for (User ptarget : targets) { - if (ptarget.getDiscriminator().equalsIgnoreCase(targettag[1])) { - target = ptarget; - break; - } - } - if (target == null) { - channel.createMessage("The user cannot be found (by discriminator): " + user + "(Found " + targets.size() - + " users with the name.)").subscribe(); - return true; - } - } else { - final List targets = getUsers(message, user); - if (targets.size() == 0) { - channel.createMessage("The user cannot be found on Discord: " + user).subscribe(); - return true; - } - if (targets.size() > 1) { - channel.createMessage("Multiple users found with that (nick)name. Please specify the whole tag, like ChromaBot#6338 or use a ping.").subscribe(); - return true; - } - target = targets.get(0); - } - } - if (target == null) { - sender.sendMessage("An error occurred."); - return true; - } - DiscordPlayer dp = ChromaGamerBase.getUser(target.getId().asString(), DiscordPlayer.class); - StringBuilder uinfo = new StringBuilder("User info for ").append(target.getUsername()).append(":\n"); - uinfo.append(dp.getInfo(InfoTarget.Discord)); - channel.createMessage(uinfo.toString()).subscribe(); - return true; - } - - private List getUsers(Message message, String args) { - final List targets; - val guild = message.getGuild().block(); - if (guild == null) //Private channel - targets = DiscordPlugin.dc.getUsers().filter(u -> u.getUsername().equalsIgnoreCase(args)) - .collectList().block(); - else - targets = guild.getMembers().filter(m -> m.getUsername().equalsIgnoreCase(args)) - .map(m -> (User) m).collectList().block(); - return targets; - } - -} diff --git a/src/main/java/buttondevteam/discordplugin/commands/VersionCommand.java b/src/main/java/buttondevteam/discordplugin/commands/VersionCommand.java deleted file mode 100644 index ac83243..0000000 --- a/src/main/java/buttondevteam/discordplugin/commands/VersionCommand.java +++ /dev/null @@ -1,26 +0,0 @@ -package buttondevteam.discordplugin.commands; - -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -import lombok.val; - -@CommandClass(helpText = { - "Version", - "Returns the plugin's version" -}) -public class VersionCommand extends ICommand2DC { - @Command2.Subcommand - public boolean def(Command2DCSender sender) { - sender.sendMessage(getVersion()); - return true; - } - - public static String[] getVersion() { - val desc = DiscordPlugin.plugin.getDescription(); - return new String[]{ // - desc.getFullName(), // - desc.getWebsite() // - }; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/exceptions/DebugMessageListener.java b/src/main/java/buttondevteam/discordplugin/exceptions/DebugMessageListener.java deleted file mode 100755 index f90f0f3..0000000 --- a/src/main/java/buttondevteam/discordplugin/exceptions/DebugMessageListener.java +++ /dev/null @@ -1,36 +0,0 @@ -package buttondevteam.discordplugin.exceptions; - -import buttondevteam.core.ComponentManager; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.TBMCDebugMessageEvent; -import discord4j.core.object.entity.channel.MessageChannel; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import reactor.core.publisher.Mono; - -public class DebugMessageListener implements Listener { - @EventHandler - public void onDebugMessage(TBMCDebugMessageEvent e) { - SendMessage(e.getDebugMessage()); - e.setSent(); - } - - private static void SendMessage(String message) { - if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(ExceptionListenerModule.class)) - return; - try { - Mono mc = ExceptionListenerModule.getChannel(); - if (mc == null) return; - StringBuilder sb = new StringBuilder(); - sb.append("```").append("\n"); - if (message.length() > 2000) - message = message.substring(0, 2000); - sb.append(message).append("\n"); - sb.append("```"); - mc.flatMap(ch -> ch.createMessage(sb.toString())).subscribe(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - -} diff --git a/src/main/java/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.java b/src/main/java/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.java deleted file mode 100755 index 16d1e93..0000000 --- a/src/main/java/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.java +++ /dev/null @@ -1,114 +0,0 @@ -package buttondevteam.discordplugin.exceptions; - -import buttondevteam.core.ComponentManager; -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.TBMCExceptionEvent; -import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.architecture.ConfigData; -import buttondevteam.lib.architecture.ReadOnlyConfigData; -import discord4j.core.object.entity.Guild; -import discord4j.core.object.entity.Role; -import discord4j.core.object.entity.channel.GuildChannel; -import discord4j.core.object.entity.channel.MessageChannel; -import org.apache.commons.lang.exception.ExceptionUtils; -import org.bukkit.Bukkit; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import reactor.core.publisher.Mono; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Listens for errors from the Chroma plugins and posts them to Discord, ignoring repeating errors so it's not that spammy. - */ -public class ExceptionListenerModule extends Component implements Listener { - private final List lastthrown = new ArrayList<>(); - private final List lastsourcemsg = new ArrayList<>(); - - @EventHandler - public void onException(TBMCExceptionEvent e) { - if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(getClass())) - return; - if (lastthrown.stream() - .anyMatch(ex -> Arrays.equals(e.getException().getStackTrace(), ex.getStackTrace()) - && (e.getException().getMessage() == null ? ex.getMessage() == null - : e.getException().getMessage().equals(ex.getMessage()))) // e.Exception.Message==ex.Message - && lastsourcemsg.contains(e.getSourceMessage())) - return; - SendException(e.getException(), e.getSourceMessage()); - if (lastthrown.size() >= 10) - lastthrown.remove(0); - if (lastsourcemsg.size() >= 10) - lastsourcemsg.remove(0); - lastthrown.add(e.getException()); - lastsourcemsg.add(e.getSourceMessage()); - e.setHandled(); - } - - private static void SendException(Throwable e, String sourcemessage) { - if (instance == null) return; - try { - getChannel().flatMap(channel -> { - Mono coderRole; - if (channel instanceof GuildChannel) - coderRole = instance.pingRole(((GuildChannel) channel).getGuild()).get(); - else - coderRole = Mono.empty(); - return coderRole.map(role -> TBMCCoreAPI.IsTestServer() ? new StringBuilder() - : new StringBuilder(role.getMention()).append("\n")) - .defaultIfEmpty(new StringBuilder()) - .flatMap(sb -> { - sb.append(sourcemessage).append("\n"); - sb.append("```").append("\n"); - String stackTrace = Arrays.stream(ExceptionUtils.getStackTrace(e).split("\\n")) - .filter(s -> !s.contains("\tat ") || s.contains("\tat buttondevteam.")) - .collect(Collectors.joining("\n")); - if (sb.length() + stackTrace.length() >= 1980) - stackTrace = stackTrace.substring(0, 1980 - sb.length()); - sb.append(stackTrace).append("\n"); - sb.append("```"); - return channel.createMessage(sb.toString()); - }); - }).subscribe(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - private static ExceptionListenerModule instance; - - public static Mono getChannel() { - if (instance != null) return instance.channel.get(); - return Mono.empty(); - } - - /** - * The channel to post the errors to. - */ - private final ReadOnlyConfigData> channel = DPUtils.channelData(getConfig(), "channel"); - - /** - * The role to ping if an error occurs. Set to empty ('') to disable. - */ - private ConfigData> pingRole(Mono guild) { - return DPUtils.roleData(getConfig(), "pingRole", "Coder", guild); - } - - @Override - protected void enable() { - if (DPUtils.disableIfConfigError(this, channel)) return; - instance = this; - Bukkit.getPluginManager().registerEvents(new ExceptionListenerModule(), getPlugin()); - TBMCCoreAPI.RegisterEventsForExceptions(new DebugMessageListener(), getPlugin()); - } - - @Override - protected void disable() { - instance = null; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/fun/FunModule.java b/src/main/java/buttondevteam/discordplugin/fun/FunModule.java deleted file mode 100644 index a0997ac..0000000 --- a/src/main/java/buttondevteam/discordplugin/fun/FunModule.java +++ /dev/null @@ -1,162 +0,0 @@ -package buttondevteam.discordplugin.fun; - -import buttondevteam.core.ComponentManager; -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.architecture.ConfigData; -import buttondevteam.lib.architecture.ReadOnlyConfigData; -import com.google.common.collect.Lists; -import discord4j.core.event.domain.PresenceUpdateEvent; -import discord4j.core.object.entity.Guild; -import discord4j.core.object.entity.Member; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.Role; -import discord4j.core.object.entity.channel.GuildChannel; -import discord4j.core.object.entity.channel.MessageChannel; -import discord4j.core.object.presence.Status; -import lombok.val; -import org.bukkit.Bukkit; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import reactor.core.publisher.Mono; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Random; -import java.util.concurrent.TimeUnit; -import java.util.stream.IntStream; - -/** - * All kinds of random things. - * The YEEHAW event uses an emoji named :YEEHAW: if available - */ -public class FunModule extends Component implements Listener { - private static final String[] serverReadyStrings = new String[]{"in one week from now", // Ali - "between now and the heat-death of the universe.", // Ghostise - "soon™", "ask again this time next month", // Ghostise - "in about 3 seconds", // Nicolai - "after we finish 8 plugins", // Ali - "tomorrow.", // Ali - "after one tiiiny feature", // Ali - "next commit", // Ali - "after we finish strangling Towny", // Ali - "when we kill every *fucking* bug", // Ali - "once the server stops screaming.", // Ali - "after HL3 comes out", // Ali - "next time you ask", // Ali - "when will *you* be open?" // Ali - }; - - /** - * Questions that the bot will choose a random answer to give to. - */ - private final ConfigData serverReady = getConfig().getData("serverReady", () -> new String[]{ - "when will the server be open", "when will the server be ready", "when will the server be done", - "when will the server be complete", "when will the server be finished", "when's the server ready", - "when's the server open", "vhen vill ze server be open?"}); - - /** - * Answers for a recognized question. Selected randomly. - */ - private final ConfigData> serverReadyAnswers = getConfig().getData("serverReadyAnswers", () -> Lists.newArrayList(serverReadyStrings)); - - private static final Random serverReadyRandom = new Random(); - private static final ArrayList usableServerReadyStrings = new ArrayList<>(0); - - private void createUsableServerReadyStrings() { - IntStream.range(0, serverReadyAnswers.get().size()) - .forEach(i -> FunModule.usableServerReadyStrings.add((short) i)); - } - - @Override - protected void enable() { - registerListener(this); - } - - @Override - protected void disable() { - lastlist = lastlistp = ListC = 0; - } - - private static short lastlist = 0; - private static short lastlistp = 0; - - private static short ListC = 0; - - public static boolean executeMemes(Message message) { - val fm = ComponentManager.getIfEnabled(FunModule.class); - if (fm == null) return false; - String msglowercased = message.getContent().toLowerCase(); - lastlist++; - if (lastlist > 5) { - ListC = 0; - lastlist = 0; - } - if (msglowercased.equals("/list") && Bukkit.getOnlinePlayers().size() == lastlistp && ListC++ > 2) // Lowered already - { - DPUtils.reply(message, Mono.empty(), "stop it. You know the answer.").subscribe(); - lastlist = 0; - lastlistp = (short) Bukkit.getOnlinePlayers().size(); - return true; //Handled - } - lastlistp = (short) Bukkit.getOnlinePlayers().size(); //Didn't handle - if (!TBMCCoreAPI.IsTestServer() - && Arrays.stream(fm.serverReady.get()).anyMatch(msglowercased::contains)) { - int next; - if (usableServerReadyStrings.size() == 0) - fm.createUsableServerReadyStrings(); - next = usableServerReadyStrings.remove(serverReadyRandom.nextInt(usableServerReadyStrings.size())); - DPUtils.reply(message, Mono.empty(), fm.serverReadyAnswers.get().get(next)).subscribe(); - return false; //Still process it as a command/mcchat if needed - } - return false; - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - ListC = 0; - } - - /** - * If all of the people who have this role are online, the bot will post a full house. - */ - private ConfigData> fullHouseDevRole(Mono guild) { - return DPUtils.roleData(getConfig(), "fullHouseDevRole", "Developer", guild); - } - - - /** - * The channel to post the full house to. - */ - private final ReadOnlyConfigData> fullHouseChannel = DPUtils.channelData(getConfig(), "fullHouseChannel"); - - private static long lasttime = 0; - - public static void handleFullHouse(PresenceUpdateEvent event) { - val fm = ComponentManager.getIfEnabled(FunModule.class); - if (fm == null) return; - if (Calendar.getInstance().get(Calendar.DAY_OF_MONTH) % 5 != 0) return; - fm.fullHouseChannel.get() - .filter(ch -> ch instanceof GuildChannel) - .flatMap(channel -> fm.fullHouseDevRole(((GuildChannel) channel).getGuild()).get() - .filter(role -> event.getOld().map(p -> p.getStatus().equals(Status.OFFLINE)).orElse(false)) - .filter(role -> !event.getCurrent().getStatus().equals(Status.OFFLINE)) - .filterWhen(devrole -> event.getMember().flatMap(m -> m.getRoles() - .any(r -> r.getId().asLong() == devrole.getId().asLong()))) - .filterWhen(devrole -> - event.getGuild().flatMapMany(g -> g.getMembers().filter(m -> m.getRoleIds().stream().anyMatch(s -> s.equals(devrole.getId())))) - .flatMap(Member::getPresence).all(pr -> !pr.getStatus().equals(Status.OFFLINE))) - .filter(devrole -> lasttime + 10 < TimeUnit.NANOSECONDS.toHours(System.nanoTime())) //This should stay so it checks this last - .flatMap(devrole -> { - lasttime = TimeUnit.NANOSECONDS.toHours(System.nanoTime()); - return channel.createMessage(mcs -> mcs.setContent("Full house!").setEmbed(ecs -> - ecs.setImage( - "https://cdn.discordapp.com/attachments/249295547263877121/249687682618359808/poker-hand-full-house-aces-kings-playing-cards-15553791.png") - )); - })).subscribe(); - } -} diff --git a/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java b/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java deleted file mode 100644 index 08bd40f..0000000 --- a/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java +++ /dev/null @@ -1,99 +0,0 @@ -package buttondevteam.discordplugin.listeners; - -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.commands.Command2DCSender; -import buttondevteam.discordplugin.util.Timings; -import buttondevteam.lib.TBMCCoreAPI; -import discord4j.common.util.Snowflake; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.Role; -import discord4j.core.object.entity.channel.PrivateChannel; -import lombok.val; -import reactor.core.publisher.Mono; - -import java.util.concurrent.atomic.AtomicBoolean; - -public class CommandListener { - /** - * Runs a ChromaBot command. If mentionedonly is false, it will only execute the command if it was in #bot with the correct prefix or in private. - * - * @param message The Discord message - * @param mentionedonly Only run the command if ChromaBot is mentioned at the start of the message - * @return Whether it did not run the command - */ - public static Mono runCommand(Message message, Snowflake commandChannelID, boolean mentionedonly) { - Timings timings = CommonListeners.timings; - Mono ret = Mono.just(true); - if (message.getContent().length() == 0) - return ret; //Pin messages and such, let the mcchat listener deal with it - val content = message.getContent(); - timings.printElapsed("A"); - return message.getChannel().flatMap(channel -> { - Mono tmp = ret; - if (!mentionedonly) { //mentionedonly conditions are in CommonListeners - timings.printElapsed("B"); - if (!(channel instanceof PrivateChannel) - && !(content.charAt(0) == DiscordPlugin.getPrefix() - && channel.getId().asLong() == commandChannelID.asLong())) // - return ret; - timings.printElapsed("C"); - tmp = ret.then(channel.type()).thenReturn(true); // Fun (this true is ignored - x) - } - final StringBuilder cmdwithargs = new StringBuilder(content); - val gotmention = new AtomicBoolean(); - timings.printElapsed("Before self"); - return tmp.flatMapMany(x -> - DiscordPlugin.dc.getSelf().flatMap(self -> self.asMember(DiscordPlugin.mainServer.getId())) - .flatMapMany(self -> { - timings.printElapsed("D"); - gotmention.set(checkanddeletemention(cmdwithargs, self.getMention(), message)); - gotmention.set(checkanddeletemention(cmdwithargs, self.getNicknameMention(), message) || gotmention.get()); - val mentions = message.getRoleMentions(); - return self.getRoles().filterWhen(r -> mentions.any(rr -> rr.getName().equals(r.getName()))) - .map(Role::getMention); - }).map(mentionRole -> { - timings.printElapsed("E"); - gotmention.set(checkanddeletemention(cmdwithargs, mentionRole, message) || gotmention.get()); // Delete all mentions - return !mentionedonly || gotmention.get(); //Stops here if false - }).switchIfEmpty(Mono.fromSupplier(() -> !mentionedonly || gotmention.get()))) - .filter(b -> b).last(false).filter(b -> b).doOnNext(b -> channel.type().subscribe()).flatMap(b -> { - String cmdwithargsString = cmdwithargs.toString(); - try { - timings.printElapsed("F"); - if (!DiscordPlugin.plugin.getManager().handleCommand(new Command2DCSender(message), cmdwithargsString)) - return DPUtils.reply(message, channel, "unknown command. Do " + DiscordPlugin.getPrefix() + "help for help.") - .map(m -> false); - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e, DiscordPlugin.plugin); - } - return Mono.just(false); //If the command succeeded or there was an error, return false - }).defaultIfEmpty(true); - }); - } - - private static boolean checkanddeletemention(StringBuilder cmdwithargs, String mention, Message message) { - final char prefix = DiscordPlugin.getPrefix(); - if (message.getContent().startsWith(mention)) // TODO: Resolve mentions: Compound arguments, either a mention or text - if (cmdwithargs.length() > mention.length() + 1) { - int i = cmdwithargs.indexOf(" ", mention.length()); - if (i == -1) - i = mention.length(); - else - //noinspection StatementWithEmptyBody - for (; i < cmdwithargs.length() && cmdwithargs.charAt(i) == ' '; i++) - ; //Removes any space before the command - cmdwithargs.delete(0, i); - cmdwithargs.insert(0, prefix); //Always use the prefix for processing - } else - cmdwithargs.replace(0, cmdwithargs.length(), prefix + "help"); - else { - if (cmdwithargs.length() == 0) - cmdwithargs.replace(0, 0, prefix + "help"); - else if (cmdwithargs.charAt(0) != prefix) - cmdwithargs.insert(0, prefix); - return false; //Don't treat / as mention, mentions can be used in public mcchat - } - return true; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/listeners/CommonListeners.java b/src/main/java/buttondevteam/discordplugin/listeners/CommonListeners.java deleted file mode 100755 index 57985aa..0000000 --- a/src/main/java/buttondevteam/discordplugin/listeners/CommonListeners.java +++ /dev/null @@ -1,88 +0,0 @@ -package buttondevteam.discordplugin.listeners; - -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.fun.FunModule; -import buttondevteam.discordplugin.mcchat.MinecraftChatModule; -import buttondevteam.discordplugin.role.GameRoleModule; -import buttondevteam.discordplugin.util.Timings; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.architecture.Component; -import discord4j.core.event.EventDispatcher; -import discord4j.core.event.domain.PresenceUpdateEvent; -import discord4j.core.event.domain.message.MessageCreateEvent; -import discord4j.core.event.domain.role.RoleCreateEvent; -import discord4j.core.event.domain.role.RoleDeleteEvent; -import discord4j.core.event.domain.role.RoleUpdateEvent; -import discord4j.core.object.entity.channel.PrivateChannel; -import lombok.val; -import reactor.core.publisher.Mono; - -public class CommonListeners { - - public static final Timings timings = new Timings(); - - /* - MentionEvent: - - CommandListener (starts with mention, only 'channelcon' and not in #bot) - - MessageReceivedEvent: - - v CommandListener (starts with mention, in #bot or a connected chat) - - Minecraft chat (is enabled in the channel and message isn't [/]mcchat) - - CommandListener (with the correct prefix in #bot, or in private) - */ - public static void register(EventDispatcher dispatcher) { - dispatcher.on(MessageCreateEvent.class).flatMap(event -> { - timings.printElapsed("Message received"); - val def = Mono.empty(); - if (DiscordPlugin.SafeMode) - return def; - val author = event.getMessage().getAuthor(); - if (!author.isPresent() || author.get().isBot()) - return def; - if (FunModule.executeMemes(event.getMessage())) - return def; - val commandChannel = DiscordPlugin.plugin.commandChannel.get(); - return event.getMessage().getChannel().map(mch -> - (commandChannel != null && mch.getId().asLong() == commandChannel.asLong()) //If mentioned, that's higher than chat - || mch instanceof PrivateChannel - || event.getMessage().getContent().contains("channelcon")) //Only 'channelcon' is allowed in other channels - .flatMap(shouldRun -> { //Only continue if this doesn't handle the event - if (!shouldRun) - return Mono.just(true); //The condition is only for the first command execution, not mcchat - timings.printElapsed("Run command 1"); - return CommandListener.runCommand(event.getMessage(), commandChannel, true); //#bot is handled here - }).filterWhen(ch -> { - timings.printElapsed("mcchat"); - val mcchat = Component.getComponents().get(MinecraftChatModule.class); - if (mcchat != null && mcchat.isEnabled()) //ComponentManager.isEnabled() searches the component again - return ((MinecraftChatModule) mcchat).getListener().handleDiscord(event); //Also runs Discord commands in chat channels - return Mono.just(true); //Wasn't handled, continue - }).filterWhen(ch -> { - timings.printElapsed("Run command 2"); - return CommandListener.runCommand(event.getMessage(), commandChannel, false); - }); - }).onErrorContinue((err, obj) -> TBMCCoreAPI.SendException("An error occured while handling a message!", err, DiscordPlugin.plugin)) - .subscribe(); - dispatcher.on(PresenceUpdateEvent.class).subscribe(event -> { - if (DiscordPlugin.SafeMode) - return; - FunModule.handleFullHouse(event); - }); - dispatcher.on(RoleCreateEvent.class).subscribe(GameRoleModule::handleRoleEvent); - dispatcher.on(RoleDeleteEvent.class).subscribe(GameRoleModule::handleRoleEvent); - dispatcher.on(RoleUpdateEvent.class).subscribe(GameRoleModule::handleRoleEvent); - - } - - private static boolean debug = false; - - public static void debug(String debug) { - if (CommonListeners.debug) //Debug - DPUtils.getLogger().info(debug); - } - - public static boolean debug() { - return debug = !debug; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java b/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java deleted file mode 100755 index febe7ea..0000000 --- a/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java +++ /dev/null @@ -1,68 +0,0 @@ -package buttondevteam.discordplugin.listeners; - -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.commands.ConnectCommand; -import buttondevteam.discordplugin.mcchat.MinecraftChatModule; -import buttondevteam.discordplugin.util.DPState; -import buttondevteam.lib.ScheduledServerRestartEvent; -import buttondevteam.lib.player.TBMCPlayerGetInfoEvent; -import discord4j.common.util.Snowflake; -import discord4j.core.object.entity.Member; -import discord4j.core.object.entity.User; -import lombok.val; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import reactor.core.publisher.Mono; - -public class MCListener implements Listener { - @EventHandler - public void onPlayerJoin(PlayerJoinEvent e) { - if (ConnectCommand.WaitingToConnect.containsKey(e.getPlayer().getName())) { - @SuppressWarnings("ConstantConditions") User user = DiscordPlugin.dc - .getUserById(Snowflake.of(ConnectCommand.WaitingToConnect.get(e.getPlayer().getName()))).block(); - if (user == null) return; - e.getPlayer().sendMessage("§bTo connect with the Discord account @" + user.getUsername() + "#" + user.getDiscriminator() - + " do /discord accept"); - e.getPlayer().sendMessage("§bIf it wasn't you, do /discord decline"); - } - } - - @EventHandler - public void onGetInfo(TBMCPlayerGetInfoEvent e) { - if (DiscordPlugin.SafeMode) - return; - DiscordPlayer dp = e.getPlayer().getAs(DiscordPlayer.class); - if (dp == null || dp.getDiscordID() == null || dp.getDiscordID().equals("")) - return; - val userOpt = DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID())).onErrorResume(t -> Mono.empty()).blockOptional(); - if (!userOpt.isPresent()) return; - User user = userOpt.get(); - e.addInfo("Discord tag: " + user.getUsername() + "#" + user.getDiscriminator()); - val memberOpt = user.asMember(DiscordPlugin.mainServer.getId()).onErrorResume(t -> Mono.empty()).blockOptional(); - if (!memberOpt.isPresent()) return; - Member member = memberOpt.get(); - val prOpt = member.getPresence().blockOptional(); - if (!prOpt.isPresent()) return; - val pr = prOpt.get(); - e.addInfo(pr.getStatus().toString()); - if (pr.getActivity().isPresent()) { - val activity = pr.getActivity().get(); - e.addInfo(activity.getType() + ": " + activity.getName()); - } - } - - /*@EventHandler - public void onCommandPreprocess(TBMCCommandPreprocessEvent e) { - if (e.getMessage().equalsIgnoreCase("/stop")) - MinecraftChatModule.state = DPState.STOPPING_SERVER; - else if (e.getMessage().equalsIgnoreCase("/restart")) - MinecraftChatModule.state = DPState.RESTARTING_SERVER; - }*/ - - @EventHandler //We don't really need this with the logger stuff but hey - public void onScheduledRestart(ScheduledServerRestartEvent e) { - MinecraftChatModule.state = DPState.RESTARTING_SERVER; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.java b/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.java deleted file mode 100644 index 190c10e..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.java +++ /dev/null @@ -1,176 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.component.channel.Channel; -import buttondevteam.core.component.channel.ChatRoom; -import buttondevteam.discordplugin.*; -import buttondevteam.discordplugin.commands.Command2DCSender; -import buttondevteam.discordplugin.commands.ICommand2DC; -import buttondevteam.lib.TBMCSystemChatEvent; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -import buttondevteam.lib.player.TBMCPlayer; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.GuildChannel; -import discord4j.core.object.entity.channel.MessageChannel; -import discord4j.rest.util.Permission; -import lombok.RequiredArgsConstructor; -import lombok.val; -import org.bukkit.Bukkit; -import reactor.core.publisher.Mono; - -import javax.annotation.Nullable; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -@SuppressWarnings("SimplifyOptionalCallChains") //Java 11 -@CommandClass(helpText = {"Channel connect", // - "This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", // - "You need to have access to the MC channel and have manage permissions on the Discord channel.", // - "You also need to have your Minecraft account connected. In #bot use /connect .", // - "Call this command from the channel you want to use.", // - "Usage: @Bot channelcon ", // - "Use the ID (command) of the channel, for example `g` for the global chat.", // - "To remove a connection use @ChromaBot channelcon remove in the channel.", // - "Mentioning the bot is needed in this case because the / prefix only works in #bot.", // - "Invite link: " // -}) -@RequiredArgsConstructor -public class ChannelconCommand extends ICommand2DC { - private final MinecraftChatModule module; - - @Command2.Subcommand - public boolean remove(Command2DCSender sender) { - val message = sender.getMessage(); - if (checkPerms(message, null)) return true; - if (MCChatCustom.removeCustomChat(message.getChannelId())) - DPUtils.reply(message, Mono.empty(), "channel connection removed.").subscribe(); - else - DPUtils.reply(message, Mono.empty(), "this channel isn't connected.").subscribe(); - return true; - } - - @Command2.Subcommand - public boolean toggle(Command2DCSender sender, @Command2.OptionalArg String toggle) { - val message = sender.getMessage(); - if (checkPerms(message, null)) return true; - val cc = MCChatCustom.getCustomChat(message.getChannelId()); - if (cc == null) - return respond(sender, "this channel isn't connected."); - Supplier togglesString = () -> Arrays.stream(ChannelconBroadcast.values()).map(t -> t.toString().toLowerCase() + ": " + ((cc.toggles & t.flag) == 0 ? "disabled" : "enabled")).collect(Collectors.joining("\n")) - + "\n\n" + TBMCSystemChatEvent.BroadcastTarget.stream().map(target -> target.getName() + ": " + (cc.brtoggles.contains(target) ? "enabled" : "disabled")).collect(Collectors.joining("\n")); - if (toggle == null) { - DPUtils.reply(message, Mono.empty(), "toggles:\n" + togglesString.get()).subscribe(); - return true; - } - String arg = toggle.toUpperCase(); - val b = Arrays.stream(ChannelconBroadcast.values()).filter(t -> t.toString().equals(arg)).findAny(); - if (!b.isPresent()) { - val bt = TBMCSystemChatEvent.BroadcastTarget.get(arg); - if (bt == null) { - DPUtils.reply(message, Mono.empty(), "cannot find toggle. Toggles:\n" + togglesString.get()).subscribe(); - return true; - } - final boolean add; - if (add = !cc.brtoggles.contains(bt)) - cc.brtoggles.add(bt); - else - cc.brtoggles.remove(bt); - return respond(sender, "'" + bt.getName() + "' " + (add ? "en" : "dis") + "abled"); - } - //A B | F - //------- A: original - B: mask - F: new - //0 0 | 0 - //0 1 | 1 - //1 0 | 1 - //1 1 | 0 - // XOR - cc.toggles ^= b.get().flag; - DPUtils.reply(message, Mono.empty(), "'" + b.get().toString().toLowerCase() + "' " + ((cc.toggles & b.get().flag) == 0 ? "disabled" : "enabled")).subscribe(); - return true; - } - - @Command2.Subcommand - public boolean def(Command2DCSender sender, String channelID) { - val message = sender.getMessage(); - if (!module.allowCustomChat.get()) { - sender.sendMessage("channel connection is not allowed on this Minecraft server."); - return true; - } - val channel = message.getChannel().block(); - if (checkPerms(message, channel)) return true; - if (MCChatCustom.hasCustomChat(message.getChannelId())) - return respond(sender, "this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it."); - val chan = Channel.getChannels().filter(ch -> ch.ID.equalsIgnoreCase(channelID) || (Arrays.stream(ch.IDs.get()).anyMatch(cid -> cid.equalsIgnoreCase(channelID)))).findAny(); - if (!chan.isPresent()) { //TODO: Red embed that disappears over time (kinda like the highlight messages in OW) - DPUtils.reply(message, channel, "MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /.").subscribe(); - return true; - } - if (!message.getAuthor().isPresent()) return true; - val author = message.getAuthor().get(); - val dp = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class); - val chp = dp.getAs(TBMCPlayer.class); - if (chp == null) { - DPUtils.reply(message, channel, "you need to connect your Minecraft account. On the main server in " + DPUtils.botmention() + " do " + DiscordPlugin.getPrefix() + "connect ").subscribe(); - return true; - } - DiscordConnectedPlayer dcp = DiscordConnectedPlayer.create(message.getAuthor().get(), channel, chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName(), module); - //Using a fake player with no login/logout, should be fine for this event - String groupid = chan.get().getGroupID(dcp); - if (groupid == null && !(chan.get() instanceof ChatRoom)) { //ChatRooms don't allow it unless the user joins, which happens later - DPUtils.reply(message, channel, "sorry, you cannot use that Minecraft channel.").subscribe(); - return true; - } - if (chan.get() instanceof ChatRoom) { //ChatRooms don't work well - DPUtils.reply(message, channel, "chat rooms are not supported yet.").subscribe(); - return true; - } - /*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) { - DPUtils.reply(message, null, "sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm."); - return true; - }*/ //TODO: "Channel admins" that can connect channels? - MCChatCustom.addCustomChat(channel, groupid, chan.get(), author, dcp, 0, new HashSet<>()); - if (chan.get() instanceof ChatRoom) - DPUtils.reply(message, channel, "alright, connection made to the room!").subscribe(); - else - DPUtils.reply(message, channel, "alright, connection made to group `" + groupid + "`!").subscribe(); - return true; - } - - @SuppressWarnings("ConstantConditions") - private boolean checkPerms(Message message, @Nullable MessageChannel channel) { - if (channel == null) - channel = message.getChannel().block(); - if (!(channel instanceof GuildChannel)) { - DPUtils.reply(message, channel, "you can only use this command in a server!").subscribe(); - return true; - } - //noinspection OptionalGetWithoutIsPresent - var perms = ((GuildChannel) channel).getEffectivePermissions(message.getAuthor().map(User::getId).get()).block(); - if (!perms.contains(Permission.ADMINISTRATOR) && !perms.contains(Permission.MANAGE_CHANNELS)) { - DPUtils.reply(message, channel, "you need to have manage permissions for this channel!").subscribe(); - return true; - } - return false; - } - - @Override - public String[] getHelpText(Method method, Command2.Subcommand ann) { - return new String[]{ // - "Channel connect", // - "This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", // - "You need to have access to the MC channel and have manage permissions on the Discord channel.", // - "You also need to have your Minecraft account connected. In " + DPUtils.botmention() + " use " + DiscordPlugin.getPrefix() + "connect .", // - "Call this command from the channel you want to use.", // - "Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf().block()).getMention() + " channelcon ", // - "Use the ID (command) of the channel, for example `g` for the global chat.", // - "To remove a connection use @ChromaBot channelcon remove in the channel.", // - "Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix() + " prefix only works in " + DPUtils.botmention() + ".", // - "Invite link: " - }; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.java deleted file mode 100755 index df7f6ad..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.java +++ /dev/null @@ -1,47 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.commands.Command2DCSender; -import buttondevteam.discordplugin.commands.ICommand2DC; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -import discord4j.core.object.entity.channel.PrivateChannel; -import lombok.RequiredArgsConstructor; -import lombok.val; - -@CommandClass(helpText = { - "MC Chat", - "This command enables or disables the Minecraft chat in private messages.", // - "It can be useful if you don't want your messages to be visible, for example when talking in a private channel.", // - "You can also run all of the ingame commands you have access to using this command, if you have your accounts connected." // -}) -@RequiredArgsConstructor -public class MCChatCommand extends ICommand2DC { - - private final MinecraftChatModule module; - - @Command2.Subcommand - public boolean def(Command2DCSender sender) { - if (!module.allowPrivateChat.get()) { - sender.sendMessage("using the private chat is not allowed on this Minecraft server."); - return true; - } - val message = sender.getMessage(); - val channel = message.getChannel().block(); - @SuppressWarnings("OptionalGetWithoutIsPresent") val author = message.getAuthor().get(); - if (!(channel instanceof PrivateChannel)) { - DPUtils.reply(message, channel, "this command can only be issued in a direct message with the bot.").subscribe(); - return true; - } - final DiscordPlayer user = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class); - boolean mcchat = !user.isMinecraftChatEnabled(); - MCChatPrivate.privateMCChat(channel, mcchat, author, user); - DPUtils.reply(message, channel, "Minecraft chat " + (mcchat // - ? "enabled. Use '" + DiscordPlugin.getPrefix() + "mcchat' again to turn it off." // - : "disabled.")).subscribe(); - return true; - } // TODO: Pin channel switching to indicate the current channel - -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java deleted file mode 100644 index 611d0e8..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java +++ /dev/null @@ -1,78 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.component.channel.Channel; -import buttondevteam.core.component.channel.ChatRoom; -import buttondevteam.discordplugin.DiscordConnectedPlayer; -import buttondevteam.lib.TBMCSystemChatEvent; -import discord4j.common.util.Snowflake; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.MessageChannel; -import lombok.NonNull; -import lombok.val; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -public class MCChatCustom { - /** - * Used for town or nation chats or anything else - */ - static final ArrayList lastmsgCustom = new ArrayList<>(); - - public static void addCustomChat(MessageChannel channel, String groupid, Channel mcchannel, User user, DiscordConnectedPlayer dcp, int toggles, Set brtoggles) { - synchronized (lastmsgCustom) { - if (mcchannel instanceof ChatRoom) { - ((ChatRoom) mcchannel).joinRoom(dcp); - if (groupid == null) groupid = mcchannel.getGroupID(dcp); - } - val lmd = new CustomLMD(channel, user, groupid, mcchannel, dcp, toggles, brtoggles); - lastmsgCustom.add(lmd); - } - } - - public static boolean hasCustomChat(Snowflake channel) { - return lastmsgCustom.stream().anyMatch(lmd -> lmd.channel.getId().asLong() == channel.asLong()); - } - - @Nullable - public static CustomLMD getCustomChat(Snowflake channel) { - return lastmsgCustom.stream().filter(lmd -> lmd.channel.getId().asLong() == channel.asLong()).findAny().orElse(null); - } - - public static boolean removeCustomChat(Snowflake channel) { - synchronized (lastmsgCustom) { - MCChatUtils.lastmsgfromd.remove(channel.asLong()); - return lastmsgCustom.removeIf(lmd -> { - if (lmd.channel.getId().asLong() != channel.asLong()) - return false; - if (lmd.mcchannel instanceof ChatRoom) - ((ChatRoom) lmd.mcchannel).leaveRoom(lmd.dcp); - return true; - }); - } - } - - public static List getCustomChats() { - return Collections.unmodifiableList(lastmsgCustom); - } - - public static class CustomLMD extends MCChatUtils.LastMsgData { - public final String groupID; - public final DiscordConnectedPlayer dcp; - public int toggles; - public Set brtoggles; - - private CustomLMD(@NonNull MessageChannel channel, @NonNull User user, - @NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp, int toggles, Set brtoggles) { - super(channel, user); - groupID = groupid; - this.mcchannel = mcchannel; - this.dcp = dcp; - this.toggles = toggles; - this.brtoggles = brtoggles; - } - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java deleted file mode 100755 index 007b90c..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java +++ /dev/null @@ -1,417 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.ComponentManager; -import buttondevteam.discordplugin.*; -import buttondevteam.discordplugin.listeners.CommandListener; -import buttondevteam.discordplugin.listeners.CommonListeners; -import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; -import buttondevteam.discordplugin.playerfaker.VanillaCommandListener14; -import buttondevteam.discordplugin.playerfaker.VanillaCommandListener15; -import buttondevteam.discordplugin.util.Timings; -import buttondevteam.lib.*; -import buttondevteam.lib.chat.ChatMessage; -import buttondevteam.lib.chat.TBMCChatAPI; -import buttondevteam.lib.player.TBMCPlayer; -import com.vdurmont.emoji.EmojiParser; -import discord4j.common.util.Snowflake; -import discord4j.core.event.domain.message.MessageCreateEvent; -import discord4j.core.object.Embed; -import discord4j.core.object.entity.Attachment; -import discord4j.core.object.entity.Guild; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.GuildChannel; -import discord4j.core.object.entity.channel.PrivateChannel; -import discord4j.core.spec.EmbedCreateSpec; -import discord4j.rest.util.Color; -import lombok.val; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.scheduler.BukkitTask; -import reactor.core.publisher.Mono; - -import java.time.Instant; -import java.util.AbstractMap; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeoutException; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -public class MCChatListener implements Listener { - private BukkitTask sendtask; - private final LinkedBlockingQueue> sendevents = new LinkedBlockingQueue<>(); - private Runnable sendrunnable; - private Thread sendthread; - private final MinecraftChatModule module; - private boolean stop = false; //A new instance will be created on enable - - public MCChatListener(MinecraftChatModule minecraftChatModule) { - module = minecraftChatModule; - } - - @EventHandler // Minecraft - public void onMCChat(TBMCChatEvent ev) { - if (!ComponentManager.isEnabled(MinecraftChatModule.class) || ev.isCancelled()) //SafeMode: Needed so it doesn't restart after server shutdown - return; - sendevents.add(new AbstractMap.SimpleEntry<>(ev, Instant.now())); - if (sendtask != null) - return; - sendrunnable = () -> { - sendthread = Thread.currentThread(); - processMCToDiscord(); - if (DiscordPlugin.plugin.isEnabled() && !stop) //Don't run again if shutting down - sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); - }; - sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); - } - - private void processMCToDiscord() { - try { - TBMCChatEvent e; - Instant time; - val se = sendevents.take(); // Wait until an element is available - e = se.getKey(); - time = se.getValue(); - - final String authorPlayer = "[" + DPUtils.sanitizeStringNoEscape(e.getChannel().DisplayName.get()) + "] " // - + ("Minecraft".equals(e.getOrigin()) ? "" : "[" + e.getOrigin().charAt(0) + "]") // - + (DPUtils.sanitizeStringNoEscape(ChromaUtils.getDisplayName(e.getSender()))); - val color = e.getChannel().Color.get(); - final Consumer embed = ecs -> { - ecs.setDescription(e.getMessage()).setColor(Color.of(color.getRed(), - color.getGreen(), color.getBlue())); - String url = module.profileURL.get(); - if (e.getSender() instanceof Player) - DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(), - url.length() > 0 ? url + "?type=minecraft&id=" - + ((Player) e.getSender()).getUniqueId() : null); - else if (e.getSender() instanceof DiscordSenderBase) - ecs.setAuthor(authorPlayer, url.length() > 0 ? url + "?type=discord&id=" - + ((DiscordSenderBase) e.getSender()).getUser().getId().asString() : null, - ((DiscordSenderBase) e.getSender()).getUser().getAvatarUrl()); - else - DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(), null); - ecs.setTimestamp(time); - }; - final long nanoTime = System.nanoTime(); - InterruptibleConsumer doit = lastmsgdata -> { - if (lastmsgdata.message == null - || !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().map(Embed.Author::getName).orElse(null)) - || lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120 - || !lastmsgdata.mcchannel.ID.equals(e.getChannel().ID) - || lastmsgdata.content.length() + e.getMessage().length() + 1 > 2048) { - lastmsgdata.message = lastmsgdata.channel.createEmbed(embed).block(); - lastmsgdata.time = nanoTime; - lastmsgdata.mcchannel = e.getChannel(); - lastmsgdata.content = e.getMessage(); - } else { - lastmsgdata.content = lastmsgdata.content + "\n" - + e.getMessage(); // The message object doesn't get updated - lastmsgdata.message.edit(mes -> mes.setEmbed(embed.andThen(ecs -> - ecs.setDescription(lastmsgdata.content)))).block(); - } - }; - // Checks if the given channel is different than where the message was sent from - // Or if it was from MC - Predicate isdifferentchannel = id -> !(e.getSender() instanceof DiscordSenderBase) - || ((DiscordSenderBase) e.getSender()).getChannel().getId().asLong() != id.asLong(); - - if (e.getChannel().isGlobal() - && (e.isFromCommand() || isdifferentchannel.test(module.chatChannel.get()))) - doit.accept(MCChatUtils.lastmsgdata == null - ? MCChatUtils.lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannelMono().block(), null) - : MCChatUtils.lastmsgdata); - - for (MCChatUtils.LastMsgData data : MCChatPrivate.lastmsgPerUser) { - if ((e.isFromCommand() || isdifferentchannel.test(data.channel.getId())) - && e.shouldSendTo(MCChatUtils.getSender(data.channel.getId(), data.user))) - doit.accept(data); - } - - synchronized (MCChatCustom.lastmsgCustom) { - val iterator = MCChatCustom.lastmsgCustom.iterator(); - while (iterator.hasNext()) { - val lmd = iterator.next(); - if ((e.isFromCommand() || isdifferentchannel.test(lmd.channel.getId())) //Test if msg is from Discord - && e.getChannel().ID.equals(lmd.mcchannel.ID) //If it's from a command, the command msg has been deleted, so we need to send it - && e.getGroupID().equals(lmd.groupID)) { //Check if this is the group we want to test - #58 - if (e.shouldSendTo(lmd.dcp)) //Check original user's permissions - doit.accept(lmd); - else { - iterator.remove(); //If the user no longer has permission, remove the connection - lmd.channel.createMessage("The user no longer has permission to view the channel, connection removed.").subscribe(); - } - } - } - } - } catch (InterruptedException ex) { //Stop if interrupted anywhere - sendtask.cancel(); - sendtask = null; - } catch (Exception ex) { - TBMCCoreAPI.SendException("Error while sending message to Discord!", ex, module); - } - } - - @EventHandler - public void onChatPreprocess(TBMCChatPreprocessEvent event) { - int start = -1; - while ((start = event.getMessage().indexOf('@', start + 1)) != -1) { - int mid = event.getMessage().indexOf('#', start + 1); - if (mid == -1) - return; - int end_ = event.getMessage().indexOf(' ', mid + 1); - if (end_ == -1) - end_ = event.getMessage().length(); - final int end = end_; - final int startF = start; - val user = DiscordPlugin.dc.getUsers().filter(u -> u.getUsername().equals(event.getMessage().substring(startF + 1, mid))) - .filter(u -> u.getDiscriminator().equals(event.getMessage().substring(mid + 1, end))).blockFirst(); - if (user != null) //TODO: Nicknames - event.setMessage(event.getMessage().substring(0, startF) + "@" + user.getUsername() - + (event.getMessage().length() > end ? event.getMessage().substring(end) : "")); // TODO: Add formatting - start = end; // Skip any @s inside the mention - } - } - - // ......................DiscordSender....DiscordConnectedPlayer.DiscordPlayerSender - // Offline public chat......x............................................ - // Online public chat.......x...........................................x - // Offline private chat.....x.......................x.................... - // Online private chat......x.......................x...................x - // If online and enabling private chat, don't login - // If leaving the server and private chat is enabled (has ConnectedPlayer), call login in a task on lowest priority - // If private chat is enabled and joining the server, logout the fake player on highest priority - // If online and disabling private chat, don't logout - // The maps may not contain the senders for UnconnectedSenders - - /** - * Stop the listener permanently. Enabling the module will create a new instance. - * - * @param wait Wait 5 seconds for the threads to stop - */ - public void stop(boolean wait) { - stop = true; - MCChatPrivate.logoutAll(); - MCChatUtils.LoggedInPlayers.clear(); - if (sendthread != null) sendthread.interrupt(); - if (recthread != null) recthread.interrupt(); - try { - if (sendthread != null) { - sendthread.interrupt(); - if (wait) - sendthread.join(5000); - } - if (recthread != null) { - recthread.interrupt(); - if (wait) - recthread.join(5000); - } - MCChatUtils.lastmsgdata = null; - MCChatPrivate.lastmsgPerUser.clear(); - MCChatCustom.lastmsgCustom.clear(); - MCChatUtils.lastmsgfromd.clear(); - MCChatUtils.UnconnectedSenders.clear(); - recthread = sendthread = null; - } catch (InterruptedException e) { - e.printStackTrace(); //This thread shouldn't be interrupted - } - } - - private BukkitTask rectask; - private final LinkedBlockingQueue recevents = new LinkedBlockingQueue<>(); - private Runnable recrun; - private Thread recthread; - - // Discord - public Mono handleDiscord(MessageCreateEvent ev) { - Timings timings = CommonListeners.timings; - timings.printElapsed("Chat event"); - val author = ev.getMessage().getAuthor(); - final boolean hasCustomChat = MCChatCustom.hasCustomChat(ev.getMessage().getChannelId()); - var prefix = DiscordPlugin.getPrefix(); - return ev.getMessage().getChannel().filter(channel -> { - timings.printElapsed("Filter 1"); - return !(ev.getMessage().getChannelId().asLong() != module.chatChannel.get().asLong() - && !(channel instanceof PrivateChannel - && author.map(u -> MCChatPrivate.isMinecraftChatEnabled(u.getId().asString())).orElse(false)) - && !hasCustomChat); //Chat isn't enabled on this channel - }).filter(channel -> { - timings.printElapsed("Filter 2"); - return !(channel instanceof PrivateChannel //Only in private chat - && ev.getMessage().getContent().length() < "/mcchat<>".length() - && ev.getMessage().getContent().replace(prefix + "", "") - .equalsIgnoreCase("mcchat")); //Either mcchat or /mcchat - //Allow disabling the chat if needed - }).filterWhen(channel -> CommandListener.runCommand(ev.getMessage(), DiscordPlugin.plugin.commandChannel.get(), true)) - //Allow running commands in chat channels - .filter(channel -> { - MCChatUtils.resetLastMessage(channel); - recevents.add(ev); - timings.printElapsed("Message event added"); - if (rectask != null) - return true; - recrun = () -> { //Don't return in a while loop next time - recthread = Thread.currentThread(); - processDiscordToMC(); - if (DiscordPlugin.plugin.isEnabled() && !stop) //Don't run again if shutting down - rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Continue message processing - }; - rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Start message processing - return true; - }).map(b -> false).defaultIfEmpty(true); - } - - private void processDiscordToMC() { - MessageCreateEvent event; - try { - event = recevents.take(); - } catch (InterruptedException e1) { - rectask.cancel(); - return; - } - val sender = event.getMessage().getAuthor().orElse(null); - String dmessage = event.getMessage().getContent(); - try { - final DiscordSenderBase dsender = MCChatUtils.getSender(event.getMessage().getChannelId(), sender); - val user = dsender.getChromaUser(); - - for (User u : event.getMessage().getUserMentions().toIterable()) { //TODO: Role mentions - dmessage = dmessage.replace(u.getMention(), "@" + u.getUsername()); // TODO: IG Formatting - val m = u.asMember(DiscordPlugin.mainServer.getId()).onErrorResume(t -> Mono.empty()).blockOptional(); - if (m.isPresent()) { - val mm = m.get(); - final String nick = mm.getDisplayName(); - dmessage = dmessage.replace(mm.getNicknameMention(), "@" + nick); - } - } - for (GuildChannel ch : event.getGuild().flux().flatMap(Guild::getChannels).toIterable()) { - dmessage = dmessage.replace(ch.getMention(), "#" + ch.getName()); // TODO: IG Formatting - } - - dmessage = EmojiParser.parseToAliases(dmessage, EmojiParser.FitzpatrickAction.PARSE); //Converts emoji to text- TODO: Add option to disable (resource pack?) - dmessage = dmessage.replaceAll(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:"); //Convert to Discord's format so it still shows up - - dmessage = dmessage.replaceAll("", ":$1:"); //We don't need info about the custom emojis, just display their text - - Function getChatMessage = msg -> // - msg + (event.getMessage().getAttachments().size() > 0 ? "\n" + event.getMessage() - .getAttachments().stream().map(Attachment::getUrl).collect(Collectors.joining("\n")) - : ""); - - MCChatCustom.CustomLMD clmd = MCChatCustom.getCustomChat(event.getMessage().getChannelId()); - - boolean react = false; - - val sendChannel = event.getMessage().getChannel().block(); - boolean isPrivate = sendChannel instanceof PrivateChannel; - if (dmessage.startsWith("/")) { // Ingame command - if (handleIngameCommand(event, dmessage, dsender, user, clmd, isPrivate)) return; - } else {// Not a command - react = handleIngameMessage(event, dmessage, dsender, user, getChatMessage, clmd, isPrivate); - } - if (react) { - try { - val lmfd = MCChatUtils.lastmsgfromd.get(event.getMessage().getChannelId().asLong()); - if (lmfd != null) { - lmfd.removeSelfReaction(DiscordPlugin.DELIVERED_REACTION).subscribe(); // Remove it no matter what, we know it's there 99.99% of the time - } - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while removing reactions from chat!", e, module); - } - MCChatUtils.lastmsgfromd.put(event.getMessage().getChannelId().asLong(), event.getMessage()); - event.getMessage().addReaction(DiscordPlugin.DELIVERED_REACTION).subscribe(); - } - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e, module); - } - } - - private boolean handleIngameMessage(MessageCreateEvent event, String dmessage, DiscordSenderBase dsender, DiscordPlayer user, Function getChatMessage, MCChatCustom.CustomLMD clmd, boolean isPrivate) { - boolean react = false; - if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0 - && !isPrivate && event.getMessage().getType() == Message.Type.CHANNEL_PINNED_MESSAGE) { - val rtr = clmd != null ? clmd.mcchannel.getRTR(clmd.dcp) - : dsender.getChromaUser().channel.get().getRTR(dsender); - TBMCChatAPI.SendSystemMessage(clmd != null ? clmd.mcchannel : dsender.getChromaUser().channel.get(), rtr, - (dsender instanceof Player ? ((Player) dsender).getDisplayName() - : dsender.getName()) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL); - } else { - val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(dmessage)).fromCommand(false); - if (clmd != null) - TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), clmd.mcchannel); - else - TBMCChatAPI.SendChatMessage(cmb.build()); - react = true; - } - return react; - } - - private boolean handleIngameCommand(MessageCreateEvent event, String dmessage, DiscordSenderBase dsender, DiscordPlayer user, MCChatCustom.CustomLMD clmd, boolean isPrivate) { - if (!isPrivate) - event.getMessage().delete().subscribe(); - final String cmd = dmessage.substring(1); - final String cmdlowercased = cmd.toLowerCase(); - if (dsender instanceof DiscordSender && module.whitelistedCommands().get().stream() - .noneMatch(s -> cmdlowercased.equals(s) || cmdlowercased.startsWith(s + " "))) { - // Command not whitelisted - dsender.sendMessage("Sorry, you can only access these commands from here:\n" - + module.whitelistedCommands().get().stream().map(uc -> "/" + uc) - .collect(Collectors.joining(", ")) - + (user.getConnectedID(TBMCPlayer.class) == null - ? "\nTo access your commands, first please connect your accounts, using /connect in " - + DPUtils.botmention() - + "\nThen y" - : "\nY") - + "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!"); - return true; - } - module.log(dsender.getName() + " ran from DC: /" + cmd); - if (dsender instanceof DiscordSender && runCustomCommand(dsender, cmdlowercased)) return true; - val channel = clmd == null ? user.channel.get() : clmd.mcchannel; - val ev = new TBMCCommandPreprocessEvent(dsender, channel, dmessage, clmd == null ? dsender : clmd.dcp); - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, //Commands need to be run sync - () -> { - Bukkit.getPluginManager().callEvent(ev); - if (ev.isCancelled()) - return; - try { - String mcpackage = Bukkit.getServer().getClass().getPackage().getName(); - if (!module.enableVanillaCommands.get()) - Bukkit.dispatchCommand(dsender, cmd); - else if (mcpackage.contains("1_12")) - VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd); - else if (mcpackage.contains("1_14")) - VanillaCommandListener14.runBukkitOrVanillaCommand(dsender, cmd); - else if (mcpackage.contains("1_15") || mcpackage.contains("1_16")) - VanillaCommandListener15.runBukkitOrVanillaCommand(dsender, cmd); - else - Bukkit.dispatchCommand(dsender, cmd); - } catch (NoClassDefFoundError e) { - TBMCCoreAPI.SendException("A class is not found when trying to run command " + cmd + "!", e, module); - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occurred when trying to run command " + cmd + "! Vanilla commands are only supported in some MC versions.", e, module); - } - }); - return true; - } - - private boolean runCustomCommand(DiscordSenderBase dsender, String cmdlowercased) { - if (cmdlowercased.startsWith("list")) { - var players = Bukkit.getOnlinePlayers(); - dsender.sendMessage("There are " + players.stream().filter(MCChatUtils::checkEssentials).count() + " out of " + Bukkit.getMaxPlayers() + " players online."); - dsender.sendMessage("Players: " + players.stream().filter(MCChatUtils::checkEssentials) - .map(Player::getDisplayName).collect(Collectors.joining(", "))); - return true; - } - return false; - } - - @FunctionalInterface - private interface InterruptibleConsumer { - void accept(T value) throws TimeoutException, InterruptedException; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.java deleted file mode 100644 index 647aa0d..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.java +++ /dev/null @@ -1,76 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.ComponentManager; -import buttondevteam.discordplugin.DiscordConnectedPlayer; -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.DiscordSenderBase; -import buttondevteam.lib.player.TBMCPlayer; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.MessageChannel; -import discord4j.core.object.entity.channel.PrivateChannel; -import lombok.val; -import org.bukkit.Bukkit; - -import java.util.ArrayList; - -public class MCChatPrivate { - - /** - * Used for messages in PMs (mcchat). - */ - static ArrayList lastmsgPerUser = new ArrayList<>(); - - public static boolean privateMCChat(MessageChannel channel, boolean start, User user, DiscordPlayer dp) { - synchronized (MCChatUtils.ConnectedSenders) { - TBMCPlayer mcp = dp.getAs(TBMCPlayer.class); - if (mcp != null) { // If the accounts aren't connected, can't make a connected sender - val p = Bukkit.getPlayer(mcp.getUUID()); - val op = Bukkit.getOfflinePlayer(mcp.getUUID()); - val mcm = ComponentManager.getIfEnabled(MinecraftChatModule.class); - if (start) { - val sender = DiscordConnectedPlayer.create(user, channel, mcp.getUUID(), op.getName(), mcm); - MCChatUtils.addSender(MCChatUtils.ConnectedSenders, user, sender); - MCChatUtils.LoggedInPlayers.put(mcp.getUUID(), sender); - if (p == null) // Player is offline - If the player is online, that takes precedence - MCChatUtils.callLoginEvents(sender); - } else { - val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel.getId(), user); - assert sender != null; - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> { - if ((p == null || p instanceof DiscordSenderBase) // Player is offline - If the player is online, that takes precedence - && sender.isLoggedIn()) //Don't call the quit event if login failed - MCChatUtils.callLogoutEvent(sender, false); //The next line has to run *after* this one, so can't use the needsSync parameter - MCChatUtils.LoggedInPlayers.remove(sender.getUniqueId()); - sender.setLoggedIn(false); - }); - } - } // ---- PermissionsEx warning is normal on logout ---- - if (!start) - MCChatUtils.lastmsgfromd.remove(channel.getId().asLong()); - return start // - ? lastmsgPerUser.add(new MCChatUtils.LastMsgData(channel, user)) // Doesn't support group DMs - : lastmsgPerUser.removeIf(lmd -> lmd.channel.getId().asLong() == channel.getId().asLong()); - } - } - - public static boolean isMinecraftChatEnabled(DiscordPlayer dp) { - return isMinecraftChatEnabled(dp.getDiscordID()); - } - - public static boolean isMinecraftChatEnabled(String did) { // Don't load the player data just for this - return lastmsgPerUser.stream() - .anyMatch(lmd -> ((PrivateChannel) lmd.channel) - .getRecipientIds().stream().anyMatch(u -> u.asString().equals(did))); - } - - public static void logoutAll() { - synchronized (MCChatUtils.ConnectedSenders) { - for (val entry : MCChatUtils.ConnectedSenders.entrySet()) - for (val valueEntry : entry.getValue().entrySet()) - if (MCChatUtils.getSender(MCChatUtils.OnlineSenders, valueEntry.getKey(), valueEntry.getValue().getUser()) == null) //If the player is online then the fake player was already logged out - MCChatUtils.callLogoutEvent(valueEntry.getValue(), !Bukkit.isPrimaryThread()); - } - } - -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.java deleted file mode 100644 index b90a328..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.java +++ /dev/null @@ -1,410 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.ComponentManager; -import buttondevteam.core.MainPlugin; -import buttondevteam.discordplugin.*; -import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.TBMCSystemChatEvent; -import com.google.common.collect.Sets; -import discord4j.common.util.Snowflake; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.Channel; -import discord4j.core.object.entity.channel.MessageChannel; -import discord4j.core.object.entity.channel.PrivateChannel; -import discord4j.core.object.entity.channel.TextChannel; -import io.netty.util.collection.LongObjectHashMap; -import lombok.RequiredArgsConstructor; -import lombok.val; -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.bukkit.event.player.AsyncPlayerPreLoginEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerLoginEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.plugin.AuthorNagException; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.RegisteredListener; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; - -import javax.annotation.Nullable; -import java.net.InetAddress; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class MCChatUtils { - /** - * May contain P<DiscordID> as key for public chat - */ - public static final ConcurrentHashMap> UnconnectedSenders = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap> ConnectedSenders = new ConcurrentHashMap<>(); - /** - * May contain P<DiscordID> as key for public chat - */ - public static final ConcurrentHashMap> OnlineSenders = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap LoggedInPlayers = new ConcurrentHashMap<>(); - static @Nullable LastMsgData lastmsgdata; - static LongObjectHashMap lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks - private static MinecraftChatModule module; - private static final HashMap, HashSet> staticExcludedPlugins = new HashMap<>(); - - public static void updatePlayerList() { - val mod = getModule(); - if (mod == null || !mod.showPlayerListOnDC.get()) return; - if (lastmsgdata != null) - updatePL(lastmsgdata); - MCChatCustom.lastmsgCustom.forEach(MCChatUtils::updatePL); - } - - private static boolean notEnabled() { - return (module == null || !module.disabling) && getModule() == null; //Allow using things while disabling the module - } - - private static MinecraftChatModule getModule() { - if (module == null || !module.isEnabled()) module = ComponentManager.getIfEnabled(MinecraftChatModule.class); - //If disabled, it will try to get it again because another instance may be enabled - useful for /discord restart - return module; - } - - private static void updatePL(LastMsgData lmd) { - if (!(lmd.channel instanceof TextChannel)) { - TBMCCoreAPI.SendException("Failed to update player list for channel " + lmd.channel.getId(), - new Exception("The channel isn't a (guild) text channel."), getModule()); - return; - } - String topic = ((TextChannel) lmd.channel).getTopic().orElse(""); - if (topic.length() == 0) - topic = ".\n----\nMinecraft chat\n----\n."; - String[] s = topic.split("\\n----\\n"); - if (s.length < 3) - return; - String gid; - if (lmd instanceof MCChatCustom.CustomLMD) - gid = ((MCChatCustom.CustomLMD) lmd).groupID; - else //If we're not using a custom chat then it's either can ("everyone") or can't (null) see at most - gid = buttondevteam.core.component.channel.Channel.GROUP_EVERYONE; // (Though it's a public chat then rn) - AtomicInteger C = new AtomicInteger(); - s[s.length - 1] = "Players: " + Bukkit.getOnlinePlayers().stream() - .filter(p -> (lmd.mcchannel == null - ? gid.equals(buttondevteam.core.component.channel.Channel.GROUP_EVERYONE) //If null, allow if public (custom chats will have their channel stored anyway) - : gid.equals(lmd.mcchannel.getGroupID(p)))) //If they can see it - .filter(MCChatUtils::checkEssentials) - .filter(p -> C.incrementAndGet() > 0) //Always true - .map(p -> DPUtils.sanitizeString(p.getDisplayName())).collect(Collectors.joining(", ")); - s[0] = C + " player" + (C.get() != 1 ? "s" : "") + " online"; - ((TextChannel) lmd.channel).edit(tce -> tce.setTopic(String.join("\n----\n", s)).setReason("Player list update")).subscribe(); //Don't wait - } - - static boolean checkEssentials(Player p) { - var ess = MainPlugin.ess; - if (ess == null) return true; - return !ess.getUser(p).isHidden(); - } - - public static T addSender(ConcurrentHashMap> senders, - User user, T sender) { - return addSender(senders, user.getId().asString(), sender); - } - - public static T addSender(ConcurrentHashMap> senders, - String did, T sender) { - var map = senders.get(did); - if (map == null) - map = new ConcurrentHashMap<>(); - map.put(sender.getChannel().getId(), sender); - senders.put(did, map); - return sender; - } - - public static T getSender(ConcurrentHashMap> senders, - Snowflake channel, User user) { - var map = senders.get(user.getId().asString()); - if (map != null) - return map.get(channel); - return null; - } - - public static T removeSender(ConcurrentHashMap> senders, - Snowflake channel, User user) { - var map = senders.get(user.getId().asString()); - if (map != null) - return map.remove(channel); - return null; - } - - public static Mono forPublicPrivateChat(Function, Mono> action) { - if (notEnabled()) return Mono.empty(); - var list = new ArrayList>(); - list.add(action.apply(module.chatChannelMono())); - for (LastMsgData data : MCChatPrivate.lastmsgPerUser) - list.add(action.apply(Mono.just(data.channel))); - // lastmsgCustom.forEach(cc -> action.accept(cc.channel)); - Only send relevant messages to custom chat - return Mono.whenDelayError(list); - } - - /** - * For custom and all MC chat - * - * @param action The action to act (cannot complete empty) - * @param toggle The toggle to check - * @param hookmsg Whether the message is also sent from the hook - */ - public static Mono forCustomAndAllMCChat(Function, Mono> action, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { - if (notEnabled()) return Mono.empty(); - var list = new ArrayList>(); - if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg) - list.add(forPublicPrivateChat(action)); - final Function> customLMDFunction = cc -> action.apply(Mono.just(cc.channel)); - if (toggle == null) - MCChatCustom.lastmsgCustom.stream().map(customLMDFunction).forEach(list::add); - else - MCChatCustom.lastmsgCustom.stream().filter(cc -> (cc.toggles & toggle.flag) != 0).map(customLMDFunction).forEach(list::add); - return Mono.whenDelayError(list); - } - - /** - * Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled. - * - * @param action The action to do - * @param sender The sender to check perms of or null to send to all that has it toggled - * @param toggle The toggle to check or null to send to all allowed - */ - public static Mono forAllowedCustomMCChat(Function, Mono> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle) { - if (notEnabled()) return Mono.empty(); - Stream> st = MCChatCustom.lastmsgCustom.stream().filter(clmd -> { - //new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple - if (toggle != null && (clmd.toggles & toggle.flag) == 0) - return false; //If null then allow - if (sender == null) - return true; - return clmd.groupID.equals(clmd.mcchannel.getGroupID(sender)); - }).map(cc -> action.apply(Mono.just(cc.channel))); //TODO: Send error messages on channel connect - return Mono.whenDelayError(st::iterator); //Can't convert as an iterator or inside the stream, but I can convert it as a stream - } - - /** - * Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled. - * - * @param action The action to do - * @param sender The sender to check perms of or null to send to all that has it toggled - * @param toggle The toggle to check or null to send to all allowed - * @param hookmsg Whether the message is also sent from the hook - */ - public static Mono forAllowedCustomAndAllMCChat(Function, Mono> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { - if (notEnabled()) return Mono.empty(); - var cc = forAllowedCustomMCChat(action, sender, toggle); - if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg) - return Mono.whenDelayError(forPublicPrivateChat(action), cc); - return Mono.whenDelayError(cc); - } - - public static Function, Mono> send(String message) { - return ch -> ch.flatMap(mc -> { - resetLastMessage(mc); - return mc.createMessage(DPUtils.sanitizeString(message)); - }); - } - - public static Mono forAllowedMCChat(Function, Mono> action, TBMCSystemChatEvent event) { - if (notEnabled()) return Mono.empty(); - var list = new ArrayList>(); - if (event.getChannel().isGlobal()) - list.add(action.apply(module.chatChannelMono())); - for (LastMsgData data : MCChatPrivate.lastmsgPerUser) - if (event.shouldSendTo(getSender(data.channel.getId(), data.user))) - list.add(action.apply(Mono.just(data.channel))); //TODO: Only store ID? - MCChatCustom.lastmsgCustom.stream().filter(clmd -> { - if (!clmd.brtoggles.contains(event.getTarget())) - return false; - return event.shouldSendTo(clmd.dcp); - }).map(clmd -> action.apply(Mono.just(clmd.channel))).forEach(list::add); - return Mono.whenDelayError(list); - } - - /** - * This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc. - */ - static DiscordSenderBase getSender(Snowflake channel, final User author) { - //noinspection OptionalGetWithoutIsPresent - return Stream.>>of( // https://stackoverflow.com/a/28833677/2703239 - () -> Optional.ofNullable(getSender(OnlineSenders, channel, author)), // Find first non-null - () -> Optional.ofNullable(getSender(ConnectedSenders, channel, author)), // This doesn't support the public chat, but it'll always return null for it - () -> Optional.ofNullable(getSender(UnconnectedSenders, channel, author)), // - () -> Optional.of(addSender(UnconnectedSenders, author, - new DiscordSender(author, (MessageChannel) DiscordPlugin.dc.getChannelById(channel).block())))).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get(); - } - - /** - * Resets the last message, so it will start a new one instead of appending to it. - * This is used when someone (even the bot) sends a message to the channel. - * - * @param channel The channel to reset in - the process is slightly different for the public, private and custom chats - */ - public static void resetLastMessage(Channel channel) { - if (notEnabled()) return; - if (channel.getId().asLong() == module.chatChannel.get().asLong()) { - (lastmsgdata == null ? lastmsgdata = new LastMsgData(module.chatChannelMono().block(), null) - : lastmsgdata).message = null; - return; - } // Don't set the whole object to null, the player and channel information should be preserved - for (LastMsgData data : channel instanceof PrivateChannel ? MCChatPrivate.lastmsgPerUser : MCChatCustom.lastmsgCustom) { - if (data.channel.getId().asLong() == channel.getId().asLong()) { - data.message = null; - return; - } - } - //If it gets here, it's sending a message to a non-chat channel - } - - public static void addStaticExcludedPlugin(Class event, String plugin) { - staticExcludedPlugins.compute(event, (e, hs) -> hs == null - ? Sets.newHashSet(plugin) - : (hs.add(plugin) ? hs : hs)); - } - - public static void callEventExcludingSome(Event event) { - if (notEnabled()) return; - val second = staticExcludedPlugins.get(event.getClass()); - String[] first = module.excludedPlugins.get(); - String[] both = second == null ? first - : Arrays.copyOf(first, first.length + second.size()); - int i = first.length; - if (second != null) - for (String plugin : second) - both[i++] = plugin; - callEventExcluding(event, false, both); - } - - /** - * Calls an event with the given details. - *

- * This method only synchronizes when the event is not asynchronous. - * - * @param event Event details - * @param only Flips the operation and includes the listed plugins - * @param plugins The plugins to exclude. Not case sensitive. - */ - public static void callEventExcluding(Event event, boolean only, String... plugins) { // Copied from Spigot-API and modified a bit - if (event.isAsynchronous()) { - if (Thread.holdsLock(Bukkit.getPluginManager())) { - throw new IllegalStateException( - event.getEventName() + " cannot be triggered asynchronously from inside synchronized code."); - } - if (Bukkit.getServer().isPrimaryThread()) { - throw new IllegalStateException( - event.getEventName() + " cannot be triggered asynchronously from primary server thread."); - } - fireEventExcluding(event, only, plugins); - } else { - synchronized (Bukkit.getPluginManager()) { - fireEventExcluding(event, only, plugins); - } - } - } - - private static void fireEventExcluding(Event event, boolean only, String... plugins) { - HandlerList handlers = event.getHandlers(); // Code taken from SimplePluginManager in Spigot-API - RegisteredListener[] listeners = handlers.getRegisteredListeners(); - val server = Bukkit.getServer(); - - for (RegisteredListener registration : listeners) { - if (!registration.getPlugin().isEnabled() - || Arrays.stream(plugins).anyMatch(p -> only ^ p.equalsIgnoreCase(registration.getPlugin().getName()))) - continue; // Modified to exclude plugins - - try { - registration.callEvent(event); - } catch (AuthorNagException ex) { - Plugin plugin = registration.getPlugin(); - - if (plugin.isNaggable()) { - plugin.setNaggable(false); - - server.getLogger().log(Level.SEVERE, - String.format("Nag author(s): '%s' of '%s' about the following: %s", - plugin.getDescription().getAuthors(), plugin.getDescription().getFullName(), - ex.getMessage())); - } - } catch (Throwable ex) { - server.getLogger().log(Level.SEVERE, "Could not pass event " + event.getEventName() + " to " - + registration.getPlugin().getDescription().getFullName(), ex); - } - } - } - - /** - * Call it from an async thread. - */ - public static void callLoginEvents(DiscordConnectedPlayer dcp) { - Consumer> loginFail = kickMsg -> { - dcp.sendMessage("Minecraft chat disabled, as the login failed: " + kickMsg.get()); - MCChatPrivate.privateMCChat(dcp.getChannel(), false, dcp.getUser(), dcp.getChromaUser()); - }; //Probably also happens if the user is banned or so - val event = new AsyncPlayerPreLoginEvent(dcp.getName(), InetAddress.getLoopbackAddress(), dcp.getUniqueId()); - callEventExcludingSome(event); - if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { - loginFail.accept(event::getKickMessage); - return; - } - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> { - val ev = new PlayerLoginEvent(dcp, "localhost", InetAddress.getLoopbackAddress()); - callEventExcludingSome(ev); - if (ev.getResult() != PlayerLoginEvent.Result.ALLOWED) { - loginFail.accept(ev::getKickMessage); - return; - } - callEventExcludingSome(new PlayerJoinEvent(dcp, "")); - dcp.setLoggedIn(true); - if (module != null) { - if (module.serverWatcher != null) - module.serverWatcher.fakePlayers.add(dcp); - module.log(dcp.getName() + " (" + dcp.getUniqueId() + ") logged in from Discord"); - } - }); - } - - /** - * Only calls the events if the player is actually logged in - * - * @param dcp The player - * @param needsSync Whether we're in an async thread - */ - public static void callLogoutEvent(DiscordConnectedPlayer dcp, boolean needsSync) { - if (!dcp.isLoggedIn()) return; - val event = new PlayerQuitEvent(dcp, ""); - if (needsSync) callEventSync(event); - else callEventExcludingSome(event); - dcp.setLoggedIn(false); - if (module != null) { - module.log(dcp.getName() + " (" + dcp.getUniqueId() + ") logged out from Discord"); - if (module.serverWatcher != null) - module.serverWatcher.fakePlayers.remove(dcp); - } - } - - static void callEventSync(Event event) { - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> callEventExcludingSome(event)); - } - - @RequiredArgsConstructor - public static class LastMsgData { - public Message message; - public long time; - public String content; - public final MessageChannel channel; - public buttondevteam.core.component.channel.Channel mcchannel; - public final User user; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java deleted file mode 100644 index 19cc441..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java +++ /dev/null @@ -1,187 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.discordplugin.*; -import buttondevteam.lib.TBMCSystemChatEvent; -import buttondevteam.lib.architecture.ConfigData; -import buttondevteam.lib.player.TBMCPlayer; -import buttondevteam.lib.player.TBMCPlayerBase; -import buttondevteam.lib.player.TBMCYEEHAWEvent; -import com.earth2me.essentials.CommandSource; -import discord4j.common.util.Snowflake; -import discord4j.core.object.entity.Role; -import lombok.val; -import net.ess3.api.events.AfkStatusChangeEvent; -import net.ess3.api.events.MuteStatusChangeEvent; -import net.ess3.api.events.NickChangeEvent; -import net.ess3.api.events.VanishStatusChangeEvent; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.entity.PlayerDeathEvent; -import org.bukkit.event.player.*; -import org.bukkit.event.player.PlayerLoginEvent.Result; -import org.bukkit.event.server.BroadcastMessageEvent; -import org.bukkit.event.server.TabCompleteEvent; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.util.Optional; - -class MCListener implements Listener { - private final MinecraftChatModule module; - private final ConfigData> muteRole; - - public MCListener(MinecraftChatModule module) { - this.module = module; - muteRole = DPUtils.roleData(module.getConfig(), "muteRole", "Muted"); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerLogin(PlayerLoginEvent e) { - if (e.getResult() != Result.ALLOWED) - return; - if (e.getPlayer() instanceof DiscordConnectedPlayer) - return; - var dcp = MCChatUtils.LoggedInPlayers.get(e.getPlayer().getUniqueId()); - if (dcp != null) - MCChatUtils.callLogoutEvent(dcp, false); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerJoin(PlayerJoinEvent e) { - if (e.getPlayer() instanceof DiscordConnectedPlayer) - return; // Don't show the joined message for the fake player - Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> { - final Player p = e.getPlayer(); - DiscordPlayer dp = TBMCPlayerBase.getPlayer(p.getUniqueId(), TBMCPlayer.class).getAs(DiscordPlayer.class); - if (dp != null) { - DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID())).flatMap(user -> user.getPrivateChannel().flatMap(chan -> module.chatChannelMono().flatMap(cc -> { - MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(), - DiscordPlayerSender.create(user, chan, p, module)); - MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(), - DiscordPlayerSender.create(user, cc, p, module)); //Stored per-channel - return Mono.empty(); - }))).subscribe(); - } - final String message = e.getJoinMessage(); - if (message != null && message.trim().length() > 0) - MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true).subscribe(); - ChromaBot.getInstance().updatePlayerList(); - }); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerLeave(PlayerQuitEvent e) { - if (e.getPlayer() instanceof DiscordConnectedPlayer) - return; // Only care about real users - MCChatUtils.OnlineSenders.entrySet() - .removeIf(entry -> entry.getValue().entrySet().stream().anyMatch(p -> p.getValue().getUniqueId().equals(e.getPlayer().getUniqueId()))); - Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, - () -> Optional.ofNullable(MCChatUtils.LoggedInPlayers.get(e.getPlayer().getUniqueId())).ifPresent(MCChatUtils::callLoginEvents)); - Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, - ChromaBot.getInstance()::updatePlayerList, 5); - final String message = e.getQuitMessage(); - if (message != null && message.trim().length() > 0) - MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true).subscribe(); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerKick(PlayerKickEvent e) { - /*if (!DiscordPlugin.hooked && !e.getReason().equals("The server is restarting") - && !e.getReason().equals("Server closed")) // The leave messages errored with the previous setup, I could make it wait since I moved it here, but instead I have a special - MCChatListener.forAllowedCustomAndAllMCChat(e.getPlayer().getName() + " left the game"); // message for this - Oh wait this doesn't even send normally because of the hook*/ - } - - @EventHandler(priority = EventPriority.LOW) - public void onPlayerDeath(PlayerDeathEvent e) { - MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(e.getDeathMessage()), e.getEntity(), ChannelconBroadcast.DEATH, true).subscribe(); - } - - @EventHandler - public void onPlayerAFK(AfkStatusChangeEvent e) { - final Player base = e.getAffected().getBase(); - if (e.isCancelled() || !base.isOnline()) - return; - final String msg = base.getDisplayName() - + " is " + (e.getValue() ? "now" : "no longer") + " AFK."; - MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(msg), base, ChannelconBroadcast.AFK, false).subscribe(); - } - - @EventHandler - public void onPlayerMute(MuteStatusChangeEvent e) { - final Mono role = muteRole.get(); - if (role == null) return; - final CommandSource source = e.getAffected().getSource(); - if (!source.isPlayer()) - return; - final DiscordPlayer p = TBMCPlayerBase.getPlayer(source.getPlayer().getUniqueId(), TBMCPlayer.class) - .getAs(DiscordPlayer.class); - if (p == null) return; - DPUtils.ignoreError(DiscordPlugin.dc.getUserById(Snowflake.of(p.getDiscordID())) - .flatMap(user -> user.asMember(DiscordPlugin.mainServer.getId())) - .flatMap(user -> role.flatMap(r -> { - if (e.getValue()) - user.addRole(r.getId()); - else - user.removeRole(r.getId()); - val modlog = module.modlogChannel.get(); - String msg = (e.getValue() ? "M" : "Unm") + "uted user: " + user.getUsername() + "#" + user.getDiscriminator(); - module.log(msg); - if (modlog != null) - return modlog.flatMap(ch -> ch.createMessage(msg)); - return Mono.empty(); - }))).subscribe(); - } - - @EventHandler - public void onChatSystemMessage(TBMCSystemChatEvent event) { - MCChatUtils.forAllowedMCChat(MCChatUtils.send(event.getMessage()), event).subscribe(); - } - - @EventHandler - public void onBroadcastMessage(BroadcastMessageEvent event) { - MCChatUtils.forCustomAndAllMCChat(MCChatUtils.send(event.getMessage()), ChannelconBroadcast.BROADCAST, false).subscribe(); - } - - @EventHandler - public void onYEEHAW(TBMCYEEHAWEvent event) { //TODO: Inherit from the chat event base to have channel support - String name = event.getSender() instanceof Player ? ((Player) event.getSender()).getDisplayName() - : event.getSender().getName(); - //Channel channel = ChromaGamerBase.getFromSender(event.getSender()).channel().get(); - TODO - DiscordPlugin.mainServer.getEmojis().filter(e -> "YEEHAW".equals(e.getName())) - .take(1).singleOrEmpty().map(Optional::of).defaultIfEmpty(Optional.empty()).flatMap(yeehaw -> - MCChatUtils.forPublicPrivateChat(MCChatUtils.send(name + (yeehaw.map(guildEmoji -> " <:YEEHAW:" + guildEmoji.getId().asString() + ">s").orElse(" YEEHAWs"))))).subscribe(); - } - - @EventHandler - public void onNickChange(NickChangeEvent event) { - MCChatUtils.updatePlayerList(); - } - - @EventHandler - public void onTabComplete(TabCompleteEvent event) { - int i = event.getBuffer().lastIndexOf(' '); - String t = event.getBuffer().substring(i + 1); //0 if not found - if (!t.startsWith("@")) - return; - String token = t.substring(1); - val x = DiscordPlugin.mainServer.getMembers() - .flatMap(m -> Flux.just(m.getUsername(), m.getNickname().orElse(""))) - .filter(s -> s.startsWith(token)) - .map(s -> "@" + s) - .doOnNext(event.getCompletions()::add).blockLast(); - } - - @EventHandler - public void onCommandSend(PlayerCommandSendEvent event) { - event.getCommands().add("g"); - } - - @EventHandler - public void onVanish(VanishStatusChangeEvent event) { - if (event.isCancelled()) return; - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, MCChatUtils::updatePlayerList); - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MinecraftChatModule.java b/src/main/java/buttondevteam/discordplugin/mcchat/MinecraftChatModule.java deleted file mode 100644 index 243cf83..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MinecraftChatModule.java +++ /dev/null @@ -1,261 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.component.channel.Channel; -import buttondevteam.discordplugin.ChannelconBroadcast; -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordConnectedPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.playerfaker.ServerWatcher; -import buttondevteam.discordplugin.playerfaker.perm.LPInjector; -import buttondevteam.discordplugin.util.DPState; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.TBMCSystemChatEvent; -import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.architecture.ConfigData; -import buttondevteam.lib.architecture.ReadOnlyConfigData; -import com.google.common.collect.Lists; -import discord4j.common.util.Snowflake; -import discord4j.core.object.entity.channel.MessageChannel; -import discord4j.rest.util.Color; -import lombok.Getter; -import lombok.val; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import reactor.core.publisher.Mono; - -import java.util.ArrayList; -import java.util.Objects; -import java.util.UUID; -import java.util.stream.Collectors; - -/** - * Provides Minecraft chat connection to Discord. Commands may be used either in a public chat (limited) or in a DM. - */ -public class MinecraftChatModule extends Component { - public static DPState state = DPState.RUNNING; - private @Getter MCChatListener listener; - ServerWatcher serverWatcher; - private LPInjector lpInjector; - boolean disabling = false; - - /** - * A list of commands that can be used in public chats - Warning: Some plugins will treat players as OPs, always test before allowing a command! - */ - public ConfigData> whitelistedCommands() { - return getConfig().getData("whitelistedCommands", () -> Lists.newArrayList("list", "u", "shrug", "tableflip", "unflip", "mwiki", - "yeehaw", "lenny", "rp", "plugins")); - } - - /** - * The channel to use as the public Minecraft chat - everything public gets broadcasted here - */ - public ReadOnlyConfigData chatChannel = DPUtils.snowflakeData(getConfig(), "chatChannel", 0L); - - public Mono chatChannelMono() { - return DPUtils.getMessageChannel(chatChannel.getPath(), chatChannel.get()); - } - - /** - * The channel where the plugin can log when it mutes a player on Discord because of a Minecraft mute - */ - public ReadOnlyConfigData> modlogChannel = DPUtils.channelData(getConfig(), "modlogChannel"); - - /** - * The plugins to exclude from fake player events used for the 'mcchat' command - some plugins may crash, add them here - */ - public ConfigData excludedPlugins = getConfig().getData("excludedPlugins", new String[]{"ProtocolLib", "LibsDisguises", "JourneyMapServer"}); - - /** - * If this setting is on then players logged in through the 'mcchat' command will be able to teleport using plugin commands. - * They can then use commands like /tpahere to teleport others to that place.
- * If this is off, then teleporting will have no effect. - */ - public ConfigData allowFakePlayerTeleports = getConfig().getData("allowFakePlayerTeleports", false); - - /** - * If this is on, each chat channel will have a player list in their description. - * It only gets added if there's no description yet or there are (at least) two lines of "----" following each other. - * Note that it will replace everything above the first and below the last "----" but it will only detect exactly four dashes. - * So if you want to use dashes for something else in the description, make sure it's either less or more dashes in one line. - */ - public ConfigData showPlayerListOnDC = getConfig().getData("showPlayerListOnDC", true); - - /** - * This setting controls whether custom chat connections can be created (existing connections will always work). - * Custom chat connections can be created using the channelcon command and they allow players to display town chat in a Discord channel for example. - * See the channelcon command for more details. - */ - public ConfigData allowCustomChat = getConfig().getData("allowCustomChat", true); - - /** - * This setting allows you to control if players can DM the bot to log on the server from Discord. - * This allows them to both chat and perform any command they can in-game. - */ - public ConfigData allowPrivateChat = getConfig().getData("allowPrivateChat", true); - - /** - * If set, message authors appearing on Discord will link to this URL. A 'type' and 'id' parameter will be added with the user's platform (Discord, Minecraft, ...) and ID. - */ - public ConfigData profileURL = getConfig().getData("profileURL", ""); - - /** - * Enables support for running vanilla commands through Discord, if you ever need it. - */ - public ConfigData enableVanillaCommands = getConfig().getData("enableVanillaCommands", true); - - /** - * Whether players logged on from Discord (mcchat command) should be recognised by other plugins. Some plugins might break if it's turned off. - * But it's really hacky. - */ - private final ConfigData addFakePlayersToBukkit = getConfig().getData("addFakePlayersToBukkit", false); - - /** - * Set by the component to report crashes. - */ - private final ConfigData serverUp = getConfig().getData("serverUp", false); - - private final MCChatCommand mcChatCommand = new MCChatCommand(this); - private final ChannelconCommand channelconCommand = new ChannelconCommand(this); - - @Override - protected void enable() { - if (DPUtils.disableIfConfigErrorRes(this, chatChannel, chatChannelMono())) - return; - listener = new MCChatListener(this); - TBMCCoreAPI.RegisterEventsForExceptions(listener, getPlugin()); - TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(this), getPlugin());//These get undone if restarting/resetting - it will ignore events if disabled - getPlugin().getManager().registerCommand(mcChatCommand); - getPlugin().getManager().registerCommand(channelconCommand); - - val chcons = getConfig().getConfig().getConfigurationSection("chcons"); - if (chcons == null) //Fallback to old place - getConfig().getConfig().getRoot().getConfigurationSection("chcons"); - if (chcons != null) { - val chconkeys = chcons.getKeys(false); - for (val chconkey : chconkeys) { - val chcon = chcons.getConfigurationSection(chconkey); - val mcch = Channel.getChannels().filter(ch -> ch.ID.equals(chcon.getString("mcchid"))).findAny(); - val ch = DiscordPlugin.dc.getChannelById(Snowflake.of(chcon.getLong("chid"))).block(); - val did = chcon.getLong("did"); - val user = DiscordPlugin.dc.getUserById(Snowflake.of(did)).block(); - val groupid = chcon.getString("groupid"); - val toggles = chcon.getInt("toggles"); - val brtoggles = chcon.getStringList("brtoggles"); - if (!mcch.isPresent() || ch == null || user == null || groupid == null) - continue; - Bukkit.getScheduler().runTask(getPlugin(), () -> { //<-- Needed because of occasional ConcurrentModificationExceptions when creating the player (PermissibleBase) - val dcp = DiscordConnectedPlayer.create(user, (MessageChannel) ch, UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname"), this); - MCChatCustom.addCustomChat((MessageChannel) ch, groupid, mcch.get(), user, dcp, toggles, brtoggles.stream().map(TBMCSystemChatEvent.BroadcastTarget::get).filter(Objects::nonNull).collect(Collectors.toSet())); - }); - } - } - - try { - if (lpInjector == null) - lpInjector = new LPInjector(DiscordPlugin.plugin); - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to init LuckPerms injector", e, this); - } catch (NoClassDefFoundError e) { - log("No LuckPerms, not injecting"); - //e.printStackTrace(); - } - - if (addFakePlayersToBukkit.get()) { - try { - serverWatcher = new ServerWatcher(); - serverWatcher.enableDisable(true); - log("Finished hooking into the server"); - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to hack the server (object)! Disable addFakePlayersToBukkit in the config.", e, this); - } - } - - if (state == DPState.RESTARTING_PLUGIN) { //These will only execute if the chat is enabled - sendStateMessage(Color.CYAN, "Discord plugin restarted - chat connected."); //Really important to note the chat, hmm - state = DPState.RUNNING; - } else if (state == DPState.DISABLED_MCCHAT) { - sendStateMessage(Color.CYAN, "Minecraft chat enabled - chat connected."); - state = DPState.RUNNING; - } else if (serverUp.get()) { - sendStateMessage(Color.YELLOW, "Server started after a crash - chat connected."); - val thr = new Throwable("The server shut down unexpectedly. See the log of the previous run for more details."); - thr.setStackTrace(new StackTraceElement[0]); - TBMCCoreAPI.SendException("The server crashed!", thr, this); - } else - sendStateMessage(Color.GREEN, "Server started - chat connected."); - serverUp.set(true); - } - - @Override - protected void disable() { - disabling = true; - if (state == DPState.RESTARTING_PLUGIN) //These will only execute if the chat is enabled - sendStateMessage(Color.ORANGE, "Discord plugin restarting"); - else if (state == DPState.RUNNING) { - sendStateMessage(Color.ORANGE, "Minecraft chat disabled"); - state = DPState.DISABLED_MCCHAT; - } else { - String kickmsg = Bukkit.getOnlinePlayers().size() > 0 - ? (DPUtils - .sanitizeString(Bukkit.getOnlinePlayers().stream() - .map(Player::getDisplayName).collect(Collectors.joining(", "))) - + (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ") - + "thrown out") //TODO: Make configurable - : ""; - if (state == DPState.RESTARTING_SERVER) - sendStateMessage(Color.ORANGE, "Server restarting", kickmsg); - else if (state == DPState.STOPPING_SERVER) - sendStateMessage(Color.RED, "Server stopping", kickmsg); - else - sendStateMessage(Color.GRAY, "Unknown state, please report."); - } //If 'restart' is disabled then this isn't shown even if joinleave is enabled - - serverUp.set(false); //Disable even if just the component is disabled because that way it won't falsely report crashes - - try { //If it's not enabled it won't do anything - if (serverWatcher != null) { - serverWatcher.enableDisable(false); - log("Finished unhooking the server"); - } - } catch ( - Exception e) { - TBMCCoreAPI.SendException("Failed to restore the server object!", e, this); - } - - val chcons = MCChatCustom.getCustomChats(); - val chconsc = getConfig().getConfig().createSection("chcons"); - for ( - val chcon : chcons) { - val chconc = chconsc.createSection(chcon.channel.getId().asString()); - chconc.set("mcchid", chcon.mcchannel.ID); - chconc.set("chid", chcon.channel.getId().asLong()); - chconc.set("did", chcon.user.getId().asLong()); - chconc.set("mcuid", chcon.dcp.getUniqueId().toString()); - chconc.set("mcname", chcon.dcp.getName()); - chconc.set("groupid", chcon.groupID); - chconc.set("toggles", chcon.toggles); - chconc.set("brtoggles", chcon.brtoggles.stream().map(TBMCSystemChatEvent.BroadcastTarget::getName).collect(Collectors.toList())); - } - if (listener != null) //Can be null if disabled because of a config error - listener.stop(true); - getPlugin().getManager().unregisterCommand(mcChatCommand); - getPlugin().getManager().unregisterCommand(channelconCommand); - disabling = false; - } - - /** - * It will block to make sure all messages are sent - */ - private void sendStateMessage(Color color, String message) { - MCChatUtils.forCustomAndAllMCChat(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(color) - .setTitle(message))), ChannelconBroadcast.RESTART, false).block(); - } - - /** - * It will block to make sure all messages are sent - */ - private void sendStateMessage(Color color, String message, String extra) { - MCChatUtils.forCustomAndAllMCChat(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(color) - .setTitle(message).setDescription(extra)).onErrorResume(t -> Mono.empty())), ChannelconBroadcast.RESTART, false).block(); - } -} 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 35e78a7..0000000 --- a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java +++ /dev/null @@ -1,148 +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.commands.ConnectCommand; -import buttondevteam.discordplugin.commands.VersionCommand; -import buttondevteam.discordplugin.mcchat.MCChatUtils; -import buttondevteam.discordplugin.mcchat.MinecraftChatModule; -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/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/ServerWatcher.java b/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java deleted file mode 100644 index 903cea3..0000000 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java +++ /dev/null @@ -1,97 +0,0 @@ -package buttondevteam.discordplugin.playerfaker; - -import buttondevteam.discordplugin.mcchat.MCChatUtils; -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/VCMDWrapper.java b/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java deleted file mode 100644 index 6ff856b..0000000 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java +++ /dev/null @@ -1,66 +0,0 @@ -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; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import javax.annotation.Nullable; - -@RequiredArgsConstructor -public class VCMDWrapper { - @Getter //Needed to mock the player - @Nullable - private final Object listener; - - /** - * This constructor will only send raw vanilla messages to the sender in plain text. - * - * @param player The Discord sender player (the wrapper) - */ - public static > Object createListener(T player, MinecraftChatModule module) { - return createListener(player, null, module); - } - - /** - * This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player. - * - * @param player The Discord sender player (the wrapper) - * @param bukkitplayer The Bukkit player to send the raw message to - * @param module The Minecraft chat module - */ - public static > Object createListener(T player, Player bukkitplayer, MinecraftChatModule module) { - try { - Object ret; - String mcpackage = Bukkit.getServer().getClass().getPackage().getName(); - if (mcpackage.contains("1_12")) - ret = new VanillaCommandListener<>(player, bukkitplayer); - else if (mcpackage.contains("1_14")) - ret = new VanillaCommandListener14<>(player, bukkitplayer); - else if (mcpackage.contains("1_15") || mcpackage.contains("1_16")) - ret = VanillaCommandListener15.create(player, bukkitplayer); //bukkitplayer may be null but that's fine - else - ret = null; - if (ret == null) - compatWarning(module); - return ret; - } catch (NoClassDefFoundError | Exception e) { - compatWarning(module); - TBMCCoreAPI.SendException("Failed to create vanilla command listener", e, module); - return null; - } - } - - private static void compatWarning(MinecraftChatModule module) { - module.logWarn("Vanilla commands won't be available from Discord due to a compatibility error. Disable vanilla command support to remove this message."); - } - - static boolean compatResponse(DiscordSenderBase dsender) { - dsender.sendMessage("Vanilla commands are not supported on this Minecraft version."); - return true; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener.java b/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener.java deleted file mode 100755 index 18b3618..0000000 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener.java +++ /dev/null @@ -1,102 +0,0 @@ -package buttondevteam.discordplugin.playerfaker; - -import buttondevteam.discordplugin.DiscordSenderBase; -import buttondevteam.discordplugin.IMCPlayer; -import lombok.Getter; -import lombok.val; -import net.minecraft.server.v1_12_R1.*; -import org.bukkit.Bukkit; -import org.bukkit.craftbukkit.v1_12_R1.CraftServer; -import org.bukkit.craftbukkit.v1_12_R1.CraftWorld; -import org.bukkit.craftbukkit.v1_12_R1.command.VanillaCommandWrapper; -import org.bukkit.craftbukkit.v1_12_R1.entity.CraftPlayer; -import org.bukkit.entity.Player; - -import java.util.Arrays; - -public class VanillaCommandListener> implements ICommandListener { - private @Getter T player; - private Player bukkitplayer; - - /** - * This constructor will only send raw vanilla messages to the sender in plain text. - * - * @param player The Discord sender player (the wrapper) - */ - public VanillaCommandListener(T player) { - this.player = player; - this.bukkitplayer = null; - } - - /** - * This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player. - * - * @param player The Discord sender player (the wrapper) - * @param bukkitplayer The Bukkit player to send the raw message to - */ - public VanillaCommandListener(T player, Player bukkitplayer) { - this.player = player; - this.bukkitplayer = bukkitplayer; - if (bukkitplayer != null && !(bukkitplayer instanceof CraftPlayer)) - throw new ClassCastException("bukkitplayer must be a Bukkit player!"); - } - - @Override - public MinecraftServer C_() { - return ((CraftServer) Bukkit.getServer()).getServer(); - } - - @Override - public boolean a(int oplevel, String cmd) { - // return oplevel <= 2; // Value from CommandBlockListenerAbstract, found what it is in EntityPlayer - Wait, that'd always allow OP commands - return oplevel == 0 || player.isOp(); - } - - @Override - public String getName() { - return player.getName(); - } - - @Override - public World getWorld() { - return ((CraftWorld) player.getWorld()).getHandle(); - } - - @Override - public void sendMessage(IChatBaseComponent arg0) { - player.sendMessage(arg0.toPlainText()); - if (bukkitplayer != null) - ((CraftPlayer) bukkitplayer).getHandle().sendMessage(arg0); - } - - public static boolean runBukkitOrVanillaCommand(DiscordSenderBase dsender, String cmdstr) { - val cmd = ((CraftServer) Bukkit.getServer()).getCommandMap().getCommand(cmdstr.split(" ")[0].toLowerCase()); - if (!(dsender instanceof Player) || !(cmd instanceof VanillaCommandWrapper)) - return Bukkit.dispatchCommand(dsender, cmdstr); // Unconnected users are treated well in vanilla cmds - - if (!(dsender instanceof IMCPlayer)) - throw new ClassCastException( - "dsender needs to implement IMCPlayer to use vanilla commands as it implements Player."); - - IMCPlayer sender = (IMCPlayer) dsender; // Don't use val on recursive interfaces :P - - val vcmd = (VanillaCommandWrapper) cmd; - if (!vcmd.testPermission(sender)) - return true; - - ICommandListener icommandlistener = (ICommandListener) sender.getVanillaCmdListener().getListener(); - if (icommandlistener == null) - return VCMDWrapper.compatResponse(dsender); - String[] args = cmdstr.split(" "); - args = Arrays.copyOfRange(args, 1, args.length); - try { - vcmd.dispatchVanillaCommand(sender, icommandlistener, args); - } catch (CommandException commandexception) { - // Taken from CommandHandler - ChatMessage chatmessage = new ChatMessage(commandexception.getMessage(), commandexception.getArgs()); - chatmessage.getChatModifier().setColor(EnumChatFormat.RED); - icommandlistener.sendMessage(chatmessage); - } - return true; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener14.java b/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener14.java deleted file mode 100644 index 7a6df61..0000000 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener14.java +++ /dev/null @@ -1,108 +0,0 @@ -package buttondevteam.discordplugin.playerfaker; - -import buttondevteam.discordplugin.DiscordSenderBase; -import buttondevteam.discordplugin.IMCPlayer; -import lombok.Getter; -import lombok.val; -import net.minecraft.server.v1_14_R1.*; -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.craftbukkit.v1_14_R1.CraftServer; -import org.bukkit.craftbukkit.v1_14_R1.CraftWorld; -import org.bukkit.craftbukkit.v1_14_R1.command.ProxiedNativeCommandSender; -import org.bukkit.craftbukkit.v1_14_R1.command.VanillaCommandWrapper; -import org.bukkit.craftbukkit.v1_14_R1.entity.CraftPlayer; -import org.bukkit.entity.Player; - -import java.util.Arrays; - -public class VanillaCommandListener14> implements ICommandListener { - private @Getter T player; - private Player bukkitplayer; - - /** - * This constructor will only send raw vanilla messages to the sender in plain text. - * - * @param player The Discord sender player (the wrapper) - */ - public VanillaCommandListener14(T player) { - this.player = player; - this.bukkitplayer = null; - } - - /** - * This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player. - * - * @param player The Discord sender player (the wrapper) - * @param bukkitplayer The Bukkit player to send the raw message to - */ - public VanillaCommandListener14(T player, Player bukkitplayer) { - this.player = player; - this.bukkitplayer = bukkitplayer; - if (bukkitplayer != null && !(bukkitplayer instanceof CraftPlayer)) - throw new ClassCastException("bukkitplayer must be a Bukkit player!"); - } - - @Override - public void sendMessage(IChatBaseComponent arg0) { - player.sendMessage(arg0.getString()); - if (bukkitplayer != null) - ((CraftPlayer) bukkitplayer).getHandle().sendMessage(arg0); - } - - @Override - public boolean shouldSendSuccess() { - return true; - } - - @Override - public boolean shouldSendFailure() { - return true; - } - - @Override - public boolean shouldBroadcastCommands() { - return true; //Broadcast to in-game admins - } - - @Override - public CommandSender getBukkitSender(CommandListenerWrapper commandListenerWrapper) { - return player; - } - - public static boolean runBukkitOrVanillaCommand(DiscordSenderBase dsender, String cmdstr) { - val cmd = ((CraftServer) Bukkit.getServer()).getCommandMap().getCommand(cmdstr.split(" ")[0].toLowerCase()); - if (!(dsender instanceof Player) || !(cmd instanceof VanillaCommandWrapper)) - return Bukkit.dispatchCommand(dsender, cmdstr); // Unconnected users are treated well in vanilla cmds - - if (!(dsender instanceof IMCPlayer)) - throw new ClassCastException( - "dsender needs to implement IMCPlayer to use vanilla commands as it implements Player."); - - IMCPlayer sender = (IMCPlayer) dsender; // Don't use val on recursive interfaces :P - - val vcmd = (VanillaCommandWrapper) cmd; - if (!vcmd.testPermission(sender)) - return true; - - val world = ((CraftWorld) Bukkit.getWorlds().get(0)).getHandle(); - ICommandListener icommandlistener = (ICommandListener) sender.getVanillaCmdListener().getListener(); - if (icommandlistener == null) - return VCMDWrapper.compatResponse(dsender); - val wrapper = new CommandListenerWrapper(icommandlistener, new Vec3D(0, 0, 0), - new Vec2F(0, 0), world, 0, sender.getName(), - new ChatComponentText(sender.getName()), world.getMinecraftServer(), null); - val pncs = new ProxiedNativeCommandSender(wrapper, sender, sender); - String[] args = cmdstr.split(" "); - args = Arrays.copyOfRange(args, 1, args.length); - try { - return vcmd.execute(pncs, cmd.getLabel(), args); - } catch (CommandException commandexception) { - // Taken from CommandHandler - ChatMessage chatmessage = new ChatMessage(commandexception.getMessage(), commandexception.a()); - chatmessage.getChatModifier().setColor(EnumChatFormat.RED); - icommandlistener.sendMessage(chatmessage); - } - return true; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener15.java b/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener15.java deleted file mode 100644 index 66dc935..0000000 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener15.java +++ /dev/null @@ -1,138 +0,0 @@ -package buttondevteam.discordplugin.playerfaker; - -import buttondevteam.discordplugin.DiscordSenderBase; -import buttondevteam.discordplugin.IMCPlayer; -import lombok.Getter; -import lombok.val; -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.command.SimpleCommandMap; -import org.bukkit.entity.Player; -import org.mockito.Answers; -import org.mockito.Mockito; - -import java.lang.reflect.Modifier; -import java.util.Arrays; - -/** - * Same as {@link VanillaCommandListener14} but with reflection - */ -public class VanillaCommandListener15> { - private @Getter T player; - private static Class vcwcl; - private static String nms; - - protected VanillaCommandListener15(T player, Player bukkitplayer) { - this.player = player; - if (bukkitplayer != null && !bukkitplayer.getClass().getSimpleName().endsWith("CraftPlayer")) - throw new ClassCastException("bukkitplayer must be a Bukkit player!"); - } - - /** - * This method will only send raw vanilla messages to the sender in plain text. - * - * @param player The Discord sender player (the wrapper) - */ - public static > VanillaCommandListener15 create(T player) throws Exception { - return create(player, null); - } - - /** - * This method will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player. - * - * @param player The Discord sender player (the wrapper) - * @param bukkitplayer The Bukkit player to send the raw message to - */ - @SuppressWarnings("unchecked") - public static > VanillaCommandListener15 create(T player, Player bukkitplayer) throws Exception { - if (vcwcl == null) { - String pkg = Bukkit.getServer().getClass().getPackage().getName(); - vcwcl = Class.forName(pkg + ".command.VanillaCommandWrapper"); - } - if (nms == null) { - var server = Bukkit.getServer(); - nms = server.getClass().getMethod("getServer").invoke(server).getClass().getPackage().getName(); //org.mockito.codegen - } - var iclcl = Class.forName(nms + ".ICommandListener"); - return Mockito.mock(VanillaCommandListener15.class, Mockito.withSettings().stubOnly() - .useConstructor(player, bukkitplayer).extraInterfaces(iclcl).defaultAnswer(invocation -> { - if (invocation.getMethod().getName().equals("sendMessage")) { - var icbc = invocation.getArgument(0); - player.sendMessage((String) icbc.getClass().getMethod("getString").invoke(icbc)); - if (bukkitplayer != null) { - var handle = bukkitplayer.getClass().getMethod("getHandle").invoke(bukkitplayer); - handle.getClass().getMethod("sendMessage", icbc.getClass()).invoke(handle, icbc); - } - return null; - } - if (!Modifier.isAbstract(invocation.getMethod().getModifiers())) - return invocation.callRealMethod(); - if (invocation.getMethod().getReturnType() == boolean.class) - return true; //shouldSend... shouldBroadcast... - if (invocation.getMethod().getReturnType() == CommandSender.class) - return player; - return Answers.RETURNS_DEFAULTS.answer(invocation); - })); - } - - public static boolean runBukkitOrVanillaCommand(DiscordSenderBase dsender, String cmdstr) throws Exception { - var server = Bukkit.getServer(); - var cmap = (SimpleCommandMap) server.getClass().getMethod("getCommandMap").invoke(server); - val cmd = cmap.getCommand(cmdstr.split(" ")[0].toLowerCase()); - if (!(dsender instanceof Player) || cmd == null || !vcwcl.isAssignableFrom(cmd.getClass())) - return Bukkit.dispatchCommand(dsender, cmdstr); // Unconnected users are treated well in vanilla cmds - - if (!(dsender instanceof IMCPlayer)) - throw new ClassCastException( - "dsender needs to implement IMCPlayer to use vanilla commands as it implements Player."); - - IMCPlayer sender = (IMCPlayer) dsender; // Don't use val on recursive interfaces :P - - if (!(Boolean) vcwcl.getMethod("testPermission", CommandSender.class).invoke(cmd, sender)) - return true; - - var cworld = Bukkit.getWorlds().get(0); - val world = cworld.getClass().getMethod("getHandle").invoke(cworld); - var icommandlistener = sender.getVanillaCmdListener().getListener(); - if (icommandlistener == null) - return VCMDWrapper.compatResponse(dsender); - var clwcl = Class.forName(nms + ".CommandListenerWrapper"); - var v3dcl = Class.forName(nms + ".Vec3D"); - var v2fcl = Class.forName(nms + ".Vec2F"); - var icbcl = Class.forName(nms + ".IChatBaseComponent"); - var mcscl = Class.forName(nms + ".MinecraftServer"); - var ecl = Class.forName(nms + ".Entity"); - var cctcl = Class.forName(nms + ".ChatComponentText"); - var iclcl = Class.forName(nms + ".ICommandListener"); - Object wrapper = clwcl.getConstructor(iclcl, v3dcl, v2fcl, world.getClass(), int.class, String.class, icbcl, mcscl, ecl) - .newInstance(icommandlistener, - v3dcl.getConstructor(double.class, double.class, double.class).newInstance(0, 0, 0), - v2fcl.getConstructor(float.class, float.class).newInstance(0, 0), - world, 0, sender.getName(), cctcl.getConstructor(String.class).newInstance(sender.getName()), - world.getClass().getMethod("getMinecraftServer").invoke(world), null); - /*val wrapper = new CommandListenerWrapper(icommandlistener, new Vec3D(0, 0, 0), - new Vec2F(0, 0), world, 0, sender.getName(), - new ChatComponentText(sender.getName()), world.getMinecraftServer(), null);*/ - var pncscl = Class.forName(vcwcl.getPackage().getName() + ".ProxiedNativeCommandSender"); - Object pncs = pncscl.getConstructor(clwcl, CommandSender.class, CommandSender.class) - .newInstance(wrapper, sender, sender); - String[] args = cmdstr.split(" "); - args = Arrays.copyOfRange(args, 1, args.length); - try { - return cmd.execute((CommandSender) pncs, cmd.getLabel(), args); - } catch (Exception commandexception) { - if (!commandexception.getClass().getSimpleName().equals("CommandException")) - throw commandexception; - // Taken from CommandHandler - var cmcl = Class.forName(nms + ".ChatMessage"); - var chatmessage = cmcl.getConstructor(String.class, Object[].class) - .newInstance(commandexception.getMessage(), - new Object[]{commandexception.getClass().getMethod("a").invoke(commandexception)}); - var modifier = cmcl.getMethod("getChatModifier").invoke(chatmessage); - var ecfcl = Class.forName(nms + ".EnumChatFormat"); - modifier.getClass().getMethod("setColor", ecfcl).invoke(modifier, ecfcl.getField("RED").get(null)); - icommandlistener.getClass().getMethod("sendMessage", icbcl).invoke(icommandlistener, chatmessage); - } - return true; - } -} 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/RoleCommand.java b/src/main/java/buttondevteam/discordplugin/role/RoleCommand.java deleted file mode 100755 index 07fd0e2..0000000 --- a/src/main/java/buttondevteam/discordplugin/role/RoleCommand.java +++ /dev/null @@ -1,109 +0,0 @@ -package buttondevteam.discordplugin.role; - -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.commands.Command2DCSender; -import buttondevteam.discordplugin.commands.ICommand2DC; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -import discord4j.core.object.entity.Role; -import lombok.val; -import 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/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/Timings.java b/src/main/java/buttondevteam/discordplugin/util/Timings.java deleted file mode 100644 index 12c12f2..0000000 --- a/src/main/java/buttondevteam/discordplugin/util/Timings.java +++ /dev/null @@ -1,16 +0,0 @@ -package buttondevteam.discordplugin.util; - -import buttondevteam.discordplugin.listeners.CommonListeners; - -public class Timings { - private long start; - - public Timings() { - start = System.nanoTime(); - } - - public void printElapsed(String message) { - CommonListeners.debug(message + " (" + (System.nanoTime() - start) / 1000000L + ")"); - start = System.nanoTime(); - } -} diff --git a/src/main/scala/buttondevteam/discordplugin/BukkitLogWatcher.scala b/src/main/scala/buttondevteam/discordplugin/BukkitLogWatcher.scala new file mode 100644 index 0000000..ecec820 --- /dev/null +++ b/src/main/scala/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.appender.AbstractAppender +import org.apache.logging.log4j.core.filter.LevelRangeFilter +import org.apache.logging.log4j.core.layout.PatternLayout +import org.apache.logging.log4j.core.{Filter, LogEvent} + +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/scala/buttondevteam/discordplugin/ChannelconBroadcast.scala b/src/main/scala/buttondevteam/discordplugin/ChannelconBroadcast.scala new file mode 100644 index 0000000..317a759 --- /dev/null +++ b/src/main/scala/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/scala/buttondevteam/discordplugin/ChromaBot.scala new file mode 100644 index 0000000..af6ae9f --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/ChromaBot.scala @@ -0,0 +1,37 @@ +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 +import reactor.core.scala.publisher.SMono + +import javax.annotation.Nullable + +object ChromaBot { + private var _enabled = false + + def enabled = _enabled + + private[discordplugin] def enabled_=(en: Boolean): Unit = _enabled = en + + /** + * Send a message to the chat channels and private chats. + * + * @param message The message to send, duh (use [[MessageChannel.createMessage]]) + */ + def sendMessage(message: SMono[MessageChannel] => SMono[Message]): Unit = + MCChatUtils.forPublicPrivateChat(message).subscribe() + + /** + * Send a message to the chat channels, private chats and custom chats. + * + * @param message The message to send, duh + * @param toggle The toggle type for channelcon + */ + def sendMessageCustomAsWell(message: SMono[MessageChannel] => SMono[Message], @Nullable toggle: ChannelconBroadcast): Unit = + MCChatUtils.forCustomAndAllMCChat(message.apply, toggle, hookmsg = false).subscribe() + + def updatePlayerList(): Unit = + MCChatUtils.updatePlayerList() +} diff --git a/src/main/scala/buttondevteam/discordplugin/DPUtils.scala b/src/main/scala/buttondevteam/discordplugin/DPUtils.scala new file mode 100644 index 0000000..be512b0 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/DPUtils.scala @@ -0,0 +1,217 @@ +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.legacy.{LegacyEmbedCreateSpec, LegacySpec} +import reactor.core.publisher.{Flux, Mono} +import reactor.core.scala.publisher.{SFlux, SMono} + +import java.util +import java.util.Comparator +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: LegacyEmbedCreateSpec, displayname: String, playername: String, profileUrl: String): LegacyEmbedCreateSpec = + 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) + val sb = new StringBuffer + while (matcher.find) matcher.appendReplacement(sb, if (Option(ts.floor(Array[Int](matcher.start, 0))).map( //Find a URL start <= our start + (a: Array[Int]) => a(1)).getOrElse(-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) Logger.getLogger("DiscordPlugin") + else DiscordPlugin.plugin.getLogger + } + + def channelData(config: IHaveConfig, key: String): ReadOnlyConfigData[SMono[MessageChannel]] = + config.getReadOnlyDataPrimDef(key, 0L, (id: Any) => + getMessageChannel(key, Snowflake.of(id.asInstanceOf[Long])), (_: SMono[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[SMono[Role]] = + roleData(config, key, defName, SMono.just(DiscordPlugin.mainServer)) + + /** + * Needs to be a [[ConfigData]] for checking if it's set + */ + def roleData(config: IHaveConfig, key: String, defName: String, guild: SMono[Guild]): ReadOnlyConfigData[SMono[Role]] = config.getReadOnlyDataPrimDef(key, defName, (name: Any) => { + def foo(name: Any): SMono[Role] = { + if (!name.isInstanceOf[String] || name.asInstanceOf[String].isEmpty) return SMono.empty[Role] + guild.flatMapMany(_.getRoles).filter((r: Role) => r.getName == name).onErrorResume((e: Throwable) => { + def foo(e: Throwable): SMono[Role] = { + getLogger.warning("Failed to get role data for " + key + "=" + name + " - " + e.getMessage) + SMono.empty[Role] + } + + foo(e) + }).next + } + + foo(name) + }, (_: SMono[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[SMono[_]] && !result.asInstanceOf[SMono[_]].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 SMono.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): SMono[Message] = { + val ch = if (channel == null) SMono(original.getChannel) + else SMono.just(channel) + reply(original, ch, message) + } + + /** + * @see #reply(Message, MessageChannel, String) + */ + def reply(original: Message, ch: SMono[MessageChannel], message: String): SMono[Message] = + ch.flatMap(channel => SMono(channel.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): SMono[MessageChannel] = { + if (id.asLong == 0L) return SMono.empty[MessageChannel] + + SMono(DiscordPlugin.dc.getChannelById(id)).onErrorResume(e => { + def foo(e: Throwable) = { + getLogger.warning("Failed to get channel data for " + key + "=" + id + " - " + e.getMessage) + SMono.empty + } + + foo(e) + }).filter(ch => ch.isInstanceOf[MessageChannel]).cast[MessageChannel] + } + + def getMessageChannel(config: ConfigData[Snowflake]): SMono[MessageChannel] = + getMessageChannel(config.getPath, config.get) + + def ignoreError[T](mono: SMono[T]): SMono[T] = mono.onErrorResume((_: Throwable) => SMono.empty) + + implicit class MonoExtensions[T](mono: Mono[T]) { + def ^^(): SMono[T] = SMono(mono) + } + + implicit class FluxExtensions[T](flux: Flux[T]) { + def ^^(): SFlux[T] = SFlux(flux) + } + + implicit class SpecExtensions[T <: LegacySpec[_]](spec: T) { + def ^^(): Unit = () + } + +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/DiscordConnectedPlayer.scala b/src/main/scala/buttondevteam/discordplugin/DiscordConnectedPlayer.scala new file mode 100644 index 0000000..71585e4 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/DiscordConnectedPlayer.scala @@ -0,0 +1,233 @@ +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 org.bukkit.* +import org.bukkit.attribute.{Attribute, AttributeInstance, AttributeModifier} +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.invocation.InvocationOnMock +import org.mockito.{MockSettings, Mockito} + +import java.lang.reflect.Modifier +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 +} + +/** + * @constructor The parameters must match with [[DiscordConnectedPlayer.create]] + * @param user May be null. + * @param channel May not be null. + * @param uniqueId The UUID of the player. + * @param name The Minecraft name of the player. + * @param module The MinecraftChatModule or null if testing. + */ +abstract class DiscordConnectedPlayer(user: User, channel: MessageChannel, val uniqueId: UUID, val name: String, val module: MinecraftChatModule) extends DiscordSenderBase(user, channel) with IMCPlayer[DiscordConnectedPlayer] { + private var loggedIn = false + private var displayName: String = name + + private var location: Location = if (module == null) null else Bukkit.getWorlds.get(0).getSpawnLocation + private val basePlayer: OfflinePlayer = if (module == null) null else Bukkit.getOfflinePlayer(uniqueId) + private var perm: PermissibleBase = if (module == null) null else new PermissibleBase(basePlayer) + private val origPerm: PermissibleBase = perm + private val vanillaCmdListener: VCMDWrapper = if (module == null) null else new VCMDWrapper(VCMDWrapper.createListener(this, module)) + + 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 + + /** + * For testing + */ + def this(user: User, channel: MessageChannel) = + this(user, channel, UUID.randomUUID(), "Test", null) + + 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 = 20d + + 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")) override def spigot: super.Spigot = new super.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*): Unit = + sendMessage(components: _*) + + override def isInvulnerable = true + }*/ +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/DiscordPlayer.scala b/src/main/scala/buttondevteam/discordplugin/DiscordPlayer.scala new file mode 100644 index 0000000..4cfca44 --- /dev/null +++ b/src/main/scala/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 + * [[MCChatPrivate.privateMCChat]] + */ + def isMinecraftChatEnabled: Boolean = MCChatPrivate.isMinecraftChatEnabled(this) +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/DiscordPlayerSender.scala b/src/main/scala/buttondevteam/discordplugin/DiscordPlayerSender.scala new file mode 100644 index 0000000..4f3f035 --- /dev/null +++ b/src/main/scala/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(user: User, 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/scala/buttondevteam/discordplugin/DiscordPlugin.scala b/src/main/scala/buttondevteam/discordplugin/DiscordPlugin.scala new file mode 100644 index 0000000..9521482 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/DiscordPlugin.scala @@ -0,0 +1,264 @@ +package buttondevteam.discordplugin + +import buttondevteam.discordplugin.DiscordPlugin.dc +import buttondevteam.discordplugin.announcer.AnnouncerModule +import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule +import buttondevteam.discordplugin.commands.* +import buttondevteam.discordplugin.exceptions.ExceptionListenerModule +import buttondevteam.discordplugin.fun.FunModule +import buttondevteam.discordplugin.listeners.{CommonListeners, MCListener} +import buttondevteam.discordplugin.mcchat.MinecraftChatModule +import buttondevteam.discordplugin.mccommands.DiscordMCCommand +import buttondevteam.discordplugin.role.GameRoleModule +import buttondevteam.discordplugin.util.{DPState, Timings} +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.architecture.* +import buttondevteam.lib.player.ChromaGamerBase +import com.google.common.io.Files +import discord4j.common.util.Snowflake +import discord4j.core.`object`.entity.{ApplicationInfo, Guild, Role} +import discord4j.core.`object`.presence.{Activity, ClientActivity, ClientPresence, Presence} +import discord4j.core.`object`.reaction.ReactionEmoji +import discord4j.core.event.domain.guild.GuildCreateEvent +import discord4j.core.event.domain.lifecycle.ReadyEvent +import discord4j.core.{DiscordClientBuilder, GatewayDiscordClient} +import discord4j.gateway.ShardInfo +import discord4j.rest.interaction.Interactions +import discord4j.store.jdk.JdkStoreService +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.Logger +import org.bukkit.command.CommandSender +import org.bukkit.configuration.file.YamlConfiguration +import org.mockito.internal.util.MockUtil +import reactor.core.Disposable +import reactor.core.scala.publisher.SMono + +import java.io.File +import java.nio.charset.StandardCharsets +import java.util.Optional + +@ButtonPlugin.ConfigOpts(disableConfigGen = true) object DiscordPlugin { + private[discordplugin] var dc: GatewayDiscordClient = null + private[discordplugin] var plugin: DiscordPlugin = null + private[discordplugin] var SafeMode = true + + def getPrefix: Char = { + if (plugin == null) '/' + else plugin.prefix.get + } + + private[discordplugin] var mainServer: Guild = null + private[discordplugin] val DELIVERED_REACTION = ReactionEmoji.unicode("✅") +} + +@ButtonPlugin.ConfigOpts(disableConfigGen = true) class DiscordPlugin extends ButtonPlugin { + private var _manager: Command2DC = null + + def manager: Command2DC = _manager + + private var starting = false + private var logWatcher: BukkitLogWatcher = null + /** + * The prefix to use with Discord commands like /role. It only works in the bot channel. + */ + final private val prefix = getIConfig.getData("prefix", '/', (str: Any) => str.asInstanceOf[String].charAt(0), (_: Char).toString) + + /** + * The main server where the roles and other information is pulled from. It's automatically set to the first server the bot's invited to. + */ + private def mainServer = getIConfig.getDataPrimDef("mainServer", 0L, (id: Any) => { + def foo(id: Any): Option[Guild] = { //It attempts to get the default as well + if (id.asInstanceOf[Long] == 0L) Option.empty + else SMono.fromPublisher(DiscordPlugin.dc.getGuildById(Snowflake.of(id.asInstanceOf[Long]))) + .onErrorResume((t: Throwable) => { + getLogger.warning("Failed to get guild: " + t.getMessage); + SMono.empty + }).blockOption() + } + + foo(id) + }, (g: Option[Guild]) => (g.map(_.getId.asLong): Option[Long]).getOrElse(0L)) + + /** + * The (bot) channel to use for Discord commands like /role. + */ + var commandChannel: ReadOnlyConfigData[Snowflake] = DPUtils.snowflakeData(getIConfig, "commandChannel", 0L) + /** + * The role that allows using mod-only Discord commands. + * If empty (''), then it will only allow for the owner. + */ + var modRole: ReadOnlyConfigData[SMono[Role]] = null + /** + * The invite link to show by /discord invite. If empty, it defaults to the first invite if the bot has access. + */ + var inviteLink: ConfigData[String] = getIConfig.getData("inviteLink", "") + + private def setupConfig(): Unit = modRole = DPUtils.roleData(getIConfig, "modRole", "Moderator") + + override def onLoad(): Unit = { //Needed by ServerWatcher + val thread = Thread.currentThread + val cl = thread.getContextClassLoader + thread.setContextClassLoader(getClassLoader) + MockUtil.isMock(null) //Load MockUtil to load Mockito plugins + thread.setContextClassLoader(cl) + getLogger.info("Load complete") + } + + override def pluginEnable(): Unit = try { + getLogger.info("Initializing...") + DiscordPlugin.plugin = this + _manager = new Command2DC + registerCommand(new DiscordMCCommand) //Register so that the restart command works + var token: String = null + val tokenFile = new File("TBMC", "Token.txt") + if (tokenFile.exists) { //Legacy support + //noinspection UnstableApiUsage + token = Files.readFirstLine(tokenFile, StandardCharsets.UTF_8) + } + else { + val privateFile = new File(getDataFolder, "private.yml") + val conf = YamlConfiguration.loadConfiguration(privateFile) + token = conf.getString("token") + if (token == null || token.equalsIgnoreCase("Token goes here")) { + conf.set("token", "Token goes here") + conf.save(privateFile) + getLogger.severe("Token not found! Please set it in private.yml then do /discord restart") + getLogger.severe("You need to have a bot account to use with your server.") + getLogger.severe("If you don't have one, go to https://discordapp.com/developers/applications/ and create an application, then create a bot for it and copy the bot token.") + return () + } + } + starting = true + //System.out.println("This line should show up for sure"); + val cb = DiscordClientBuilder.create(token).build.gateway + //System.out.println("Got gateway bootstrap"); + cb.setInitialPresence((si: ShardInfo) => ClientPresence.doNotDisturb(ClientActivity.playing("booting"))) + //cb.setStore(new JdkStoreService) //The default doesn't work for some reason - it's waaay faster now + //System.out.println("Initial status and store service set"); + cb.login.doOnError((t: Throwable) => { + def foo(t: Throwable): Unit = { + stopStarting() + //System.out.println("Got this error: " + t); t.printStackTrace(); + } + + foo(t) + }).subscribe((dc: GatewayDiscordClient) => { + DiscordPlugin.dc = dc //Set to gateway client + dc.on(classOf[ReadyEvent]).map(_.getGuilds.size).flatMap(dc.on(classOf[GuildCreateEvent]).take(_).collectList) + .doOnError(_ => stopStarting()).subscribe(this.handleReady _) // Take all received GuildCreateEvents and make it a List + () + }) /* All guilds have been received, client is fully connected */ + } catch { + case e: Exception => + TBMCCoreAPI.SendException("Failed to enable the Discord plugin!", e, this) + getLogger.severe("You may be able to restart the plugin using /discord restart") + stopStarting() + } + + private def stopStarting(): Unit = { + this synchronized { + starting = false + notifyAll() + } + } + + private def handleReady(event: java.util.List[GuildCreateEvent]): Unit = { //System.out.println("Got ready event"); + try { + if (DiscordPlugin.mainServer != null) { //This is not the first ready event + getLogger.info("Ready event already handled") //TODO: It should probably handle disconnections + DiscordPlugin.dc.updatePresence(ClientPresence.online(ClientActivity.playing("Minecraft"))).subscribe //Update from the initial presence + return () + } + DiscordPlugin.mainServer = mainServer.get.orNull //Shouldn't change afterwards + if (DiscordPlugin.mainServer == null) { + if (event.size == 0) { + getLogger.severe("Main server not found! Invite the bot and do /discord restart") + DiscordPlugin.dc.getApplicationInfo.subscribe((info: ApplicationInfo) => getLogger.severe("Click here: https://discordapp.com/oauth2/authorize?client_id=" + info.getId.asString + "&scope=bot&permissions=268509264")) + saveConfig() //Put default there + return () //We should have all guilds by now, no need to retry + } + DiscordPlugin.mainServer = event.get(0).getGuild + getLogger.warning("Main server set to first one: " + DiscordPlugin.mainServer.getName) + mainServer.set(Option(DiscordPlugin.mainServer)) //Save in config + } + DiscordPlugin.SafeMode = false + setupConfig() + DPUtils.disableIfConfigErrorRes(null, commandChannel, DPUtils.getMessageChannel(commandChannel)) + //Won't disable, just prints the warning here + if (MinecraftChatModule.state eq DPState.STOPPING_SERVER) { + stopStarting() + return () //Reusing that field to check if stopping while still initializing + } + CommonListeners.register(DiscordPlugin.dc.getEventDispatcher) + TBMCCoreAPI.RegisterEventsForExceptions(new MCListener, this) + TBMCCoreAPI.RegisterUserClass(classOf[DiscordPlayer], () => new DiscordPlayer) + ChromaGamerBase.addConverter((sender: CommandSender) => Optional.ofNullable(sender match { + case dsender: DiscordSenderBase => dsender.getChromaUser + case _ => null + })) + IHaveConfig.pregenConfig(this, null) + ChromaBot.enabled = true //Initialize ChromaBot + Component.registerComponent(this, new GeneralEventBroadcasterModule) + Component.registerComponent(this, new MinecraftChatModule) + Component.registerComponent(this, new ExceptionListenerModule) + Component.registerComponent(this, new GameRoleModule) //Needs the mainServer to be set + Component.registerComponent(this, new AnnouncerModule) + Component.registerComponent(this, new FunModule) + ChromaBot.updatePlayerList() //The MCChatModule is tested to be enabled + val applicationId = dc.getRestClient.getApplicationId.block() + val guildId = Some(DiscordPlugin.mainServer.getId.asLong()) + manager.registerCommand(new VersionCommand, applicationId, guildId) + manager.registerCommand(new UserinfoCommand, applicationId, guildId) + manager.registerCommand(new HelpCommand, applicationId, guildId) + manager.registerCommand(new DebugCommand, applicationId, guildId) + manager.registerCommand(new ConnectCommand, applicationId, guildId) + TBMCCoreAPI.SendUnsentExceptions() + TBMCCoreAPI.SendUnsentDebugMessages() + val blw = new BukkitLogWatcher + blw.start() + LogManager.getRootLogger.asInstanceOf[Logger].addAppender(blw) + logWatcher = blw + if (!TBMCCoreAPI.IsTestServer) DiscordPlugin.dc.updatePresence(ClientPresence.online(ClientActivity.playing("Minecraft"))).subscribe() + else DiscordPlugin.dc.updatePresence(ClientPresence.online(ClientActivity.playing("testing"))).subscribe() + getLogger.info("Loaded!") + } catch { + case e: Exception => + TBMCCoreAPI.SendException("An error occurred while enabling DiscordPlugin!", e, this) + } + stopStarting() + } + + override def pluginPreDisable(): Unit = { + if (MinecraftChatModule.state eq DPState.RUNNING) MinecraftChatModule.state = DPState.STOPPING_SERVER + this synchronized { + if (starting) try wait(10000) + catch { + case e: InterruptedException => + e.printStackTrace() + } + } + if (!ChromaBot.enabled) return () //Failed to load + val timings = new Timings + timings.printElapsed("Disable start") + timings.printElapsed("Updating player list") + ChromaBot.updatePlayerList() + timings.printElapsed("Done") + } + + override def pluginDisable(): Unit = { + val timings = new Timings + timings.printElapsed("Actual disable start (logout)") + if (!ChromaBot.enabled) return () + try { + DiscordPlugin.SafeMode = true // Stop interacting with Discord + ChromaBot.enabled = false + LogManager.getRootLogger.asInstanceOf[Logger].removeAppender(logWatcher) + timings.printElapsed("Logging out...") + DiscordPlugin.dc.logout.block + DiscordPlugin.mainServer = null //Allow ReadyEvent again + } catch { + case e: Exception => + TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e, this) + } + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/DiscordSender.scala b/src/main/scala/buttondevteam/discordplugin/DiscordSender.scala new file mode 100644 index 0000000..a77979e --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/DiscordSender.scala @@ -0,0 +1,61 @@ +package buttondevteam.discordplugin + +import discord4j.core.`object`.entity.User +import discord4j.core.`object`.entity.channel.MessageChannel +import org.bukkit.command.CommandSender +import org.bukkit.permissions.{PermissibleBase, Permission, PermissionAttachment, PermissionAttachmentInfo} +import org.bukkit.plugin.Plugin +import org.bukkit.{Bukkit, Server} +import reactor.core.scala.publisher.SMono + +import java.util + +class DiscordSender(user: User, channel: MessageChannel, pname: String) extends DiscordSenderBase(user, channel) with CommandSender { + private val perm = new PermissibleBase(this) + private val name: String = Option(pname) + .orElse(Option(user).flatMap(u => SMono(u.asMember(DiscordPlugin.mainServer.getId)) + .onErrorResume(_ => SMono.empty).blockOption() + .map(u => u.getDisplayName))) + .getOrElse("Discord user") + + def this(user: User, channel: MessageChannel) = { + this(user, channel, null) + } + + 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")) false + else 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(): CommandSender.Spigot = new CommandSender.Spigot + override def spigot(): CommandSender.Spigot = ??? +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/DiscordSenderBase.scala b/src/main/scala/buttondevteam/discordplugin/DiscordSenderBase.scala new file mode 100644 index 0000000..46a9e69 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/DiscordSenderBase.scala @@ -0,0 +1,61 @@ +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, (() => { + channel.createMessage((if (user != null) user.getMention + "\n" else "") + msgtosend.trim).subscribe() + sendtask = null + msgtosend = "" + }): Runnable, 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/scala/buttondevteam/discordplugin/IMCPlayer.scala b/src/main/scala/buttondevteam/discordplugin/IMCPlayer.scala new file mode 100644 index 0000000..70f1a5d --- /dev/null +++ b/src/main/scala/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/scala/buttondevteam/discordplugin/announcer/AnnouncerModule.scala b/src/main/scala/buttondevteam/discordplugin/announcer/AnnouncerModule.scala new file mode 100644 index 0000000..5c72574 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/announcer/AnnouncerModule.scala @@ -0,0 +1,104 @@ +package buttondevteam.discordplugin.announcer + +import buttondevteam.discordplugin.{DPUtils, DiscordPlayer, DiscordPlugin} +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.architecture.{Component, ComponentMetadata} +import buttondevteam.lib.player.ChromaGamerBase +import com.google.gson.JsonParser +import discord4j.core.`object`.entity.channel.MessageChannel +import reactor.core.scala.publisher.SMono + +import scala.annotation.tailrec + +/** + * Posts new posts from Reddit to the specified channel(s). It will pin the regular posts (not the mod posts). + */ +@ComponentMetadata(enabledByDefault = false) object AnnouncerModule { + private var stop = false +} + +@ComponentMetadata(enabledByDefault = false) class AnnouncerModule extends Component[DiscordPlugin] { + /** + * Channel to post new posts. + */ + final val channel = DPUtils.channelData(getConfig, "channel") + /** + * Channel where distinguished (moderator) posts go. + */ + final private val modChannel = DPUtils.channelData(getConfig, "modChannel") + /** + * Automatically unpins all messages except the last few. Set to 0 or >50 to disable + */ + final private val keepPinned = getConfig.getData("keepPinned", 40.toShort) + final private val lastAnnouncementTime = getConfig.getData("lastAnnouncementTime", 0L) + final private val lastSeenTime = getConfig.getData("lastSeenTime", 0L) + /** + * The subreddit to pull the posts from + */ + final private val subredditURL = getConfig.getData("subredditURL", "https://www.reddit.com/r/ChromaGamers") + + override protected def enable(): Unit = { + if (DPUtils.disableIfConfigError(this, channel, modChannel)) return () + AnnouncerModule.stop = false //If not the first time + val kp = keepPinned.get + if (kp <= 0) return () + val msgs = channel.get.flatMapMany(_.getPinnedMessages).takeLast(kp) + msgs.subscribe(_.unpin) + new Thread(() => this.AnnouncementGetterThreadMethod()).start() + } + + override protected def disable(): Unit = AnnouncerModule.stop = true + + @tailrec + private def AnnouncementGetterThreadMethod(): Unit = { + if (AnnouncerModule.stop) return () + if (isEnabled) try { //If not enabled, just wait + val body = TBMCCoreAPI.DownloadString(subredditURL.get + "/new/.json?limit=10") + val json = new JsonParser().parse(body).getAsJsonObject.get("data").getAsJsonObject.get("children").getAsJsonArray + val msgsb = new StringBuilder + val modmsgsb = new StringBuilder + var lastanntime = lastAnnouncementTime.get + for (i <- json.size - 1 to 0 by -1) { + val item = json.get(i).getAsJsonObject + val data = item.get("data").getAsJsonObject + var author = data.get("author").getAsString + val distinguishedjson = data.get("distinguished") + val distinguished = if (distinguishedjson.isJsonNull) null else distinguishedjson.getAsString + val permalink = "https://www.reddit.com" + data.get("permalink").getAsString + val date = data.get("created_utc").getAsLong + if (date > lastSeenTime.get) lastSeenTime.set(date) + else if (date > lastAnnouncementTime.get) { //noinspection ConstantConditions + { + val reddituserclass = ChromaGamerBase.getTypeForFolder("reddit") + if (reddituserclass != null) { + val user = ChromaGamerBase.getUser(author, reddituserclass) + val id = user.getConnectedID(classOf[DiscordPlayer]) + if (id != null) author = "<@" + id + ">" + } + } + if (!author.startsWith("<")) author = "/u/" + author + (if (distinguished != null && distinguished == "moderator") modmsgsb else msgsb) + .append("A new post was submitted to the subreddit by ").append(author).append("\n") + .append(permalink).append("\n") + lastanntime = date + } + } + + def sendMsg(ch: SMono[MessageChannel], msg: String) = + ch.asJava().flatMap(c => c.createMessage(msg)).flatMap(_.pin).subscribe() + + if (msgsb.nonEmpty) sendMsg(channel.get(), msgsb.toString()) + if (modmsgsb.nonEmpty) sendMsg(modChannel.get(), modmsgsb.toString()) + if (lastAnnouncementTime.get != lastanntime) lastAnnouncementTime.set(lastanntime) // If sending succeeded + } catch { + case e: Exception => + e.printStackTrace() + } + try Thread.sleep(10000) + catch { + case ex: InterruptedException => + Thread.currentThread.interrupt() + } + AnnouncementGetterThreadMethod() + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/broadcaster/GeneralEventBroadcasterModule.scala b/src/main/scala/buttondevteam/discordplugin/broadcaster/GeneralEventBroadcasterModule.scala new file mode 100644 index 0000000..0072c77 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/broadcaster/GeneralEventBroadcasterModule.scala @@ -0,0 +1,39 @@ +package buttondevteam.discordplugin.broadcaster + +import buttondevteam.discordplugin.DiscordPlugin +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.architecture.{Component, ComponentMetadata} + +/** + * Uses a bit of a hacky method of getting all broadcasted messages, including advancements and any other message that's for everyone. + * If this component is enabled then these messages will show up on Discord. + */ +@ComponentMetadata(enabledByDefault = false) object GeneralEventBroadcasterModule { + def isHooked: Boolean = GeneralEventBroadcasterModule.hooked + + private var hooked = false +} + +@ComponentMetadata(enabledByDefault = false) class GeneralEventBroadcasterModule extends Component[DiscordPlugin] { + override protected def enable(): Unit = try { + PlayerListWatcher.hookUpDown(true, this) + log("Finished hooking into the player list") + GeneralEventBroadcasterModule.hooked = true + } catch { + case e: Exception => + TBMCCoreAPI.SendException("Error while hacking the player list! Disable this module if you're on an incompatible version.", e, this) + case _: NoClassDefFoundError => + logWarn("Error while hacking the player list! Disable this module if you're on an incompatible version.") + } + + override protected def disable(): Unit = try { + if (!GeneralEventBroadcasterModule.hooked) return () + if (PlayerListWatcher.hookUpDown(false, this)) log("Finished unhooking the player list!") + else log("Didn't have the player list hooked.") + GeneralEventBroadcasterModule.hooked = false + } catch { + case e: Exception => + TBMCCoreAPI.SendException("Error while hacking the player list!", e, this) + case _: NoClassDefFoundError => + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.scala b/src/main/scala/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.scala new file mode 100644 index 0000000..49c2a01 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.scala @@ -0,0 +1,168 @@ +package buttondevteam.discordplugin.broadcaster + +import buttondevteam.discordplugin.mcchat.MCChatUtils +import buttondevteam.discordplugin.playerfaker.DelegatingMockMaker +import buttondevteam.lib.TBMCCoreAPI +import org.bukkit.Bukkit +import org.mockito.Mockito +import org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker +import org.mockito.invocation.InvocationOnMock +import org.mockito.stubbing.Answer + +import java.lang.invoke.{MethodHandle, MethodHandles} +import java.lang.reflect.{Constructor, Method, Modifier} +import java.util.UUID + +object PlayerListWatcher { + private var plist: AnyRef = null + private var mock: AnyRef = null + private var fHandle: MethodHandle = null //Handle for PlayerList.f(EntityPlayer) - Only needed for 1.16 + @throws[Exception] + private[broadcaster] def hookUpDown(up: Boolean, module: GeneralEventBroadcasterModule): Boolean = { + val csc = Bukkit.getServer.getClass + val conf = csc.getDeclaredField("console") + conf.setAccessible(true) + val server = conf.get(Bukkit.getServer) + val nms = server.getClass.getPackage.getName + val dplc = Class.forName(nms + ".DedicatedPlayerList") + val currentPL = server.getClass.getMethod("getPlayerList").invoke(server) + if (up) { + if (currentPL eq mock) { + module.logWarn("Player list already mocked!") + return false + } + DelegatingMockMaker.getInstance.setMockMaker(new SubclassByteBuddyMockMaker) + val icbcl = Class.forName(nms + ".IChatBaseComponent") + var sendMessageTemp: Method = null + try sendMessageTemp = server.getClass.getMethod("sendMessage", icbcl, classOf[UUID]) + catch { + case e: NoSuchMethodException => + sendMessageTemp = server.getClass.getMethod("sendMessage", icbcl) + } + val sendMessageMethod = sendMessageTemp + val cmtcl = Class.forName(nms + ".ChatMessageType") + val systemType = cmtcl.getDeclaredField("SYSTEM").get(null) + val chatType = cmtcl.getDeclaredField("CHAT").get(null) + val obc = csc.getPackage.getName + val ccmcl = Class.forName(obc + ".util.CraftChatMessage") + val fixComponent = ccmcl.getMethod("fixComponent", icbcl) + val ppoc = Class.forName(nms + ".PacketPlayOutChat") + var ppocCTemp: Constructor[_] = null + try ppocCTemp = ppoc.getConstructor(icbcl, cmtcl, classOf[UUID]) + catch { + case _: Exception => + ppocCTemp = ppoc.getConstructor(icbcl, cmtcl) + } + val ppocC = ppocCTemp + val sendAllMethod = dplc.getMethod("sendAll", Class.forName(nms + ".Packet")) + var tpt: Method = null + try tpt = icbcl.getMethod("toPlainText") + catch { + case _: NoSuchMethodException => + tpt = icbcl.getMethod("getString") + } + val toPlainText = tpt + val sysb = Class.forName(nms + ".SystemUtils").getField("b") + //Find the original method without overrides + var lookupConstructor: Constructor[MethodHandles.Lookup] = null + if (nms.contains("1_16")) { + lookupConstructor = classOf[MethodHandles.Lookup].getDeclaredConstructor(classOf[Class[_]]) + lookupConstructor.setAccessible(true) //Create lookup with a given class instead of caller + } + else lookupConstructor = null + mock = Mockito.mock(dplc, Mockito.withSettings.defaultAnswer(new Answer[AnyRef]() { // Cannot call super constructor + @throws[Throwable] + override def answer(invocation: InvocationOnMock): AnyRef = { + val method = invocation.getMethod + if (!(method.getName == "sendMessage")) { + if (method.getName == "sendAll") { + sendAll(invocation.getArgument(0)) + return null + } + //In 1.16 it passes a reference to the player list to advancement data for each player + if (nms.contains("1_16") && method.getName == "f" && method.getParameterCount > 0 && method.getParameterTypes()(0).getSimpleName == "EntityPlayer") { + method.setAccessible(true) + if (fHandle == null) { + assert(lookupConstructor != null) + val lookup = lookupConstructor.newInstance(mock.getClass) + fHandle = lookup.unreflectSpecial(method, mock.getClass) //Special: super.method() + } + return fHandle.invoke(mock, invocation.getArgument(0)) //Invoke with our instance, so it passes that to advancement data, we have the fields as well + } + return method.invoke(plist, invocation.getArguments) + } + val args = invocation.getArguments + val params = method.getParameterTypes + if (params.isEmpty) { + TBMCCoreAPI.SendException("Found a strange method", new Exception("Found a sendMessage() method without arguments."), module) + return null + } + if (params(0).getSimpleName == "IChatBaseComponent[]") for (arg <- args(0).asInstanceOf[Array[AnyRef]]) { + sendMessage(arg, system = true) + } + else if (params(0).getSimpleName == "IChatBaseComponent") if (params.length > 1 && params(1).getSimpleName.equalsIgnoreCase("boolean")) sendMessage(args(0), args(1).asInstanceOf[Boolean]) + else sendMessage(args(0), system = true) + else TBMCCoreAPI.SendException("Found a method with interesting params", new Exception("Found a sendMessage(" + params(0).getSimpleName + ") method"), module) + null + } + + private + + def sendMessage(chatComponent: Any, system: Boolean) = try { //Converted to use reflection + if (sendMessageMethod.getParameterCount == 2) sendMessageMethod.invoke(server, chatComponent, sysb.get(null)) + else sendMessageMethod.invoke(server, chatComponent) + val chatmessagetype = if (system) systemType + else chatType + // CraftBukkit start - we run this through our processor first so we can get web links etc + val comp = fixComponent.invoke(null, chatComponent) + val packet = if (ppocC.getParameterCount == 3) ppocC.newInstance(comp, chatmessagetype, sysb.get(null)) + else ppocC.newInstance(comp, chatmessagetype) + this.sendAll(packet) + } catch { + case e: Exception => + TBMCCoreAPI.SendException("An error occurred while passing a vanilla message through the player list", e, module) + } + + private + + def sendAll(packet: Any) = try { // Some messages get sent by directly constructing a packet + sendAllMethod.invoke(plist, packet) + if (packet.getClass eq ppoc) { + val msgf = ppoc.getDeclaredField("a") + msgf.setAccessible(true) + MCChatUtils.forPublicPrivateChat(MCChatUtils.send(toPlainText.invoke(msgf.get(packet)).asInstanceOf[String])).subscribe() + } + } catch { + case e: Exception => + TBMCCoreAPI.SendException("Failed to broadcast message sent to all players - hacking failed.", e, module) + } + }).stubOnly).asInstanceOf + plist = currentPL + var plc = dplc + while ( { + plc != null + }) { //Set all fields + for (f <- plc.getDeclaredFields) { + f.setAccessible(true) + val modf = f.getClass.getDeclaredField("modifiers") + modf.setAccessible(true) + modf.set(f, f.getModifiers & ~Modifier.FINAL) + f.set(mock, f.get(plist)) + } + plc = plc.getSuperclass + } + } + try server.getClass.getMethod("a", dplc).invoke(server, if (up) mock + else plist) + catch { + case e: NoSuchMethodException => + server.getClass.getMethod("a", Class.forName(server.getClass.getPackage.getName + ".PlayerList")).invoke(server, if (up) mock + else plist) + } + val pllf = csc.getDeclaredField("playerList") + pllf.setAccessible(true) + pllf.set(Bukkit.getServer, if (up) mock + else plist) + true + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/commands/Command2DC.scala b/src/main/scala/buttondevteam/discordplugin/commands/Command2DC.scala new file mode 100644 index 0000000..7301784 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/commands/Command2DC.scala @@ -0,0 +1,39 @@ +package buttondevteam.discordplugin.commands + +import buttondevteam.discordplugin.DiscordPlugin +import buttondevteam.lib.chat.Command2 +import discord4j.common.util.Snowflake +import discord4j.core.`object`.command.ApplicationCommandOption +import discord4j.discordjson.json.{ApplicationCommandOptionData, ApplicationCommandRequest} + +import java.lang.reflect.Method + +class Command2DC extends Command2[ICommand2DC, Command2DCSender] { + override def registerCommand(command: ICommand2DC): Unit = { + registerCommand(command, DiscordPlugin.dc.getApplicationInfo.block().getId.asLong()) + } + + def registerCommand(command: ICommand2DC, appId: Long, guildId: Option[Long] = None): Unit = { + super.registerCommand(command, DiscordPlugin.getPrefix) //Needs to be configurable for the helps + val greetCmdRequest = ApplicationCommandRequest.builder() + .name(command.getCommandPath) //TODO: Main path + .description("A ChromaBot command.") //TODO: Description + .addOption(ApplicationCommandOptionData.builder() + .name("name") + .description("Your name") + .`type`(ApplicationCommandOption.Type.STRING.getValue) + .required(true) + .build() + ).build() + val service = DiscordPlugin.dc.getRestClient.getApplicationService + guildId match { + case Some(id) => service.createGuildApplicationCommand(appId, id, greetCmdRequest).subscribe() + case None => service.createGlobalApplicationCommand(appId, greetCmdRequest).subscribe() + } + } + + override def hasPermission(sender: Command2DCSender, command: ICommand2DC, method: Method): Boolean = { + //return !command.isModOnly() || sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.modRole().get()); //TODO: modRole may be null; more customisable way? + true + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/commands/Command2DCSender.scala b/src/main/scala/buttondevteam/discordplugin/commands/Command2DCSender.scala new file mode 100644 index 0000000..5cac3bf --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/commands/Command2DCSender.scala @@ -0,0 +1,21 @@ +package buttondevteam.discordplugin.commands + +import buttondevteam.discordplugin.DPUtils +import buttondevteam.lib.chat.Command2Sender +import discord4j.core.`object`.entity.channel.MessageChannel +import discord4j.core.`object`.entity.{Message, User} + +class Command2DCSender(val message: Message) extends Command2Sender { + def getMessage: Message = this.message + + override def sendMessage(message: String): Unit = { + if (message.isEmpty) return () + var msg = DPUtils.sanitizeString(message) + msg = Character.toLowerCase(message.charAt(0)) + message.substring(1) + this.message.getChannel.flatMap((ch: MessageChannel) => ch.createMessage(this.message.getAuthor.map((u: User) => DPUtils.nickMention(u.getId) + ", ").orElse("") + msg)).subscribe() + } + + override def sendMessage(message: Array[String]): Unit = sendMessage(String.join("\n", message: _*)) + + override def getName: String = Option(message.getAuthor.orElse(null)).map(_.getUsername).getOrElse("Discord") +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/commands/ConnectCommand.scala b/src/main/scala/buttondevteam/discordplugin/commands/ConnectCommand.scala new file mode 100644 index 0000000..dc05087 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/commands/ConnectCommand.scala @@ -0,0 +1,49 @@ +package buttondevteam.discordplugin.commands + +import buttondevteam.discordplugin.DiscordPlayer +import buttondevteam.lib.chat.{Command2, CommandClass} +import buttondevteam.lib.player.{TBMCPlayer, TBMCPlayerBase} +import com.google.common.collect.HashBiMap +import org.bukkit.Bukkit +import org.bukkit.entity.Player + +@CommandClass(helpText = Array("Connect command", // + "This command lets you connect your account with a Minecraft account." + + " This allows using the private Minecraft chat and other things.")) object ConnectCommand { + /** + * Key: Minecraft name
+ * Value: Discord ID + */ + var WaitingToConnect: HashBiMap[String, String] = HashBiMap.create +} + +@CommandClass(helpText = Array("Connect command", + "This command lets you connect your account with a Minecraft account." + + " This allows using the private Minecraft chat and other things.")) class ConnectCommand extends ICommand2DC { + @Command2.Subcommand def `def`(sender: Command2DCSender, Minecraftname: String): Boolean = { + val message = sender.getMessage + val channel = message.getChannel.block + val author = message.getAuthor.orElse(null) + if (author == null || channel == null) return true + if (ConnectCommand.WaitingToConnect.inverse.containsKey(author.getId.asString)) { + channel.createMessage("Replacing " + ConnectCommand.WaitingToConnect.inverse.get(author.getId.asString) + " with " + Minecraftname).subscribe() + ConnectCommand.WaitingToConnect.inverse.remove(author.getId.asString) + } + //noinspection ScalaDeprecation + val p = Bukkit.getOfflinePlayer(Minecraftname) + if (p == null) { + channel.createMessage("The specified Minecraft player cannot be found").subscribe() + return true + } + val pl = TBMCPlayerBase.getPlayer(p.getUniqueId, classOf[TBMCPlayer]) + val dp = pl.getAs(classOf[DiscordPlayer]) + if (dp != null && author.getId.asString == dp.getDiscordID) { + channel.createMessage("You already have this account connected.").subscribe() + return true + } + ConnectCommand.WaitingToConnect.put(p.getName, author.getId.asString) + channel.createMessage("Alright! Now accept the connection in Minecraft from the account " + Minecraftname + " before the next server restart. You can also adjust the Minecraft name you want to connect to by running this command again.").subscribe() + if (p.isOnline) p.asInstanceOf[Player].sendMessage("§bTo connect with the Discord account " + author.getUsername + "#" + author.getDiscriminator + " do /discord accept") + true + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/commands/DebugCommand.scala b/src/main/scala/buttondevteam/discordplugin/commands/DebugCommand.scala new file mode 100644 index 0000000..637c14b --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/commands/DebugCommand.scala @@ -0,0 +1,30 @@ +package buttondevteam.discordplugin.commands + +import buttondevteam.discordplugin.DiscordPlugin +import buttondevteam.discordplugin.listeners.CommonListeners +import buttondevteam.lib.chat.{Command2, CommandClass} +import discord4j.common.util.Snowflake +import discord4j.core.`object`.entity.{Member, User} +import reactor.core.scala.publisher.SMono + +@CommandClass(helpText = Array("Switches debug mode.")) +class DebugCommand extends ICommand2DC { + @Command2.Subcommand + override def `def`(sender: Command2DCSender): Boolean = { + SMono(sender.getMessage.getAuthorAsMember) + .switchIfEmpty(Option(sender.getMessage.getAuthor.orElse(null)) //Support DMs + .map((u: User) => SMono(u.asMember(DiscordPlugin.mainServer.getId))).getOrElse(SMono.empty)) + .flatMap((m: Member) => DiscordPlugin.plugin.modRole.get + .map(mr => m.getRoleIds.stream.anyMatch((r: Snowflake) => r == mr.getId)) + .switchIfEmpty(SMono.fromCallable(() => DiscordPlugin.mainServer.getOwnerId.asLong == m.getId.asLong))) + .onErrorResume(_ => SMono.just(false)) //Role not found + .subscribe(success => { + if (success) { + CommonListeners.debug = !CommonListeners.debug; + sender.sendMessage("debug " + (if (CommonListeners.debug) "enabled" else "disabled")) + } else + sender.sendMessage("you need to be a moderator to use this command.") + }) + true + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/commands/HelpCommand.scala b/src/main/scala/buttondevteam/discordplugin/commands/HelpCommand.scala new file mode 100644 index 0000000..77d51df --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/commands/HelpCommand.scala @@ -0,0 +1,18 @@ +package buttondevteam.discordplugin.commands + +import buttondevteam.lib.chat.{Command2, CommandClass} + +@CommandClass(helpText = Array("Help command", // + "Shows some info about a command or lists the available commands.")) +class HelpCommand extends ICommand2DC { + @Command2.Subcommand + def `def`(sender: Command2DCSender, @Command2.TextArg @Command2.OptionalArg args: String): Boolean = { + if (args == null || args.isEmpty) sender.sendMessage(getManager.getCommandsText) + else { + val ht = getManager.getHelpText(args) + if (ht == null) sender.sendMessage("Command not found: " + args) + else sender.sendMessage(ht) + } + true + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/commands/ICommand2DC.scala b/src/main/scala/buttondevteam/discordplugin/commands/ICommand2DC.scala new file mode 100644 index 0000000..31ae228 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/commands/ICommand2DC.scala @@ -0,0 +1,16 @@ +package buttondevteam.discordplugin.commands + +import buttondevteam.discordplugin.DiscordPlugin +import buttondevteam.lib.chat.{CommandClass, ICommand2} + +abstract class ICommand2DC() extends ICommand2[Command2DCSender](DiscordPlugin.plugin.manager) { + final private var modOnly = false + + { + val ann: CommandClass = getClass.getAnnotation(classOf[CommandClass]) + if (ann == null) modOnly = false + else modOnly = ann.modOnly + } + + def isModOnly: Boolean = this.modOnly +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/commands/UserinfoCommand.scala b/src/main/scala/buttondevteam/discordplugin/commands/UserinfoCommand.scala new file mode 100644 index 0000000..5fa100d --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/commands/UserinfoCommand.scala @@ -0,0 +1,73 @@ +package buttondevteam.discordplugin.commands + +import buttondevteam.discordplugin.{DiscordPlayer, DiscordPlugin} +import buttondevteam.lib.chat.{Command2, CommandClass} +import buttondevteam.lib.player.ChromaGamerBase +import buttondevteam.lib.player.ChromaGamerBase.InfoTarget +import discord4j.core.`object`.entity.{Message, User} +import reactor.core.scala.publisher.SFlux + +import scala.jdk.CollectionConverters.ListHasAsScala + +@CommandClass(helpText = Array("User information", // + "Shows some information about users, from Discord, from Minecraft or from Reddit if they have these accounts connected.", + "If used without args, shows your info.")) +class UserinfoCommand extends ICommand2DC { + @Command2.Subcommand + def `def`(sender: Command2DCSender, @Command2.OptionalArg @Command2.TextArg user: String): Boolean = { + val message = sender.getMessage + var target: User = null + val channel = message.getChannel.block + assert(channel != null) + if (user == null || user.isEmpty) target = message.getAuthor.orElse(null) + else { + val firstmention = message.getUserMentions.asScala.find((m: User) => !(m.getId.asString == DiscordPlugin.dc.getSelfId.asString)) + if (firstmention.isDefined) target = firstmention.get + else if (user.contains("#")) { + val targettag = user.split("#") + val targets = getUsers(message, targettag(0)) + if (targets.isEmpty) { + channel.createMessage("The user cannot be found (by name): " + user).subscribe() + return true + } + targets.collectFirst { + case user => user.getDiscriminator.equalsIgnoreCase(targettag(1)) + } + if (target == null) { + channel.createMessage("The user cannot be found (by discriminator): " + user + "(Found " + targets.size + " users with the name.)").subscribe() + return true + } + } + else { + val targets = getUsers(message, user) + if (targets.isEmpty) { + channel.createMessage("The user cannot be found on Discord: " + user).subscribe() + return true + } + if (targets.size > 1) { + channel.createMessage("Multiple users found with that (nick)name. Please specify the whole tag, like ChromaBot#6338 or use a ping.").subscribe() + return true + } + target = targets.head + } + } + if (target == null) { + sender.sendMessage("An error occurred.") + return true + } + val dp = ChromaGamerBase.getUser(target.getId.asString, classOf[DiscordPlayer]) + val uinfo = new StringBuilder("User info for ").append(target.getUsername).append(":\n") + uinfo.append(dp.getInfo(InfoTarget.Discord)) + channel.createMessage(uinfo.toString).subscribe() + true + } + + private def getUsers(message: Message, args: String) = { + val guild = message.getGuild.block + if (guild == null) { //Private channel + SFlux(DiscordPlugin.dc.getUsers).filter(u => u.getUsername.equalsIgnoreCase(args)).collectSeq().block() + } + else + SFlux(guild.getMembers).filter(_.getUsername.equalsIgnoreCase(args)).map(_.asInstanceOf[User]).collectSeq().block() + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/commands/VersionCommand.scala b/src/main/scala/buttondevteam/discordplugin/commands/VersionCommand.scala new file mode 100644 index 0000000..9f8544c --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/commands/VersionCommand.scala @@ -0,0 +1,20 @@ +package buttondevteam.discordplugin.commands + +import buttondevteam.discordplugin.DiscordPlugin +import buttondevteam.lib.chat.{Command2, CommandClass} + +@CommandClass(helpText = Array("Version", "Returns the plugin's version")) +object VersionCommand { + def getVersion: Array[String] = { + val desc = DiscordPlugin.plugin.getDescription + Array[String](desc.getFullName, desc.getWebsite) + } +} + +@CommandClass(helpText = Array("Version", "Returns the plugin's version")) +class VersionCommand extends ICommand2DC { + @Command2.Subcommand override def `def`(sender: Command2DCSender): Boolean = { + sender.sendMessage(VersionCommand.getVersion) + true + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/exceptions/DebugMessageListener.scala b/src/main/scala/buttondevteam/discordplugin/exceptions/DebugMessageListener.scala new file mode 100644 index 0000000..d6bf7aa --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/exceptions/DebugMessageListener.scala @@ -0,0 +1,33 @@ +package buttondevteam.discordplugin.exceptions + +import buttondevteam.core.ComponentManager +import buttondevteam.discordplugin.DiscordPlugin +import buttondevteam.lib.TBMCDebugMessageEvent +import discord4j.core.`object`.entity.channel.MessageChannel +import org.bukkit.event.{EventHandler, Listener} +import reactor.core.scala.publisher.SMono + +object DebugMessageListener { + private def SendMessage(message: String): Unit = { + if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(classOf[ExceptionListenerModule])) return () + try { + val mc = ExceptionListenerModule.getChannel + if (mc == null) return () + val sb = new StringBuilder + sb.append("```").append("\n") + sb.append(if (message.length > 2000) message.substring(0, 2000) else message).append("\n") + sb.append("```") + mc.flatMap((ch: MessageChannel) => SMono(ch.createMessage(sb.toString))).subscribe() + } catch { + case ex: Exception => + ex.printStackTrace() + } + } +} + +class DebugMessageListener extends Listener { + @EventHandler def onDebugMessage(e: TBMCDebugMessageEvent): Unit = { + DebugMessageListener.SendMessage(e.getDebugMessage) + e.setSent() + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.scala b/src/main/scala/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.scala new file mode 100644 index 0000000..bd5826f --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.scala @@ -0,0 +1,93 @@ +package buttondevteam.discordplugin.exceptions + +import buttondevteam.core.ComponentManager +import buttondevteam.discordplugin.{DPUtils, DiscordPlugin} +import buttondevteam.lib.architecture.Component +import buttondevteam.lib.{TBMCCoreAPI, TBMCExceptionEvent} +import discord4j.core.`object`.entity.channel.{GuildChannel, MessageChannel} +import discord4j.core.`object`.entity.{Guild, Role} +import org.apache.commons.lang.exception.ExceptionUtils +import org.bukkit.Bukkit +import org.bukkit.event.{EventHandler, Listener} +import reactor.core.scala.publisher.SMono + +import java.util +import java.util.stream.Collectors + +/** + * Listens for errors from the Chroma plugins and posts them to Discord, ignoring repeating errors so it's not that spammy. + */ +object ExceptionListenerModule { + private def SendException(e: Throwable, sourcemessage: String): Unit = { + if (instance == null) return () + try getChannel.flatMap(channel => { + val coderRole = channel match { + case ch: GuildChannel => instance.pingRole(SMono(ch.getGuild)).get + case _ => SMono.empty + } + coderRole.map((role: Role) => if (TBMCCoreAPI.IsTestServer) new StringBuilder + else new StringBuilder(role.getMention).append("\n")) + .defaultIfEmpty(new StringBuilder).flatMap(sb => { + sb.append(sourcemessage).append("\n") + sb.append("```").append("\n") + var stackTrace = util.Arrays.stream(ExceptionUtils.getStackTrace(e).split("\\n")) + .filter(s => !s.contains("\tat ") || s.contains("buttondevteam.")) + .collect(Collectors.joining("\n")) + if (sb.length + stackTrace.length >= 1980) stackTrace = stackTrace.substring(0, 1980 - sb.length) + sb.append(stackTrace).append("\n") + sb.append("```") + SMono(channel.createMessage(sb.toString)) + }) + }).subscribe() + catch { + case ex: Exception => + ex.printStackTrace() + } + } + + private var instance: ExceptionListenerModule = null + + def getChannel: SMono[MessageChannel] = { + if (instance != null) return instance.channel.get + SMono.empty + } +} + +class ExceptionListenerModule extends Component[DiscordPlugin] with Listener { + final private val lastthrown = new util.ArrayList[Throwable] + final private val lastsourcemsg = new util.ArrayList[String] + + @EventHandler def onException(e: TBMCExceptionEvent): Unit = { + if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(getClass)) return () + if (lastthrown.stream.anyMatch(ex => e.getException.getStackTrace.sameElements(ex.getStackTrace) + && (if (e.getException.getMessage == null) ex.getMessage == null else e.getException.getMessage == ex.getMessage)) + && lastsourcemsg.contains(e.getSourceMessage)) { + return () + } + ExceptionListenerModule.SendException(e.getException, e.getSourceMessage) + if (lastthrown.size >= 10) lastthrown.remove(0) + if (lastsourcemsg.size >= 10) lastsourcemsg.remove(0) + lastthrown.add(e.getException) + lastsourcemsg.add(e.getSourceMessage) + e.setHandled() + } + + /** + * The channel to post the errors to. + */ + final private val channel = DPUtils.channelData(getConfig, "channel") + + /** + * The role to ping if an error occurs. Set to empty ('') to disable. + */ + private def pingRole(guild: SMono[Guild]) = DPUtils.roleData(getConfig, "pingRole", "Coder", guild) + + override protected def enable(): Unit = { + if (DPUtils.disableIfConfigError(this, channel)) return () + ExceptionListenerModule.instance = this + Bukkit.getPluginManager.registerEvents(new ExceptionListenerModule, getPlugin) + TBMCCoreAPI.RegisterEventsForExceptions(new DebugMessageListener, getPlugin) + } + + override protected def disable(): Unit = ExceptionListenerModule.instance = null +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/fun/FunModule.scala b/src/main/scala/buttondevteam/discordplugin/fun/FunModule.scala new file mode 100644 index 0000000..d924c09 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/fun/FunModule.scala @@ -0,0 +1,134 @@ +package buttondevteam.discordplugin.fun + +import buttondevteam.core.ComponentManager +import buttondevteam.discordplugin.{DPUtils, DiscordPlugin} +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.architecture.{Component, ConfigData} +import com.google.common.collect.Lists +import discord4j.core.`object`.entity.channel.{GuildChannel, MessageChannel} +import discord4j.core.`object`.entity.{Guild, Message} +import discord4j.core.`object`.presence.Status +import discord4j.core.event.domain.PresenceUpdateEvent +import discord4j.core.spec.legacy.{LegacyEmbedCreateSpec, LegacyMessageCreateSpec} +import org.bukkit.Bukkit +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.{EventHandler, Listener} +import reactor.core.scala.publisher.{SFlux, SMono} + +import java.util +import java.util.Calendar +import java.util.concurrent.TimeUnit +import java.util.stream.IntStream +import scala.util.Random + +/** + * All kinds of random things. + * The YEEHAW event uses an emoji named :YEEHAW: if available + */ +object FunModule { + private val serverReadyStrings = Array[String]("in one week from now", // Ali + "between now and the heat-death of the universe.", // Ghostise + "soon™", "ask again this time next month", "in about 3 seconds", // Nicolai + "after we finish 8 plugins", "tomorrow.", "after one tiiiny feature", + "next commit", "after we finish strangling Towny", "when we kill every *fucking* bug", + "once the server stops screaming.", "after HL3 comes out", "next time you ask", + "when will *you* be open?") // Ali + private val serverReadyRandom = new Random + private val usableServerReadyStrings = new java.util.ArrayList[Short](0) + private var lastlist = 0 + private var lastlistp = 0 + private var ListC = 0 + + def executeMemes(message: Message): Boolean = { + val fm = ComponentManager.getIfEnabled(classOf[FunModule]) + if (fm == null) return false + val msglowercased = message.getContent.toLowerCase + lastlist += 1 + if (lastlist > 5) { + ListC = 0 + lastlist = 0 + } + if (msglowercased == "/list" && Bukkit.getOnlinePlayers.size == lastlistp && { + ListC += 1 + ListC - 1 + } > 2) { // Lowered already + DPUtils.reply(message, SMono.empty, "stop it. You know the answer.").subscribe() + lastlist = 0 + lastlistp = Bukkit.getOnlinePlayers.size.toShort + return true //Handled + } + lastlistp = Bukkit.getOnlinePlayers.size.toShort //Didn't handle + if (!TBMCCoreAPI.IsTestServer && fm.serverReady.get.exists(msglowercased.contains)) { + var next = 0 + if (usableServerReadyStrings.size == 0) fm.createUsableServerReadyStrings() + next = usableServerReadyStrings.remove(serverReadyRandom.nextInt(usableServerReadyStrings.size)) + DPUtils.reply(message, SMono.empty, fm.serverReadyAnswers.get.get(next)).subscribe() + return false //Still process it as a command/mcchat if needed + } + false + } + + private var lasttime: Long = 0 + + def handleFullHouse(event: PresenceUpdateEvent): Unit = { + val fm = ComponentManager.getIfEnabled(classOf[FunModule]) + if (fm == null) return () + if (Calendar.getInstance.get(Calendar.DAY_OF_MONTH) % 5 != 0) return () + if (!Option(event.getOld.orElse(null)).exists(_.getStatus == Status.OFFLINE) + || event.getCurrent.getStatus == Status.OFFLINE) + return () //If it's not an offline -> online change + fm.fullHouseChannel.get.filter((ch: MessageChannel) => ch.isInstanceOf[GuildChannel]) + .flatMap(channel => fm.fullHouseDevRole(SMono(channel.asInstanceOf[GuildChannel].getGuild)).get + .filterWhen(devrole => SMono(event.getMember) + .flatMap(m => SFlux(m.getRoles).any(_.getId.asLong == devrole.getId.asLong))) + .filterWhen(devrole => SMono(event.getGuild) + .flatMapMany(g => SFlux(g.getMembers).filter(_.getRoleIds.stream.anyMatch(_ == devrole.getId))) + .flatMap(_.getPresence).all(_.getStatus != Status.OFFLINE)) + .filter(_ => lasttime + 10 < TimeUnit.NANOSECONDS.toHours(System.nanoTime)) //This should stay so it checks this last + .flatMap(_ => { + lasttime = TimeUnit.NANOSECONDS.toHours(System.nanoTime) + SMono(channel.createMessage(_.setContent("Full house!") + .setEmbed((ecs: LegacyEmbedCreateSpec) => ecs.setImage("https://cdn.discordapp.com/attachments/249295547263877121/249687682618359808/poker-hand-full-house-aces-kings-playing-cards-15553791.png")))) + })).subscribe() + } +} + +class FunModule extends Component[DiscordPlugin] with Listener { + /** + * Questions that the bot will choose a random answer to give to. + */ + final private val serverReady: ConfigData[Array[String]] = + getConfig.getData("serverReady", () => Array[String]( + "when will the server be open", "when will the server be ready", + "when will the server be done", "when will the server be complete", + "when will the server be finished", "when's the server ready", + "when's the server open", "vhen vill ze server be open?")) + /** + * Answers for a recognized question. Selected randomly. + */ + final private val serverReadyAnswers: ConfigData[util.ArrayList[String]] = + getConfig.getData("serverReadyAnswers", () => Lists.newArrayList(FunModule.serverReadyStrings: _*)) + + private def createUsableServerReadyStrings(): Unit = + IntStream.range(0, serverReadyAnswers.get.size).forEach((i: Int) => FunModule.usableServerReadyStrings.add(i.toShort)) + + override protected def enable(): Unit = registerListener(this) + + override protected def disable(): Unit = { + FunModule.lastlist = 0 + FunModule.lastlistp = 0 + FunModule.ListC = 0 + } + + @EventHandler def onPlayerJoin(event: PlayerJoinEvent): Unit = FunModule.ListC = 0 + + /** + * If all of the people who have this role are online, the bot will post a full house. + */ + private def fullHouseDevRole(guild: SMono[Guild]) = DPUtils.roleData(getConfig, "fullHouseDevRole", "Developer", guild) + + /** + * The channel to post the full house to. + */ + final private val fullHouseChannel = DPUtils.channelData(getConfig, "fullHouseChannel") +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/listeners/CommonListeners.scala b/src/main/scala/buttondevteam/discordplugin/listeners/CommonListeners.scala new file mode 100644 index 0000000..3b7da35 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/listeners/CommonListeners.scala @@ -0,0 +1,55 @@ +package buttondevteam.discordplugin.listeners + +import buttondevteam.discordplugin.fun.FunModule +import buttondevteam.discordplugin.mcchat.MinecraftChatModule +import buttondevteam.discordplugin.role.GameRoleModule +import buttondevteam.discordplugin.util.Timings +import buttondevteam.discordplugin.{DPUtils, DiscordPlugin} +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.architecture.Component +import discord4j.core.`object`.entity.Message +import discord4j.core.`object`.entity.channel.{MessageChannel, PrivateChannel} +import discord4j.core.event.EventDispatcher +import discord4j.core.event.domain.PresenceUpdateEvent +import discord4j.core.event.domain.interaction.{ChatInputInteractionEvent, MessageInteractionEvent} +import discord4j.core.event.domain.message.MessageCreateEvent +import discord4j.core.event.domain.role.{RoleCreateEvent, RoleDeleteEvent, RoleUpdateEvent} +import reactor.core.Disposable +import reactor.core.publisher.Mono +import reactor.core.scala.publisher.{SFlux, SMono} + +object CommonListeners { + val timings = new Timings + + def register(dispatcher: EventDispatcher): Unit = { + dispatcher.on(classOf[MessageCreateEvent]).flatMap((event: MessageCreateEvent) => { + SMono.just(event.getMessage).filter(_ => !DiscordPlugin.SafeMode) + .filter(message => message.getAuthor.filter(!_.isBot).isPresent) + .filter(message => !FunModule.executeMemes(message)) + .filterWhen(message => { + Option(Component.getComponents.get(classOf[MinecraftChatModule])).filter(_.isEnabled) + .map(_.asInstanceOf[MinecraftChatModule].getListener.handleDiscord(event)) + .getOrElse(SMono.just(true)) //Wasn't handled, continue + }) + }).onErrorContinue((err, _) => TBMCCoreAPI.SendException("An error occured while handling a message!", err, DiscordPlugin.plugin)).subscribe() + dispatcher.on(classOf[PresenceUpdateEvent]).subscribe((event: PresenceUpdateEvent) => { + if (!DiscordPlugin.SafeMode) + FunModule.handleFullHouse(event) + }) + SFlux(dispatcher.on(classOf[RoleCreateEvent])).subscribe(GameRoleModule.handleRoleEvent) + SFlux(dispatcher.on(classOf[RoleDeleteEvent])).subscribe(GameRoleModule.handleRoleEvent) + SFlux(dispatcher.on(classOf[RoleUpdateEvent])).subscribe(GameRoleModule.handleRoleEvent) + SFlux(dispatcher.on(classOf[ChatInputInteractionEvent], event => { + if(event.getCommandName() eq "help") + event.reply("Hello there") + else + Mono.empty() + })).subscribe() + } + + var debug = false + + def debug(debug: String): Unit = if (CommonListeners.debug) { //Debug + DPUtils.getLogger.info(debug) + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/listeners/MCListener.scala b/src/main/scala/buttondevteam/discordplugin/listeners/MCListener.scala new file mode 100644 index 0000000..a68a48e --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/listeners/MCListener.scala @@ -0,0 +1,51 @@ +package buttondevteam.discordplugin.listeners + +import buttondevteam.discordplugin.commands.ConnectCommand +import buttondevteam.discordplugin.mcchat.MinecraftChatModule +import buttondevteam.discordplugin.util.DPState +import buttondevteam.discordplugin.{DiscordPlayer, DiscordPlugin} +import buttondevteam.lib.ScheduledServerRestartEvent +import buttondevteam.lib.player.TBMCPlayerGetInfoEvent +import discord4j.common.util.Snowflake +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.{EventHandler, Listener} +import reactor.core.publisher.Mono +import reactor.core.scala.publisher.javaOptional2ScalaOption + +class MCListener extends Listener { + @EventHandler def onPlayerJoin(e: PlayerJoinEvent): Unit = + if (ConnectCommand.WaitingToConnect.containsKey(e.getPlayer.getName)) { + @SuppressWarnings(Array("ConstantConditions")) val user = DiscordPlugin.dc.getUserById(Snowflake.of(ConnectCommand.WaitingToConnect.get(e.getPlayer.getName))).block + if (user == null) return () + e.getPlayer.sendMessage("§bTo connect with the Discord account @" + user.getUsername + "#" + user.getDiscriminator + " do /discord accept") + e.getPlayer.sendMessage("§bIf it wasn't you, do /discord decline") + } + + @EventHandler def onGetInfo(e: TBMCPlayerGetInfoEvent): Unit = { + Option(DiscordPlugin.SafeMode).filterNot(identity).flatMap(_ => Option(e.getPlayer.getAs(classOf[DiscordPlayer]))) + .flatMap(dp => Option(dp.getDiscordID)).filter(_.nonEmpty) + .map(Snowflake.of).flatMap(id => DiscordPlugin.dc.getUserById(id).onErrorResume(_ => Mono.empty).blockOptional()) + .map(user => { + e.addInfo("Discord tag: " + user.getUsername + "#" + user.getDiscriminator) + user + }) + .flatMap(user => user.asMember(DiscordPlugin.mainServer.getId).onErrorResume(t => Mono.empty).blockOptional()) + .flatMap(member => member.getPresence.blockOptional()) + .map(pr => { + e.addInfo(pr.getStatus.toString) + pr + }) + .flatMap(_.getActivity).foreach(activity => e.addInfo(s"${activity.getType}: ${activity.getName}")) + } + + /*@EventHandler + public void onCommandPreprocess(TBMCCommandPreprocessEvent e) { + if (e.getMessage().equalsIgnoreCase("/stop")) + MinecraftChatModule.state = DPState.STOPPING_SERVER; + else if (e.getMessage().equalsIgnoreCase("/restart")) + MinecraftChatModule.state = DPState.RESTARTING_SERVER; + }*/ @EventHandler //We don't really need this with the logger stuff but hey + def onScheduledRestart(e: ScheduledServerRestartEvent): Unit = + MinecraftChatModule.state = DPState.RESTARTING_SERVER + +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/mcchat/ChannelconCommand.scala b/src/main/scala/buttondevteam/discordplugin/mcchat/ChannelconCommand.scala new file mode 100644 index 0000000..aa77081 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/mcchat/ChannelconCommand.scala @@ -0,0 +1,179 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.core.component.channel.{Channel, ChatRoom} +import buttondevteam.discordplugin.* +import buttondevteam.discordplugin.ChannelconBroadcast.ChannelconBroadcast +import buttondevteam.discordplugin.commands.{Command2DCSender, ICommand2DC} +import buttondevteam.lib.TBMCSystemChatEvent +import buttondevteam.lib.chat.{Command2, CommandClass} +import buttondevteam.lib.player.{ChromaGamerBase, TBMCPlayer} +import discord4j.core.`object`.entity.Message +import discord4j.core.`object`.entity.channel.{GuildChannel, MessageChannel} +import discord4j.rest.util.{Permission, PermissionSet} +import org.bukkit.Bukkit +import reactor.core.scala.publisher.SMono + +import java.lang.reflect.Method +import java.util +import java.util.function.Supplier +import java.util.stream.Collectors +import java.util.{Objects, Optional} +import javax.annotation.Nullable + +@SuppressWarnings(Array("SimplifyOptionalCallChains")) //Java 11 +@CommandClass(helpText = Array("Channel connect", // + "This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", + "You need to have access to the MC channel and have manage permissions on the Discord channel.", + "You also need to have your Minecraft account connected. In #bot use /connect .", + "Call this command from the channel you want to use.", "Usage: @Bot channelcon ", + "Use the ID (command) of the channel, for example `g` for the global chat.", + "To remove a connection use @ChromaBot channelcon remove in the channel.", + "Mentioning the bot is needed in this case because the / prefix only works in #bot.", + "Invite link: " // +)) +class ChannelconCommand(private val module: MinecraftChatModule) extends ICommand2DC { + @Command2.Subcommand def remove(sender: Command2DCSender): Boolean = { + val message = sender.getMessage + if (checkPerms(message, null)) return true + else if (MCChatCustom.removeCustomChat(message.getChannelId)) + DPUtils.reply(message, SMono.empty, "channel connection removed.").subscribe() + else + DPUtils.reply(message, SMono.empty, "this channel isn't connected.").subscribe() + true + } + + @Command2.Subcommand def toggle(sender: Command2DCSender, @Command2.OptionalArg toggle: String): Boolean = { + val message = sender.getMessage + if (checkPerms(message, null)) { + return true + } + val cc: MCChatCustom.CustomLMD = MCChatCustom.getCustomChat(message.getChannelId) + if (cc == null) { + return respond(sender, "this channel isn't connected.") + } + val togglesString: Supplier[String] = () => ChannelconBroadcast.values + .map(t => t.toString.toLowerCase + ": " + (if ((cc.toggles & (1 << t.id)) == 0) "disabled" else "enabled")) + .mkString("\n") + "\n\n" + + TBMCSystemChatEvent.BroadcastTarget.stream.map((target: TBMCSystemChatEvent.BroadcastTarget) => + target.getName + ": " + (if (cc.brtoggles.contains(target)) "enabled" else "disabled")) + .collect(Collectors.joining("\n")) + if (toggle == null) { + DPUtils.reply(message, SMono.empty, "toggles:\n" + togglesString.get).subscribe() + return true + } + val arg: String = toggle.toUpperCase + val b = ChannelconBroadcast.values.find((t: ChannelconBroadcast) => t.toString == arg) + if (b.isEmpty) { + val bt: TBMCSystemChatEvent.BroadcastTarget = TBMCSystemChatEvent.BroadcastTarget.get(arg) + if (bt == null) { + DPUtils.reply(message, SMono.empty, "cannot find toggle. Toggles:\n" + togglesString.get).subscribe() + return true + } + val add: Boolean = !(cc.brtoggles.contains(bt)) + if (add) { + cc.brtoggles += bt + } + else { + cc.brtoggles -= bt + } + return respond(sender, "'" + bt.getName + "' " + (if (add) "en" else "dis") + "abled") + } + //A B | F + //------- A: original - B: mask - F: new + //0 0 | 0 + //0 1 | 1 + //1 0 | 1 + //1 1 | 0 + // XOR + cc.toggles ^= (1 << b.get.id) + DPUtils.reply(message, SMono.empty, "'" + b.get.toString.toLowerCase + "' " + + (if ((cc.toggles & (1 << b.get.id)) == 0) "disabled" else "enabled")).subscribe() + true + } + + @Command2.Subcommand def `def`(sender: Command2DCSender, channelID: String): Boolean = { + val message = sender.getMessage + if (!(module.allowCustomChat.get)) { + sender.sendMessage("channel connection is not allowed on this Minecraft server.") + return true + } + val channel = message.getChannel.block + if (checkPerms(message, channel)) { + return true + } + if (MCChatCustom.hasCustomChat(message.getChannelId)) { + return respond(sender, "this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it.") + } + val chan: Optional[Channel] = Channel.getChannels.filter((ch: Channel) => ch.ID.equalsIgnoreCase(channelID) || (util.Arrays.stream(ch.IDs.get).anyMatch((cid: String) => cid.equalsIgnoreCase(channelID)))).findAny + if (!(chan.isPresent)) { //TODO: Red embed that disappears over time (kinda like the highlight messages in OW) + DPUtils.reply(message, channel, "MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /.").subscribe() + return true + } + if (!(message.getAuthor.isPresent)) { + return true + } + val author = message.getAuthor.get + val dp: DiscordPlayer = ChromaGamerBase.getUser(author.getId.asString, classOf[DiscordPlayer]) + val chp: TBMCPlayer = dp.getAs(classOf[TBMCPlayer]) + if (chp == null) { + DPUtils.reply(message, channel, "you need to connect your Minecraft account. On the main server in " + DPUtils.botmention + " do " + DiscordPlugin.getPrefix + "connect ").subscribe() + return true + } + val dcp: DiscordConnectedPlayer = DiscordConnectedPlayer.create(message.getAuthor.get, channel, chp.getUUID, Bukkit.getOfflinePlayer(chp.getUUID).getName, module) + //Using a fake player with no login/logout, should be fine for this event + val groupid: String = chan.get.getGroupID(dcp) + if (groupid == null && !((chan.get.isInstanceOf[ChatRoom]))) { //ChatRooms don't allow it unless the user joins, which happens later + DPUtils.reply(message, channel, "sorry, you cannot use that Minecraft channel.").subscribe() + return true + } + if (chan.get.isInstanceOf[ChatRoom]) { //ChatRooms don't work well + DPUtils.reply(message, channel, "chat rooms are not supported yet.").subscribe() + return true + } + /*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) { + DPUtils.reply(message, null, "sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm."); + return true; + }*/ + //TODO: "Channel admins" that can connect channels? + MCChatCustom.addCustomChat(channel, groupid, chan.get, author, dcp, 0, Set()) + if (chan.get.isInstanceOf[ChatRoom]) { + DPUtils.reply(message, channel, "alright, connection made to the room!").subscribe() + } + else { + DPUtils.reply(message, channel, "alright, connection made to group `" + groupid + "`!").subscribe() + } + true + } + + @SuppressWarnings(Array("ConstantConditions")) + private def checkPerms(message: Message, @Nullable channel: MessageChannel): Boolean = { + if (channel == null) { + return checkPerms(message, message.getChannel.block) + } + if (!((channel.isInstanceOf[GuildChannel]))) { + DPUtils.reply(message, channel, "you can only use this command in a server!").subscribe() + return true + } + //noinspection OptionalGetWithoutIsPresent + val perms: PermissionSet = (channel.asInstanceOf[GuildChannel]).getEffectivePermissions(message.getAuthor.map(_.getId).get).block + if (!(perms.contains(Permission.ADMINISTRATOR)) && !(perms.contains(Permission.MANAGE_CHANNELS))) { + DPUtils.reply(message, channel, "you need to have manage permissions for this channel!").subscribe() + return true + } + false + } + + override def getHelpText(method: Method, ann: Command2.Subcommand): Array[String] = + Array[String]( + "Channel connect", + "This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", + "You need to have access to the MC channel and have manage permissions on the Discord channel.", + "You also need to have your Minecraft account connected. In " + DPUtils.botmention + " use " + DiscordPlugin.getPrefix + "connect .", + "Call this command from the channel you want to use.", "Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf.block).getMention + " channelcon ", + "Use the ID (command) of the channel, for example `g` for the global chat.", + "To remove a connection use @ChromaBot channelcon remove in the channel.", + "Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix + " prefix only works in " + DPUtils.botmention + ".", + "Invite link: ") +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatCommand.scala b/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatCommand.scala new file mode 100644 index 0000000..74c27f0 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatCommand.scala @@ -0,0 +1,37 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.discordplugin.commands.{Command2DCSender, ICommand2DC} +import buttondevteam.discordplugin.{DPUtils, DiscordPlayer, DiscordPlugin} +import buttondevteam.lib.chat.{Command2, CommandClass} +import buttondevteam.lib.player.ChromaGamerBase +import discord4j.core.`object`.entity.channel.PrivateChannel + +@CommandClass(helpText = Array( + "MC Chat", + "This command enables or disables the Minecraft chat in private messages.", // + "It can be useful if you don't want your messages to be visible, for example when talking in a private channel.", + "You can also run all of the ingame commands you have access to using this command, if you have your accounts connected." // +)) +class MCChatCommand(private val module: MinecraftChatModule) extends ICommand2DC { + @Command2.Subcommand override def `def`(sender: Command2DCSender): Boolean = { + if (!module.allowPrivateChat.get) { + sender.sendMessage("using the private chat is not allowed on this Minecraft server.") + return true + } + val message = sender.getMessage + val channel = message.getChannel.block + @SuppressWarnings(Array("OptionalGetWithoutIsPresent")) val author = message.getAuthor.get + if (!channel.isInstanceOf[PrivateChannel]) { + DPUtils.reply(message, channel, "this command can only be issued in a direct message with the bot.").subscribe() + return true + } + val user: DiscordPlayer = ChromaGamerBase.getUser(author.getId.asString, classOf[DiscordPlayer]) + val mcchat: Boolean = !(user.isMinecraftChatEnabled) + MCChatPrivate.privateMCChat(channel, mcchat, author, user) + DPUtils.reply(message, channel, "Minecraft chat " + + (if (mcchat) "enabled. Use '" + DiscordPlugin.getPrefix + "mcchat' again to turn it off." + else "disabled.")).subscribe() + true + // TODO: Pin channel switching to indicate the current channel + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatCustom.scala b/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatCustom.scala new file mode 100644 index 0000000..df29986 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatCustom.scala @@ -0,0 +1,66 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.core.component.channel.{Channel, ChatRoom} +import buttondevteam.discordplugin.DiscordConnectedPlayer +import buttondevteam.lib.TBMCSystemChatEvent +import discord4j.common.util.Snowflake +import discord4j.core.`object`.entity.User +import discord4j.core.`object`.entity.channel.MessageChannel + +import javax.annotation.Nullable +import scala.collection.mutable.ListBuffer + +object MCChatCustom { + /** + * Used for town or nation chats or anything else + */ + private[mcchat] val lastmsgCustom = new ListBuffer[MCChatCustom.CustomLMD] + + def addCustomChat(channel: MessageChannel, groupid: String, mcchannel: Channel, user: User, dcp: DiscordConnectedPlayer, toggles: Int, brtoggles: Set[TBMCSystemChatEvent.BroadcastTarget]): Boolean = { + lastmsgCustom synchronized { + var gid: String = null + mcchannel match { + case room: ChatRoom => + room.joinRoom(dcp) + gid = if (groupid == null) mcchannel.getGroupID(dcp) else groupid + case _ => + gid = groupid + } + val lmd = new MCChatCustom.CustomLMD(channel, user, gid, mcchannel, dcp, toggles, brtoggles) + lastmsgCustom += lmd + } + true + } + + def hasCustomChat(channel: Snowflake): Boolean = + lastmsgCustom.exists(_.channel.getId.asLong == channel.asLong) + + @Nullable def getCustomChat(channel: Snowflake): CustomLMD = + lastmsgCustom.find(_.channel.getId.asLong == channel.asLong).orNull + + def removeCustomChat(channel: Snowflake): Boolean = { + lastmsgCustom synchronized { + MCChatUtils.lastmsgfromd.remove(channel.asLong) + val count = lastmsgCustom.size + lastmsgCustom.filterInPlace(lmd => { + if (lmd.channel.getId.asLong != channel.asLong) true + else { + lmd.mcchannel match { + case room: ChatRoom => room.leaveRoom(lmd.dcp) + case _ => + } + false + } + }) + lastmsgCustom.size < count + } + } + + def getCustomChats: List[CustomLMD] = lastmsgCustom.toList + + class CustomLMD private[mcchat](channel: MessageChannel, user: User, val groupID: String, + mcchannel: Channel, val dcp: DiscordConnectedPlayer, var toggles: Int, + var brtoggles: Set[TBMCSystemChatEvent.BroadcastTarget]) extends MCChatUtils.LastMsgData(channel, user, mcchannel) { + } + +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatListener.scala b/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatListener.scala new file mode 100644 index 0000000..c1c1af9 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatListener.scala @@ -0,0 +1,474 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.core.ComponentManager +import buttondevteam.discordplugin.* +import buttondevteam.discordplugin.DPUtils.SpecExtensions +import buttondevteam.discordplugin.playerfaker.{VanillaCommandListener, VanillaCommandListener14, VanillaCommandListener15} +import buttondevteam.lib.* +import buttondevteam.lib.chat.{ChatMessage, TBMCChatAPI} +import buttondevteam.lib.player.TBMCPlayer +import com.vdurmont.emoji.EmojiParser +import discord4j.common.util.Snowflake +import discord4j.core.`object`.entity.channel.{MessageChannel, PrivateChannel} +import discord4j.core.`object`.entity.{Member, Message, User} +import discord4j.core.event.domain.message.MessageCreateEvent +import discord4j.core.spec.legacy.{LegacyEmbedCreateSpec, LegacyMessageEditSpec} +import discord4j.rest.util.Color +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.event.{EventHandler, Listener} +import org.bukkit.scheduler.BukkitTask +import reactor.core.publisher.Mono +import reactor.core.scala.publisher.{SFlux, SMono} + +import java.time.Instant +import java.util +import java.util.concurrent.{LinkedBlockingQueue, TimeoutException} +import java.util.function.{Consumer, Predicate} +import java.util.stream.Collectors +import scala.jdk.CollectionConverters.{ListHasAsScala, SetHasAsScala} +import scala.jdk.OptionConverters.RichOptional + +object MCChatListener { + + // ......................DiscordSender....DiscordConnectedPlayer.DiscordPlayerSender + // Offline public chat......x............................................ + // Online public chat.......x...........................................x + // Offline private chat.....x.......................x.................... + // Online private chat......x.......................x...................x + // If online and enabling private chat, don't login + // If leaving the server and private chat is enabled (has ConnectedPlayer), call login in a task on lowest priority + // If private chat is enabled and joining the server, logout the fake player on highest priority + // If online and disabling private chat, don't logout + // The maps may not contain the senders for UnconnectedSenders + @FunctionalInterface private trait InterruptibleConsumer[T] { + @throws[TimeoutException] + @throws[InterruptedException] + def accept(value: T): Unit + } + +} + +class MCChatListener(val module: MinecraftChatModule) extends Listener { + private var sendtask: BukkitTask = null + final private val sendevents = new LinkedBlockingQueue[util.AbstractMap.SimpleEntry[TBMCChatEvent, Instant]] + private var sendrunnable: Runnable = null + private var sendthread: Thread = null + private var stop = false //A new instance will be created on enable + @EventHandler // Minecraft + def onMCChat(ev: TBMCChatEvent): Unit = { + if (!(ComponentManager.isEnabled(classOf[MinecraftChatModule])) || ev.isCancelled) { //SafeMode: Needed so it doesn't restart after server shutdown + return () + } + + sendevents.add(new util.AbstractMap.SimpleEntry[TBMCChatEvent, Instant](ev, Instant.now)) + if (sendtask != null) { + return () + } + sendrunnable = () => { + def foo(): Unit = { + sendthread = Thread.currentThread + processMCToDiscord() + if (DiscordPlugin.plugin.isEnabled && !(stop)) { //Don't run again if shutting down + sendtask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable) + } + } + + foo() + } + sendtask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable) + } + + private def processMCToDiscord(): Unit = { + try { + var e: TBMCChatEvent = null + var time: Instant = null + val se: util.AbstractMap.SimpleEntry[TBMCChatEvent, Instant] = sendevents.take // Wait until an element is available + e = se.getKey + time = se.getValue + val authorPlayer: String = "[" + DPUtils.sanitizeStringNoEscape(e.getChannel.DisplayName.get) + "] " + // + (if ("Minecraft" == e.getOrigin) "" else "[" + e.getOrigin.charAt(0) + "]") + + DPUtils.sanitizeStringNoEscape(ChromaUtils.getDisplayName(e.getSender)) + val color: chat.Color = e.getChannel.Color.get + val embed: Consumer[LegacyEmbedCreateSpec] = (ecs: LegacyEmbedCreateSpec) => { + def foo(ecs: LegacyEmbedCreateSpec) = { + ecs.setDescription(e.getMessage).setColor(Color.of(color.getRed, color.getGreen, color.getBlue)) + val url: String = module.profileURL.get + e.getSender match { + case player: Player => + DPUtils.embedWithHead(ecs, authorPlayer, e.getSender.getName, + if (url.nonEmpty) url + "?type=minecraft&id=" + player.getUniqueId else null) + case dsender: DiscordSenderBase => + ecs.setAuthor(authorPlayer, + if (url.nonEmpty) url + "?type=discord&id=" + dsender.getUser.getId.asString else null, + dsender.getUser.getAvatarUrl) + case _ => + DPUtils.embedWithHead(ecs, authorPlayer, e.getSender.getName, null) + } + ecs.setTimestamp(time) + } + + foo(ecs) + } + val nanoTime: Long = System.nanoTime + val doit = (lastmsgdata: MCChatUtils.LastMsgData) => { + if (lastmsgdata.message == null + || authorPlayer != lastmsgdata.message.getEmbeds.get(0).getAuthor.toScala.flatMap(_.getName.toScala).orNull + || lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120 + || !(lastmsgdata.mcchannel.ID == e.getChannel.ID) + || lastmsgdata.content.length + e.getMessage.length + 1 > 2048) { + lastmsgdata.message = lastmsgdata.channel.createEmbed(embed).block + lastmsgdata.time = nanoTime + lastmsgdata.mcchannel = e.getChannel + lastmsgdata.content = e.getMessage + } + else { + lastmsgdata.content = lastmsgdata.content + "\n" + e.getMessage // The message object doesn't get updated + lastmsgdata.message.edit((mes: LegacyMessageEditSpec) => mes.setEmbed(embed.andThen((ecs: LegacyEmbedCreateSpec) => ecs.setDescription(lastmsgdata.content))).^^()).block + } + } + // Checks if the given channel is different than where the message was sent from + // Or if it was from MC + val isdifferentchannel: Predicate[Snowflake] = (id: Snowflake) => !((e.getSender.isInstanceOf[DiscordSenderBase])) || (e.getSender.asInstanceOf[DiscordSenderBase]).getChannel.getId.asLong != id.asLong + if (e.getChannel.isGlobal && (e.isFromCommand || isdifferentchannel.test(module.chatChannel.get))) { + if (MCChatUtils.lastmsgdata == null) + MCChatUtils.lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannelMono.block(), null) + doit(MCChatUtils.lastmsgdata) + } + + for (data <- MCChatPrivate.lastmsgPerUser) { + if ((e.isFromCommand || isdifferentchannel.test(data.channel.getId)) && e.shouldSendTo(MCChatUtils.getSender(data.channel.getId, data.user))) { + doit(data) + } + } + MCChatCustom.lastmsgCustom synchronized { + MCChatCustom.lastmsgCustom.filterInPlace(lmd => { + if ((e.isFromCommand || isdifferentchannel.test(lmd.channel.getId)) //Test if msg is from Discord + && e.getChannel.ID == lmd.mcchannel.ID //If it's from a command, the command msg has been deleted, so we need to send it + && e.getGroupID() == lmd.groupID) { //Check if this is the group we want to test - #58 + if (e.shouldSendTo(lmd.dcp)) { //Check original user's permissions + doit(lmd) + true + } + else { + lmd.channel.createMessage("The user no longer has permission to view the channel, connection removed.").subscribe() + false //If the user no longer has permission, remove the connection + } + } + else true + }) + } + } catch { + case ex: InterruptedException => + //Stop if interrupted anywhere + sendtask.cancel() + sendtask = null + case ex: Exception => + TBMCCoreAPI.SendException("Error while sending message to Discord!", ex, module) + } + } + + @EventHandler def onChatPreprocess(event: TBMCChatPreprocessEvent): Unit = { + var start: Int = -(1) + while ( { + (start = event.getMessage.indexOf('@', start + 1), start) != ((), -1) + }) { + val mid: Int = event.getMessage.indexOf('#', start + 1) + if (mid == -1) { + return () + } + var end_ = event.getMessage.indexOf(' ', mid + 1) + if (end_ == -1) { + end_ = event.getMessage.length + } + val end: Int = end_ + val startF: Int = start + val user = DiscordPlugin.dc.getUsers.filter((u) => u.getUsername.equals(event.getMessage.substring(startF + 1, mid))).filter((u) => u.getDiscriminator.equals(event.getMessage.substring(mid + 1, end))).blockFirst + if (user != null) { //TODO: Nicknames + event.setMessage(event.getMessage.substring(0, startF) + "@" + user.getUsername + (if (event.getMessage.length > end) { + event.getMessage.substring(end) + } + else { + "" + })) // TODO: Add formatting + } + start = end // Skip any @s inside the mention + } + } + + /** + * Stop the listener permanently. Enabling the module will create a new instance. + * + * @param wait Wait 5 seconds for the threads to stop + */ + def stop(wait: Boolean): Unit = { + stop = true + MCChatPrivate.logoutAll() + MCChatUtils.LoggedInPlayers.clear() + if (sendthread != null) { + sendthread.interrupt() + } + if (recthread != null) { + recthread.interrupt() + } + try { + if (sendthread != null) { + sendthread.interrupt() + if (wait) { + sendthread.join(5000) + } + } + if (recthread != null) { + recthread.interrupt() + if (wait) { + recthread.join(5000) + } + } + MCChatUtils.lastmsgdata = null + MCChatPrivate.lastmsgPerUser.clear() + MCChatCustom.lastmsgCustom.clear() + MCChatUtils.lastmsgfromd.clear() + MCChatUtils.UnconnectedSenders.clear() + recthread = null + sendthread = null + } catch { + case e: InterruptedException => + e.printStackTrace() //This thread shouldn't be interrupted + } + } + + private var rectask: BukkitTask = null + final private val recevents: LinkedBlockingQueue[MessageCreateEvent] = new LinkedBlockingQueue[MessageCreateEvent] + private var recrun: Runnable = null + private var recthread: Thread = null + + // Discord + def handleDiscord(ev: MessageCreateEvent): SMono[Boolean] = { + val author = Option(ev.getMessage.getAuthor.orElse(null)) + val hasCustomChat = MCChatCustom.hasCustomChat(ev.getMessage.getChannelId) + val prefix = DiscordPlugin.getPrefix + SMono(ev.getMessage.getChannel) + .filter(channel => isChatEnabled(channel, author, hasCustomChat)) + .filter(channel => !isRunningMCChatCommand(channel, ev.getMessage.getContent, prefix)) + .filter(channel => { + MCChatUtils.resetLastMessage(channel) + recevents.add(ev) + if (rectask == null) { + recrun = () => { + recthread = Thread.currentThread + processDiscordToMC() + if (DiscordPlugin.plugin.isEnabled && !(stop)) { + rectask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, recrun) //Continue message processing + } + } + rectask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, recrun) //Start message processing + } + true + }).map(_ => false).defaultIfEmpty(true) + } + + private def isChatEnabled(channel: MessageChannel, author: Option[User], hasCustomChat: Boolean) = { + def hasPrivateChat = channel.isInstanceOf[PrivateChannel] && + author.exists((u: User) => MCChatPrivate.isMinecraftChatEnabled(u.getId.asString)) + + def hasPublicChat = channel.getId.asLong == module.chatChannel.get.asLong + + hasPublicChat || hasPrivateChat || hasCustomChat + } + + private def isRunningMCChatCommand(channel: MessageChannel, content: String, prefix: Char) = { + (channel.isInstanceOf[PrivateChannel] //Only in private chat + && content.length < "/mcchat<>".length + && content.replace(prefix + "", "").equalsIgnoreCase("mcchat")) //Either mcchat or /mcchat + //Allow disabling the chat if needed + } + + private def processDiscordToMC(): Unit = { + var event: MessageCreateEvent = null + try event = recevents.take + catch { + case _: InterruptedException => + rectask.cancel() + return () + } + val sender: User = event.getMessage.getAuthor.orElse(null) + var dmessage: String = event.getMessage.getContent + try { + val dsender: DiscordSenderBase = MCChatUtils.getSender(event.getMessage.getChannelId, sender) + val user: DiscordPlayer = dsender.getChromaUser + + def replaceUserMentions(): Unit = { + for (u <- event.getMessage.getUserMentions.asScala) { //TODO: Role mentions + dmessage = dmessage.replace(u.getMention, "@" + u.getUsername) // TODO: IG Formatting + val m = u.asMember(DiscordPlugin.mainServer.getId).onErrorResume(_ => Mono.empty).blockOptional + if (m.isPresent) { + val mm: Member = m.get + val nick: String = mm.getDisplayName + dmessage = dmessage.replace(mm.getNicknameMention, "@" + nick) + } + } + } + + replaceUserMentions() + + def replaceChannelMentions(): Unit = { + for (ch <- SFlux(event.getGuild.flux).flatMap(_.getChannels).toIterable()) { + dmessage = dmessage.replace(ch.getMention, "#" + ch.getName) + } + } + + replaceChannelMentions() + + def replaceEmojis(): Unit = { + dmessage = EmojiParser.parseToAliases(dmessage, EmojiParser.FitzpatrickAction.PARSE) //Converts emoji to text- TODO: Add option to disable (resource pack?) + dmessage = dmessage.replaceAll(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:") //Convert to Discord's format so it still shows up + dmessage = dmessage.replaceAll("", ":$1:") //We don't need info about the custom emojis, just display their text + } + + replaceEmojis() + val clmd = MCChatCustom.getCustomChat(event.getMessage.getChannelId) + val sendChannel = event.getMessage.getChannel.block + val isPrivate = sendChannel.isInstanceOf[PrivateChannel] + + def addCheckmark() = { + try { + val lmfd = MCChatUtils.lastmsgfromd.get(event.getMessage.getChannelId.asLong) + if (lmfd != null) + lmfd.removeSelfReaction(DiscordPlugin.DELIVERED_REACTION).subscribe // Remove it no matter what, we know it's there 99.99% of the time + } catch { + case e: Exception => + TBMCCoreAPI.SendException("An error occured while removing reactions from chat!", e, module) + } + MCChatUtils.lastmsgfromd.put(event.getMessage.getChannelId.asLong, event.getMessage) + event.getMessage.addReaction(DiscordPlugin.DELIVERED_REACTION).subscribe() + } + + if (dmessage.startsWith("/")) // Ingame command + handleIngameCommand(event, dmessage, dsender, user, clmd, isPrivate) + else if (handleIngameMessage(event, dmessage, dsender, user, clmd, isPrivate)) // Not a command + addCheckmark() + } catch { + case e: Exception => + TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e, module) + } + } + + /** + * Handles a message coming from Discord to Minecraft. + * + * @param event The Discord event + * @param dmessage The message itself + * @param dsender The sender who sent it + * @param user The Chroma user of the sender + * @param clmd Custom chat last message data (if in a custom chat) + * @param isPrivate Whether the chat is private + * @return Whether the bot should react with a checkmark + */ + private def handleIngameMessage(event: MessageCreateEvent, dmessage: String, dsender: DiscordSenderBase, user: DiscordPlayer, + clmd: MCChatCustom.CustomLMD, isPrivate: Boolean): Boolean = { + def getAttachmentText = { + val att = event.getMessage.getAttachments.asScala + if (att.nonEmpty) att map (_.getUrl) mkString "\n" + else "" + } + + if (event.getMessage.getType eq Message.Type.CHANNEL_PINNED_MESSAGE) { + val mcchannel = if (clmd != null) clmd.mcchannel else dsender.getChromaUser.channel.get + val rtr = mcchannel getRTR (if (clmd != null) clmd.dcp else dsender) + TBMCChatAPI.SendSystemMessage(mcchannel, rtr, (dsender match { + case player: Player => player.getDisplayName + case _ => dsender.getName + }) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL) + false + } + else { + val cmb = ChatMessage.builder(dsender, user, dmessage + getAttachmentText).fromCommand(false) + if (clmd != null) + TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build, clmd.mcchannel) + else + TBMCChatAPI.SendChatMessage(cmb.build) + true + } + } + + /** + * Handle a Minecraft command coming from Discord. + * + * @param event The Discord event + * @param dmessage The Discord mewsage, starting with a slash + * @param dsender The sender who sent it + * @param user The Chroma user of the sender + * @param clmd The custom last message data (if in a custom chat) + * @param isPrivate Whether the chat is private + * @return + */ + private def handleIngameCommand(event: MessageCreateEvent, dmessage: String, dsender: DiscordSenderBase, user: DiscordPlayer, + clmd: MCChatCustom.CustomLMD, isPrivate: Boolean): Unit = { + def notWhitelisted(cmd: String) = module.whitelistedCommands.get.stream + .noneMatch(s => cmd == s || cmd.startsWith(s + " ")) + + def whitelistedCommands = module.whitelistedCommands.get.stream + .map("/" + _).collect(Collectors.joining(", ")) + + if (!isPrivate) + event.getMessage.delete.subscribe() + val cmd = dmessage.substring(1) + val cmdlowercased = cmd.toLowerCase + if (dsender.isInstanceOf[DiscordSender] && notWhitelisted(cmdlowercased)) { // Command not whitelisted + dsender.sendMessage("Sorry, you can only access these commands from here:\n" + whitelistedCommands + + (if (user.getConnectedID(classOf[TBMCPlayer]) == null) + "\nTo access your commands, first please connect your accounts, using /connect in " + DPUtils.botmention + + "\nThen y" else "\nY") + "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!") + return () + } + module.log(dsender.getName + " ran from DC: /" + cmd) + if (dsender.isInstanceOf[DiscordSender] && runCustomCommand(dsender, cmdlowercased)) { + return () + } + val channel = if (clmd == null) user.channel.get else clmd.mcchannel + val ev = new TBMCCommandPreprocessEvent(dsender, channel, dmessage, if (clmd == null) dsender else clmd.dcp) + Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => { //Commands need to be run sync + Bukkit.getPluginManager.callEvent(ev) + if (!ev.isCancelled) + runMCCommand(dsender, cmd) + }) + } + + private def runMCCommand(dsender: DiscordSenderBase, cmd: String): Unit = { + try { + val mcpackage = Bukkit.getServer.getClass.getPackage.getName + if (!module.enableVanillaCommands.get) + Bukkit.dispatchCommand(dsender, cmd) + else if (mcpackage.contains("1_12")) + VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd) + else if (mcpackage.contains("1_14")) + VanillaCommandListener14.runBukkitOrVanillaCommand(dsender, cmd) + else if (mcpackage.contains("1_15") || mcpackage.contains("1_16")) + VanillaCommandListener15.runBukkitOrVanillaCommand(dsender, cmd) + else + Bukkit.dispatchCommand(dsender, cmd) + } catch { + case e: NoClassDefFoundError => + TBMCCoreAPI.SendException("A class is not found when trying to run command " + cmd + "!", e, module) + case e: Exception => + TBMCCoreAPI.SendException("An error occurred when trying to run command " + cmd + "! Vanilla commands are only supported in some MC versions.", e, module) + } + } + + /** + * Handles custom public commands. Used to hide sensitive information in public chats. + * + * @param dsender The Discord sender + * @param cmdlowercased The command, lowercased + * @return Whether the command was a custom command + */ + private def runCustomCommand(dsender: DiscordSenderBase, cmdlowercased: String): Boolean = { + if (cmdlowercased.startsWith("list")) { + val players = Bukkit.getOnlinePlayers + dsender.sendMessage("There are " + players.stream.filter(MCChatUtils.checkEssentials).count + " out of " + Bukkit.getMaxPlayers + " players online.") + dsender.sendMessage("Players: " + players.stream.filter(MCChatUtils.checkEssentials).map(_.getDisplayName).collect(Collectors.joining(", "))) + true + } + else false + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatPrivate.scala b/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatPrivate.scala new file mode 100644 index 0000000..773113f --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatPrivate.scala @@ -0,0 +1,78 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.core.ComponentManager +import buttondevteam.discordplugin.mcchat.MCChatUtils.LastMsgData +import buttondevteam.discordplugin.{DiscordConnectedPlayer, DiscordPlayer, DiscordPlugin, DiscordSenderBase} +import buttondevteam.lib.player.TBMCPlayer +import discord4j.core.`object`.entity.User +import discord4j.core.`object`.entity.channel.{MessageChannel, PrivateChannel} +import org.bukkit.Bukkit + +import scala.collection.mutable.ListBuffer +import scala.jdk.javaapi.CollectionConverters.asScala + +object MCChatPrivate { + /** + * Used for messages in PMs (mcchat). + */ + private[mcchat] var lastmsgPerUser: ListBuffer[LastMsgData] = ListBuffer() + + def privateMCChat(channel: MessageChannel, start: Boolean, user: User, dp: DiscordPlayer): Unit = { + MCChatUtils.ConnectedSenders synchronized { + val mcp = dp.getAs(classOf[TBMCPlayer]) + if (mcp != null) { // If the accounts aren't connected, can't make a connected sender + val p = Bukkit.getPlayer(mcp.getUUID) + val op = Bukkit.getOfflinePlayer(mcp.getUUID) + val mcm = ComponentManager.getIfEnabled(classOf[MinecraftChatModule]) + if (start) { + val sender = DiscordConnectedPlayer.create(user, channel, mcp.getUUID, op.getName, mcm) + MCChatUtils.addSender(MCChatUtils.ConnectedSenders, user, sender) + MCChatUtils.LoggedInPlayers.put(mcp.getUUID, sender) + if (p == null) { // Player is offline - If the player is online, that takes precedence + MCChatUtils.callLoginEvents(sender) + } + } + else { + val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel.getId, user) + assert(sender != null) + Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => { + def foo(): Unit = { + if ((p == null || p.isInstanceOf[DiscordSenderBase]) // Player is offline - If the player is online, that takes precedence + && sender.isLoggedIn) { //Don't call the quit event if login failed + MCChatUtils.callLogoutEvent(sender, needsSync = false) //The next line has to run *after* this one, so can't use the needsSync parameter + } + + MCChatUtils.LoggedInPlayers.remove(sender.getUniqueId) + sender.setLoggedIn(false) + } + + foo() + } + ) + } + // ---- PermissionsEx warning is normal on logout ---- + } + if (!start) MCChatUtils.lastmsgfromd.remove(channel.getId.asLong) + if (start) lastmsgPerUser += new MCChatUtils.LastMsgData(channel, user) // Doesn't support group DMs + else lastmsgPerUser.filterInPlace(_.channel.getId.asLong != channel.getId.asLong) //Remove + } + } + + def isMinecraftChatEnabled(dp: DiscordPlayer): Boolean = isMinecraftChatEnabled(dp.getDiscordID) + + def isMinecraftChatEnabled(did: String): Boolean = { // Don't load the player data just for this + lastmsgPerUser.exists(_.channel.asInstanceOf[PrivateChannel].getRecipientIds.stream.anyMatch(u => u.asString == did)) + } + + def logoutAll(): Unit = { + MCChatUtils.ConnectedSenders synchronized { + for ((_, userMap) <- MCChatUtils.ConnectedSenders) { + for (valueEntry <- asScala(userMap.entrySet)) { + if (MCChatUtils.getSender(MCChatUtils.OnlineSenders, valueEntry.getKey, valueEntry.getValue.getUser) == null) { //If the player is online then the fake player was already logged out + MCChatUtils.callLogoutEvent(valueEntry.getValue, !Bukkit.isPrimaryThread) + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatUtils.scala b/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatUtils.scala new file mode 100644 index 0000000..b76182f --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/mcchat/MCChatUtils.scala @@ -0,0 +1,363 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.core.{ComponentManager, MainPlugin, component} +import buttondevteam.discordplugin.* +import buttondevteam.discordplugin.ChannelconBroadcast.ChannelconBroadcast +import buttondevteam.discordplugin.DPUtils.SpecExtensions +import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule +import buttondevteam.discordplugin.mcchat.MCChatCustom.CustomLMD +import buttondevteam.lib.{TBMCCoreAPI, TBMCSystemChatEvent} +import com.google.common.collect.Sets +import discord4j.common.util.Snowflake +import discord4j.core.`object`.entity.channel.{Channel, MessageChannel, PrivateChannel, TextChannel} +import discord4j.core.`object`.entity.{Message, User} +import discord4j.core.spec.legacy.LegacyTextChannelEditSpec +import io.netty.util.collection.LongObjectHashMap +import org.bukkit.Bukkit +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import org.bukkit.event.Event +import org.bukkit.event.player.{AsyncPlayerPreLoginEvent, PlayerJoinEvent, PlayerLoginEvent, PlayerQuitEvent} +import org.bukkit.plugin.AuthorNagException +import reactor.core.publisher.{Flux as JFlux, Mono as JMono} +import reactor.core.scala.publisher.SMono + +import java.net.InetAddress +import java.util +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger +import java.util.logging.Level +import java.util.stream.Collectors +import javax.annotation.Nullable +import scala.collection.concurrent +import scala.collection.convert.ImplicitConversions.`map AsJavaMap` +import scala.collection.mutable.ListBuffer +import scala.jdk.CollectionConverters.{CollectionHasAsScala, SeqHasAsJava} +import scala.jdk.javaapi.CollectionConverters.asScala + +object MCChatUtils { + /** + * May contain P<DiscordID> as key for public chat + */ + val UnconnectedSenders = asScala(new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordSender]]) + val ConnectedSenders = asScala(new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordConnectedPlayer]]) + val OnlineSenders = asScala(new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordPlayerSender]]) + val LoggedInPlayers = asScala(new ConcurrentHashMap[UUID, DiscordConnectedPlayer]) + @Nullable private[mcchat] var lastmsgdata: MCChatUtils.LastMsgData = null + private[mcchat] val lastmsgfromd = new LongObjectHashMap[Message] // Last message sent by a Discord user, used for clearing checkmarks + private var module: MinecraftChatModule = null + private val staticExcludedPlugins: concurrent.Map[Class[_ <: Event], util.HashSet[String]] = concurrent.TrieMap() + + def updatePlayerList(): Unit = { + val mod = getModule + if (mod == null || !mod.showPlayerListOnDC.get) return () + if (lastmsgdata != null) updatePL(lastmsgdata) + MCChatCustom.lastmsgCustom.foreach(MCChatUtils.updatePL) + } + + private def notEnabled = (module == null || !module.disabling) && getModule == null //Allow using things while disabling the module + + private def getModule = { + if (module == null || !module.isEnabled) module = ComponentManager.getIfEnabled(classOf[MinecraftChatModule]) + //If disabled, it will try to get it again because another instance may be enabled - useful for /discord restart + module + } + + private def updatePL(lmd: MCChatUtils.LastMsgData): Unit = { + if (!lmd.channel.isInstanceOf[TextChannel]) { + TBMCCoreAPI.SendException("Failed to update player list for channel " + lmd.channel.getId, new Exception("The channel isn't a (guild) text channel."), getModule) + return () + } + var topic = lmd.channel.asInstanceOf[TextChannel].getTopic.orElse("") + if (topic.isEmpty) topic = ".\n----\nMinecraft chat\n----\n." + val s = topic.split("\\n----\\n") + if (s.length < 3) return () + var gid: String = null + lmd match { + case clmd: CustomLMD => gid = clmd.groupID + case _ => //If we're not using a custom chat then it's either can ("everyone") or can't (null) see at most + gid = buttondevteam.core.component.channel.Channel.GROUP_EVERYONE // (Though it's a public chat then rn) + } + val C = new AtomicInteger + s(s.length - 1) = "Players: " + Bukkit.getOnlinePlayers.stream.filter(p => if (lmd.mcchannel == null) { + gid == buttondevteam.core.component.channel.Channel.GROUP_EVERYONE //If null, allow if public (custom chats will have their channel stored anyway) + } + else { + gid == lmd.mcchannel.getGroupID(p) + } + ).filter(MCChatUtils.checkEssentials) //If they can see it + .filter(_ => C.incrementAndGet > 0) //Always true + .map((p) => DPUtils.sanitizeString(p.getDisplayName)).collect(Collectors.joining(", ")) + s(0) = s"$C player${if (C.get != 1) "s" else ""} online" + lmd.channel.asInstanceOf[TextChannel].edit((tce: LegacyTextChannelEditSpec) => + tce.setTopic(String.join("\n----\n", s: _*)).setReason("Player list update").^^()).subscribe //Don't wait + } + + private[mcchat] def checkEssentials(p: Player): Boolean = { + val ess = MainPlugin.ess + if (ess == null) return true + !ess.getUser(p).isHidden + } + + def addSender[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], user: User, sender: T): T = + addSender(senders, user.getId.asString, sender) + + def addSender[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], did: String, sender: T): T = { + val mapOpt = senders.get(did) + val map = if (mapOpt.isEmpty) new ConcurrentHashMap[Snowflake, T] else mapOpt.get + map.put(sender.getChannel.getId, sender) + senders.put(did, map) + sender + } + + def getSender[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], channel: Snowflake, user: User): T = { + val mapOpt = senders.get(user.getId.asString) + if (mapOpt.nonEmpty) mapOpt.get.get(channel) + else null.asInstanceOf + } + + def removeSender[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], channel: Snowflake, user: User): T = { + val mapOpt = senders.get(user.getId.asString) + if (mapOpt.nonEmpty) mapOpt.get.remove(channel) + else null.asInstanceOf + } + + def forPublicPrivateChat(action: SMono[MessageChannel] => SMono[_]): SMono[_] = { + if (notEnabled) return SMono.empty + val list = MCChatPrivate.lastmsgPerUser.map(data => action(SMono.just(data.channel))) + .prepend(action(module.chatChannelMono)) + SMono(JMono.whenDelayError(list.asJava)) + } + + /** + * For custom and all MC chat + * + * @param action The action to act (cannot complete empty) + * @param toggle The toggle to check + * @param hookmsg Whether the message is also sent from the hook + */ + def forCustomAndAllMCChat(action: SMono[MessageChannel] => SMono[_], @Nullable toggle: ChannelconBroadcast, hookmsg: Boolean): SMono[_] = { + if (notEnabled) return SMono.empty + val list = List(if (!GeneralEventBroadcasterModule.isHooked || !hookmsg) + forPublicPrivateChat(action) else SMono.empty) ++ + (if (toggle == null) MCChatCustom.lastmsgCustom + else MCChatCustom.lastmsgCustom.filter(cc => (cc.toggles & (1 << toggle.id)) != 0)) + .map(_.channel).map(SMono.just).map(action) + SMono(JMono.whenDelayError(list.asJava)) + } + + /** + * Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled. + * + * @param action The action to do + * @param sender The sender to check perms of or null to send to all that has it toggled + * @param toggle The toggle to check or null to send to all allowed + */ + def forAllowedCustomMCChat(action: SMono[MessageChannel] => SMono[_], @Nullable sender: CommandSender, @Nullable toggle: ChannelconBroadcast): SMono[_] = { + if (notEnabled) return SMono.empty + val st = MCChatCustom.lastmsgCustom.filter(clmd => { //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 & (1 << toggle.id)) == 0)) false //If null then allow + else if (sender == null) true + else clmd.groupID.equals(clmd.mcchannel.getGroupID(sender)) + }).map(cc => action.apply(SMono.just(cc.channel))) //TODO: Send error messages on channel connect + //Mono.whenDelayError((() => st.iterator).asInstanceOf[java.lang.Iterable[Mono[_]]]) //Can't convert as an iterator or inside the stream, but I can convert it as a stream + SMono.whenDelayError(st) + } + + /** + * Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled. + * + * @param action The action to do + * @param sender The sender to check perms of or null to send to all that has it toggled + * @param toggle The toggle to check or null to send to all allowed + * @param hookmsg Whether the message is also sent from the hook + */ + def forAllowedCustomAndAllMCChat(action: SMono[MessageChannel] => SMono[_], @Nullable sender: CommandSender, @Nullable toggle: ChannelconBroadcast, hookmsg: Boolean): SMono[_] = { + if (notEnabled) return SMono.empty + val cc = forAllowedCustomMCChat(action, sender, toggle) + if (!GeneralEventBroadcasterModule.isHooked || !hookmsg) return SMono.whenDelayError(Array(forPublicPrivateChat(action), cc)) + SMono.whenDelayError(Array(cc)) + } + + def send(message: String): SMono[MessageChannel] => SMono[_] = _.flatMap((mc: MessageChannel) => { + resetLastMessage(mc) + SMono(mc.createMessage(DPUtils.sanitizeString(message))) + }) + + def forAllowedMCChat(action: SMono[MessageChannel] => SMono[_], event: TBMCSystemChatEvent): SMono[_] = { + if (notEnabled) return SMono.empty + val list = new ListBuffer[SMono[_]] + if (event.getChannel.isGlobal) list.append(action(module.chatChannelMono)) + for (data <- MCChatPrivate.lastmsgPerUser) + if (event.shouldSendTo(getSender(data.channel.getId, data.user))) + list.append(action(SMono.just(data.channel))) //TODO: Only store ID? + MCChatCustom.lastmsgCustom.filter(clmd => + clmd.brtoggles.contains(event.getTarget) && event.shouldSendTo(clmd.dcp)) + .map(clmd => action(SMono.just(clmd.channel))).foreach(elem => { + list.append(elem) + () + }) + SMono.whenDelayError(list) + } + + /** + * This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc. + */ + private[mcchat] def getSender(channel: Snowflake, author: User): DiscordSenderBase = { //noinspection OptionalGetWithoutIsPresent + Option(getSender(OnlineSenders, channel, author)) // Find first non-null + .orElse(Option(getSender(ConnectedSenders, channel, author))) // This doesn't support the public chat, but it'll always return null for it + .orElse(Option(getSender(UnconnectedSenders, channel, author))) // + .orElse(Option(addSender(UnconnectedSenders, author, + new DiscordSender(author, SMono(DiscordPlugin.dc.getChannelById(channel)).block().asInstanceOf[MessageChannel])))) + .get + } + + /** + * Resets the last message, so it will start a new one instead of appending to it. + * This is used when someone (even the bot) sends a message to the channel. + * + * @param channel The channel to reset in - the process is slightly different for the public, private and custom chats + */ + def resetLastMessage(channel: Channel): Unit = { + if (notEnabled) return () + if (channel.getId.asLong == module.chatChannel.get.asLong) { + if (lastmsgdata == null) lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannelMono.block(), null) + else lastmsgdata.message = null + return () + } // Don't set the whole object to null, the player and channel information should be preserved + for (data <- if (channel.isInstanceOf[PrivateChannel]) MCChatPrivate.lastmsgPerUser + else MCChatCustom.lastmsgCustom) { + if (data.channel.getId.asLong == channel.getId.asLong) { + data.message = null + return () + } + } + //If it gets here, it's sending a message to a non-chat channel + } + + def addStaticExcludedPlugin(event: Class[_ <: Event], plugin: String): util.HashSet[String] = + staticExcludedPlugins.compute(event, (_, hs: util.HashSet[String]) => + if (hs == null) Sets.newHashSet(plugin) else if (hs.add(plugin)) hs else hs) + + def callEventExcludingSome(event: Event): Unit = { + if (notEnabled) return () + val second = staticExcludedPlugins.get(event.getClass) + val first = module.excludedPlugins.get + val both = if (second.isEmpty) first + else util.Arrays.copyOf(first, first.length + second.size) + var i = first.length + if (second.nonEmpty) { + for (plugin <- second.get.asScala) { + both(i) = plugin + i += 1 + } + } + callEventExcluding(event, false, both: _*) + } + + /** + * Calls an event with the given details. + *

+ * This method only synchronizes when the event is not asynchronous. + * + * @param event Event details + * @param only Flips the operation and includes the listed plugins + * @param plugins The plugins to exclude. Not case sensitive. + */ + def callEventExcluding(event: Event, only: Boolean, plugins: String*): Unit = { // Copied from Spigot-API and modified a bit + if (event.isAsynchronous) { + if (Thread.holdsLock(Bukkit.getPluginManager)) throw new IllegalStateException(event.getEventName + " cannot be triggered asynchronously from inside synchronized code.") + if (Bukkit.getServer.isPrimaryThread) throw new IllegalStateException(event.getEventName + " cannot be triggered asynchronously from primary server thread.") + fireEventExcluding(event, only, plugins: _*) + } + else Bukkit.getPluginManager synchronized fireEventExcluding(event, only, plugins: _*) + } + + private def fireEventExcluding(event: Event, only: Boolean, plugins: String*): Unit = { + val handlers = event.getHandlers // Code taken from SimplePluginManager in Spigot-API + val listeners = handlers.getRegisteredListeners + val server = Bukkit.getServer + for (registration <- listeners) { // Modified to exclude plugins + if (registration.getPlugin.isEnabled + && !plugins.exists(only ^ _.equalsIgnoreCase(registration.getPlugin.getName))) try registration.callEvent(event) + catch { + case ex: AuthorNagException => + val plugin = registration.getPlugin + if (plugin.isNaggable) { + plugin.setNaggable(false) + server.getLogger.log(Level.SEVERE, String.format("Nag author(s): '%s' of '%s' about the following: %s", plugin.getDescription.getAuthors, plugin.getDescription.getFullName, ex.getMessage)) + } + case ex: Throwable => + server.getLogger.log(Level.SEVERE, "Could not pass event " + event.getEventName + " to " + registration.getPlugin.getDescription.getFullName, ex) + } + } + } + + /** + * Call it from an async thread. + */ + def callLoginEvents(dcp: DiscordConnectedPlayer): Unit = { + val loginFail = (kickMsg: String) => { + dcp.sendMessage("Minecraft chat disabled, as the login failed: " + kickMsg) + MCChatPrivate.privateMCChat(dcp.getChannel, start = false, dcp.getUser, dcp.getChromaUser) + } //Probably also happens if the user is banned or so + val event = new AsyncPlayerPreLoginEvent(dcp.getName, InetAddress.getLoopbackAddress, dcp.getUniqueId) + callEventExcludingSome(event) + if (event.getLoginResult ne AsyncPlayerPreLoginEvent.Result.ALLOWED) { + loginFail(event.getKickMessage) + return () + } + Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => { + def foo(): Unit = { + val ev = new PlayerLoginEvent(dcp, "localhost", InetAddress.getLoopbackAddress) + callEventExcludingSome(ev) + if (ev.getResult ne PlayerLoginEvent.Result.ALLOWED) { + loginFail(ev.getKickMessage) + return () + } + callEventExcludingSome(new PlayerJoinEvent(dcp, "")) + dcp.setLoggedIn(true) + if (module != null) { + if (module.serverWatcher != null) module.serverWatcher.fakePlayers.add(dcp) + module.log(dcp.getName + " (" + dcp.getUniqueId + ") logged in from Discord") + } + } + + foo() + }) + } + + /** + * Only calls the events if the player is actually logged in + * + * @param dcp The player + * @param needsSync Whether we're in an async thread + */ + def callLogoutEvent(dcp: DiscordConnectedPlayer, needsSync: Boolean): Unit = { + if (!dcp.isLoggedIn) return () + val event = new PlayerQuitEvent(dcp, "") + if (needsSync) callEventSync(event) + else callEventExcludingSome(event) + dcp.setLoggedIn(false) + if (module != null) { + module.log(dcp.getName + " (" + dcp.getUniqueId + ") logged out from Discord") + if (module.serverWatcher != null) module.serverWatcher.fakePlayers.remove(dcp) + } + } + + private[mcchat] def callEventSync(event: Event) = Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => callEventExcludingSome(event)) + + class LastMsgData(val channel: MessageChannel, val user: User) { + var message: Message = null + var time = 0L + var content: String = null + var mcchannel: component.channel.Channel = null + + protected def this(channel: MessageChannel, user: User, mcchannel: component.channel.Channel) = { + this(channel, user) + this.mcchannel = mcchannel + } + } + +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/mcchat/MCListener.scala b/src/main/scala/buttondevteam/discordplugin/mcchat/MCListener.scala new file mode 100644 index 0000000..5782f0e --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/mcchat/MCListener.scala @@ -0,0 +1,142 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.discordplugin.* +import buttondevteam.discordplugin.DPUtils.{FluxExtensions, MonoExtensions} +import buttondevteam.lib.TBMCSystemChatEvent +import buttondevteam.lib.player.{TBMCPlayer, TBMCPlayerBase, TBMCYEEHAWEvent} +import discord4j.common.util.Snowflake +import discord4j.core.`object`.entity.Role +import discord4j.core.`object`.entity.channel.MessageChannel +import net.ess3.api.events.{AfkStatusChangeEvent, MuteStatusChangeEvent, NickChangeEvent, VanishStatusChangeEvent} +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.event.entity.PlayerDeathEvent +import org.bukkit.event.player.* +import org.bukkit.event.player.PlayerLoginEvent.Result +import org.bukkit.event.server.{BroadcastMessageEvent, TabCompleteEvent} +import org.bukkit.event.{EventHandler, EventPriority, Listener} +import reactor.core.scala.publisher.{SFlux, SMono} + +class MCListener(val module: MinecraftChatModule) extends Listener { + final private val muteRole = DPUtils.roleData(module.getConfig, "muteRole", "Muted") + + @EventHandler(priority = EventPriority.HIGHEST) def onPlayerLogin(e: PlayerLoginEvent): Unit = { + if (e.getResult ne Result.ALLOWED) return () + if (e.getPlayer.isInstanceOf[DiscordConnectedPlayer]) return () + val dcp = MCChatUtils.LoggedInPlayers.get(e.getPlayer.getUniqueId) + if (dcp.nonEmpty) MCChatUtils.callLogoutEvent(dcp.get, needsSync = false) + } + + @EventHandler(priority = EventPriority.MONITOR) def onPlayerJoin(e: PlayerJoinEvent): Unit = { + if (e.getPlayer.isInstanceOf[DiscordConnectedPlayer]) return () // Don't show the joined message for the fake player + Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, () => { + def foo(): Unit = { + val p = e.getPlayer + val dp = TBMCPlayerBase.getPlayer(p.getUniqueId, classOf[TBMCPlayer]).getAs(classOf[DiscordPlayer]) + if (dp != null) + DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID)).^^().flatMap(user => + user.getPrivateChannel.^^().flatMap(chan => module.chatChannelMono.flatMap(cc => { + MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID, DiscordPlayerSender.create(user, chan, p, module)) + MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID, DiscordPlayerSender.create(user, cc, p, module)) //Stored per-channel + SMono.empty + }))).subscribe() + val message = e.getJoinMessage + sendJoinLeaveMessage(message, e.getPlayer) + ChromaBot.updatePlayerList() + } + + foo() + }) + } + + private def sendJoinLeaveMessage(message: String, player: Player): Unit = + if (message != null && message.trim.nonEmpty) + MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), player, ChannelconBroadcast.JOINLEAVE, hookmsg = true).subscribe() + + @EventHandler(priority = EventPriority.MONITOR) def onPlayerLeave(e: PlayerQuitEvent): Unit = { + if (e.getPlayer.isInstanceOf[DiscordConnectedPlayer]) return () // Only care about real users + MCChatUtils.OnlineSenders.filterInPlace((_, userMap) => userMap.entrySet.stream.noneMatch(_.getValue.getUniqueId.equals(e.getPlayer.getUniqueId))) + Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, () => MCChatUtils.LoggedInPlayers.get(e.getPlayer.getUniqueId).foreach(MCChatUtils.callLoginEvents)) + Bukkit.getScheduler.runTaskLaterAsynchronously(DiscordPlugin.plugin, () => ChromaBot.updatePlayerList(), 5) + val message = e.getQuitMessage + sendJoinLeaveMessage(message, e.getPlayer) + } + + @EventHandler(priority = EventPriority.HIGHEST) def onPlayerKick(e: PlayerKickEvent): Unit = { + /*if (!DiscordPlugin.hooked && !e.getReason().equals("The server is restarting") + && !e.getReason().equals("Server closed")) // The leave messages errored with the previous setup, I could make it wait since I moved it here, but instead I have a special + MCChatListener.forAllowedCustomAndAllMCChat(e.getPlayer().getName() + " left the game"); // message for this - Oh wait this doesn't even send normally because of the hook*/ + } + + @EventHandler(priority = EventPriority.LOW) def onPlayerDeath(e: PlayerDeathEvent): Unit = + MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(e.getDeathMessage), e.getEntity, ChannelconBroadcast.DEATH, hookmsg = true).subscribe() + + @EventHandler def onPlayerAFK(e: AfkStatusChangeEvent): Unit = { + val base = e.getAffected.getBase + if (e.isCancelled || !base.isOnline) return () + val msg = base.getDisplayName + " is " + (if (e.getValue) "now" + else "no longer") + " AFK." + MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(msg), base, ChannelconBroadcast.AFK, hookmsg = false).subscribe() + } + + @EventHandler def onPlayerMute(e: MuteStatusChangeEvent): Unit = { + val role = muteRole.get + if (role == null) return () + val source = e.getAffected.getSource + if (!source.isPlayer) return () + val p = TBMCPlayerBase.getPlayer(source.getPlayer.getUniqueId, classOf[TBMCPlayer]).getAs(classOf[DiscordPlayer]) + if (p == null) return () + DPUtils.ignoreError(SMono(DiscordPlugin.dc.getUserById(Snowflake.of(p.getDiscordID))) + .flatMap(user => SMono(user.asMember(DiscordPlugin.mainServer.getId))) + .flatMap(user => role.flatMap((r: Role) => { + def foo(r: Role): SMono[_] = { + if (e.getValue) user.addRole(r.getId) + else user.removeRole(r.getId) + val modlog = module.modlogChannel.get + val msg = (if (e.getValue) "M" + else "Unm") + "uted user: " + user.getUsername + "#" + user.getDiscriminator + module.log(msg) + if (modlog != null) return modlog.flatMap((ch: MessageChannel) => SMono(ch.createMessage(msg))) + SMono.empty + } + + foo(r) + }))).subscribe() + } + + @EventHandler def onChatSystemMessage(event: TBMCSystemChatEvent): Unit = + MCChatUtils.forAllowedMCChat(MCChatUtils.send(event.getMessage), event).subscribe() + + @EventHandler def onBroadcastMessage(event: BroadcastMessageEvent): Unit = + MCChatUtils.forCustomAndAllMCChat(MCChatUtils.send(event.getMessage), ChannelconBroadcast.BROADCAST, hookmsg = false).subscribe() + + @EventHandler def onYEEHAW(event: TBMCYEEHAWEvent): Unit = { //TODO: Inherit from the chat event base to have channel support + val name = event.getSender match { + case player: Player => player.getDisplayName + case _ => event.getSender.getName + } + //Channel channel = ChromaGamerBase.getFromSender(event.getSender()).channel().get(); - TODO + DiscordPlugin.mainServer.getEmojis.^^().filter(e => "YEEHAW" == e.getName).take(1).singleOrEmpty + .map(Option.apply).defaultIfEmpty(Option.empty) + .flatMap(yeehaw => MCChatUtils.forPublicPrivateChat(MCChatUtils.send(name + + yeehaw.map(guildEmoji => " <:YEEHAW:" + guildEmoji.getId.asString + ">s").getOrElse(" YEEHAWs")))).subscribe() + } + + @EventHandler def onNickChange(event: NickChangeEvent): Unit = MCChatUtils.updatePlayerList() + + @EventHandler def onTabComplete(event: TabCompleteEvent): Unit = { + val i = event.getBuffer.lastIndexOf(' ') + val t = event.getBuffer.substring(i + 1) //0 if not found + if (!t.startsWith("@")) return () + val token = t.substring(1) + val x = DiscordPlugin.mainServer.getMembers.^^().flatMap(m => SFlux.just(m.getUsername, m.getNickname.orElse(""))) + .filter(_.startsWith(token)).map("@" + _).doOnNext(event.getCompletions.add(_)).blockLast() + } + + @EventHandler def onCommandSend(event: PlayerCommandSendEvent): Boolean = event.getCommands.add("g") + + @EventHandler def onVanish(event: VanishStatusChangeEvent): Unit = { + if (event.isCancelled) return () + Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => MCChatUtils.updatePlayerList()) + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/mcchat/MinecraftChatModule.scala b/src/main/scala/buttondevteam/discordplugin/mcchat/MinecraftChatModule.scala new file mode 100644 index 0000000..c9e2f67 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/mcchat/MinecraftChatModule.scala @@ -0,0 +1,232 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.core.component.channel.Channel +import buttondevteam.discordplugin.DPUtils.{MonoExtensions, SpecExtensions} +import buttondevteam.discordplugin.playerfaker.ServerWatcher +import buttondevteam.discordplugin.playerfaker.perm.LPInjector +import buttondevteam.discordplugin.util.DPState +import buttondevteam.discordplugin.{ChannelconBroadcast, DPUtils, DiscordConnectedPlayer, DiscordPlugin} +import buttondevteam.lib.architecture.{Component, ConfigData, ReadOnlyConfigData} +import buttondevteam.lib.{TBMCCoreAPI, TBMCSystemChatEvent} +import com.google.common.collect.Lists +import discord4j.common.util.Snowflake +import discord4j.core.`object`.entity.channel.MessageChannel +import discord4j.rest.util.Color +import org.bukkit.Bukkit +import reactor.core.scala.publisher.SMono + +import java.util +import java.util.stream.Collectors +import java.util.{Objects, UUID} +import scala.jdk.CollectionConverters.IterableHasAsScala + +/** + * Provides Minecraft chat connection to Discord. Commands may be used either in a public chat (limited) or in a DM. + */ +object MinecraftChatModule { + var state = DPState.RUNNING +} + +class MinecraftChatModule extends Component[DiscordPlugin] { + def getListener: MCChatListener = this.listener + + private var listener: MCChatListener = null + private[mcchat] var serverWatcher: ServerWatcher = null + private var lpInjector: LPInjector = null + private[mcchat] var disabling = false + + /** + * A list of commands that can be used in public chats - Warning: Some plugins will treat players as OPs, always test before allowing a command! + */ + val whitelistedCommands: ConfigData[util.ArrayList[String]] = getConfig.getData("whitelistedCommands", + () => Lists.newArrayList("list", "u", "shrug", "tableflip", "unflip", "mwiki", "yeehaw", "lenny", "rp", "plugins")) + + /** + * The channel to use as the public Minecraft chat - everything public gets broadcasted here + */ + val chatChannel: ReadOnlyConfigData[Snowflake] = DPUtils.snowflakeData(getConfig, "chatChannel", 0L) + + def chatChannelMono: SMono[MessageChannel] = DPUtils.getMessageChannel(chatChannel.getPath, chatChannel.get) + + /** + * The channel where the plugin can log when it mutes a player on Discord because of a Minecraft mute + */ + val modlogChannel: ReadOnlyConfigData[SMono[MessageChannel]] = DPUtils.channelData(getConfig, "modlogChannel") + /** + * The plugins to exclude from fake player events used for the 'mcchat' command - some plugins may crash, add them here + */ + val excludedPlugins: ConfigData[Array[String]] = getConfig.getData("excludedPlugins", Array[String]("ProtocolLib", "LibsDisguises", "JourneyMapServer")) + /** + * If this setting is on then players logged in through the 'mcchat' command will be able to teleport using plugin commands. + * They can then use commands like /tpahere to teleport others to that place.
+ * If this is off, then teleporting will have no effect. + */ + val allowFakePlayerTeleports: ConfigData[Boolean] = getConfig.getData("allowFakePlayerTeleports", false) + /** + * If this is on, each chat channel will have a player list in their description. + * It only gets added if there's no description yet or there are (at least) two lines of "----" following each other. + * Note that it will replace everything above the first and below the last "----" but it will only detect exactly four dashes. + * So if you want to use dashes for something else in the description, make sure it's either less or more dashes in one line. + */ + val showPlayerListOnDC: ConfigData[Boolean] = getConfig.getData("showPlayerListOnDC", true) + /** + * This setting controls whether custom chat connections can be created (existing connections will always work). + * Custom chat connections can be created using the channelcon command and they allow players to display town chat in a Discord channel for example. + * See the channelcon command for more details. + */ + val allowCustomChat: ConfigData[Boolean] = getConfig.getData("allowCustomChat", true) + /** + * This setting allows you to control if players can DM the bot to log on the server from Discord. + * This allows them to both chat and perform any command they can in-game. + */ + val allowPrivateChat: ConfigData[Boolean] = getConfig.getData("allowPrivateChat", true) + /** + * If set, message authors appearing on Discord will link to this URL. A 'type' and 'id' parameter will be added with the user's platform (Discord, Minecraft, ...) and ID. + */ + val profileURL: ConfigData[String] = getConfig.getData("profileURL", "") + /** + * Enables support for running vanilla commands through Discord, if you ever need it. + */ + val enableVanillaCommands: ConfigData[Boolean] = getConfig.getData("enableVanillaCommands", true) + /** + * Whether players logged on from Discord (mcchat command) should be recognised by other plugins. Some plugins might break if it's turned off. + * But it's really hacky. + */ + final private val addFakePlayersToBukkit = getConfig.getData("addFakePlayersToBukkit", false) + /** + * Set by the component to report crashes. + */ + final private val serverUp = getConfig.getData("serverUp", false) + final private val mcChatCommand = new MCChatCommand(this) + final private val channelconCommand = new ChannelconCommand(this) + + override protected def enable(): Unit = { + if (DPUtils.disableIfConfigErrorRes(this, chatChannel, chatChannelMono)) return () + listener = new MCChatListener(this) + TBMCCoreAPI.RegisterEventsForExceptions(listener, getPlugin) + TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(this), getPlugin) //These get undone if restarting/resetting - it will ignore events if disabled + getPlugin.manager.registerCommand(mcChatCommand) + getPlugin.manager.registerCommand(channelconCommand) + val chcons = getConfig.getConfig.getConfigurationSection("chcons") + if (chcons == null) { //Fallback to old place + getConfig.getConfig.getRoot.getConfigurationSection("chcons") + } + if (chcons != null) { + val chconkeys = chcons.getKeys(false) + for (chconkey <- chconkeys.asScala) { + val chcon = chcons.getConfigurationSection(chconkey) + val mcch = Channel.getChannels.filter((ch: Channel) => ch.ID == chcon.getString("mcchid")).findAny + val ch = DiscordPlugin.dc.getChannelById(Snowflake.of(chcon.getLong("chid"))).block + val did = chcon.getLong("did") + val user = DiscordPlugin.dc.getUserById(Snowflake.of(did)).block + val groupid = chcon.getString("groupid") + val toggles = chcon.getInt("toggles") + val brtoggles = chcon.getStringList("brtoggles") + if (mcch.isPresent && ch != null && user != null && groupid != null) { + Bukkit.getScheduler.runTask(getPlugin, () => { //<-- Needed because of occasional ConcurrentModificationExceptions when creating the player (PermissibleBase) + val dcp = DiscordConnectedPlayer.create(user, ch.asInstanceOf[MessageChannel], + UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname"), this) + MCChatCustom.addCustomChat(ch.asInstanceOf[MessageChannel], groupid, mcch.get, user, dcp, toggles, + brtoggles.asScala.map(TBMCSystemChatEvent.BroadcastTarget.get).filter(Objects.nonNull).toSet) + () + }) + } + } + } + try if (lpInjector == null) lpInjector = new LPInjector //new LPInjector(DiscordPlugin.plugin) + catch { + case e: Exception => + TBMCCoreAPI.SendException("Failed to init LuckPerms injector", e, this) + case e: NoClassDefFoundError => + log("No LuckPerms, not injecting") + //e.printStackTrace(); + } + if (addFakePlayersToBukkit.get) try { + serverWatcher = new ServerWatcher + serverWatcher.enableDisable(true) + log("Finished hooking into the server") + } catch { + case e: Exception => + TBMCCoreAPI.SendException("Failed to hack the server (object)! Disable addFakePlayersToBukkit in the config.", e, this) + } + if (MinecraftChatModule.state eq DPState.RESTARTING_PLUGIN) { //These will only execute if the chat is enabled + sendStateMessage(Color.CYAN, "Discord plugin restarted - chat connected.") //Really important to note the chat, hmm + MinecraftChatModule.state = DPState.RUNNING + } + else if (MinecraftChatModule.state eq DPState.DISABLED_MCCHAT) { + sendStateMessage(Color.CYAN, "Minecraft chat enabled - chat connected.") + MinecraftChatModule.state = DPState.RUNNING + } + else if (serverUp.get) { + sendStateMessage(Color.YELLOW, "Server started after a crash - chat connected.") + val thr = new Throwable("The server shut down unexpectedly. See the log of the previous run for more details.") + thr.setStackTrace(new Array[StackTraceElement](0)) + TBMCCoreAPI.SendException("The server crashed!", thr, this) + } + else sendStateMessage(Color.GREEN, "Server started - chat connected.") + serverUp.set(true) + } + + override protected def disable(): Unit = { + disabling = true + if (MinecraftChatModule.state eq DPState.RESTARTING_PLUGIN) sendStateMessage(Color.ORANGE, "Discord plugin restarting") + else if (MinecraftChatModule.state eq DPState.RUNNING) { + sendStateMessage(Color.ORANGE, "Minecraft chat disabled") + MinecraftChatModule.state = DPState.DISABLED_MCCHAT + } + else { + val kickmsg = if (Bukkit.getOnlinePlayers.size > 0) + DPUtils.sanitizeString(Bukkit.getOnlinePlayers.stream.map(_.getDisplayName).collect(Collectors.joining(", "))) + + (if (Bukkit.getOnlinePlayers.size == 1) " was " else " were ") + "thrown out" //TODO: Make configurable + else "" + if (MinecraftChatModule.state eq DPState.RESTARTING_SERVER) sendStateMessage(Color.ORANGE, "Server restarting", kickmsg) + else if (MinecraftChatModule.state eq DPState.STOPPING_SERVER) sendStateMessage(Color.RED, "Server stopping", kickmsg) + else sendStateMessage(Color.GRAY, "Unknown state, please report.") + //If 'restart' is disabled then this isn't shown even if joinleave is enabled} + } + serverUp.set(false) //Disable even if just the component is disabled because that way it won't falsely report crashes + try //If it's not enabled it won't do anything + if (serverWatcher != null) { + serverWatcher.enableDisable(false) + log("Finished unhooking the server") + } + catch { + case e: Exception => + TBMCCoreAPI.SendException("Failed to restore the server object!", e, this) + } + val chcons = MCChatCustom.getCustomChats + val chconsc = getConfig.getConfig.createSection("chcons") + for (chcon <- chcons) { + val chconc = chconsc.createSection(chcon.channel.getId.asString) + chconc.set("mcchid", chcon.mcchannel.ID) + chconc.set("chid", chcon.channel.getId.asLong) + chconc.set("did", chcon.user.getId.asLong) + chconc.set("mcuid", chcon.dcp.getUniqueId.toString) + chconc.set("mcname", chcon.dcp.getName) + chconc.set("groupid", chcon.groupID) + chconc.set("toggles", chcon.toggles) + chconc.set("brtoggles", chcon.brtoggles.map(_.getName).toList) + } + if (listener != null) { //Can be null if disabled because of a config error + listener.stop(true) + } + getPlugin.manager.unregisterCommand(mcChatCommand) + getPlugin.manager.unregisterCommand(channelconCommand) + disabling = false + } + + /** + * It will block to make sure all messages are sent + */ + private def sendStateMessage(color: Color, message: String) = + MCChatUtils.forCustomAndAllMCChat(_.flatMap( + _.createEmbed(_.setColor(color).setTitle(message).^^()).^^() + .onErrorResume(_ => SMono.empty) + ), ChannelconBroadcast.RESTART, hookmsg = false).block() + + private def sendStateMessage(color: Color, message: String, extra: String) = + MCChatUtils.forCustomAndAllMCChat(_.flatMap( + _.createEmbed(_.setColor(color).setTitle(message).setDescription(extra).^^()).^^() + .onErrorResume(_ => SMono.empty) + ), ChannelconBroadcast.RESTART, hookmsg = false).block() +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/mccommands/DiscordMCCommand.scala b/src/main/scala/buttondevteam/discordplugin/mccommands/DiscordMCCommand.scala new file mode 100644 index 0000000..db4c38e --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/mccommands/DiscordMCCommand.scala @@ -0,0 +1,128 @@ +package buttondevteam.discordplugin.mccommands + +import buttondevteam.discordplugin.commands.{ConnectCommand, VersionCommand} +import buttondevteam.discordplugin.mcchat.{MCChatUtils, MinecraftChatModule} +import buttondevteam.discordplugin.util.DPState +import buttondevteam.discordplugin.{DPUtils, DiscordPlayer, DiscordPlugin, DiscordSenderBase} +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( + "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( + "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( + "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( + "Version command", + "Prints the plugin version")) def version(sender: CommandSender): Unit = { + sender.sendMessage(VersionCommand.getVersion) + } + + @Command2.Subcommand(helpText = 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/scala/buttondevteam/discordplugin/playerfaker/DelegatingMockMaker.scala b/src/main/scala/buttondevteam/discordplugin/playerfaker/DelegatingMockMaker.scala new file mode 100644 index 0000000..b9f7ca7 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/playerfaker/DelegatingMockMaker.scala @@ -0,0 +1,51 @@ +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: java.util.function.Function[MockedConstruction.Context, + MockCreationSettings[T]], handlerFactory: java.util.function.Function[MockedConstruction.Context, + MockHandler[T]], mockInitializer: MockedConstruction.MockInitializer[T]): MockMaker.ConstructionMockControl[T] = + this.mockMaker.createConstructionMock[T](`type`, settingsFactory, 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/DiscordInventory.java b/src/main/scala/buttondevteam/discordplugin/playerfaker/DiscordInventory.java similarity index 96% rename from src/main/java/buttondevteam/discordplugin/playerfaker/DiscordInventory.java rename to src/main/scala/buttondevteam/discordplugin/playerfaker/DiscordInventory.java index 1c0ebb3..b730cc8 100644 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordInventory.java +++ b/src/main/scala/buttondevteam/discordplugin/playerfaker/DiscordInventory.java @@ -1,7 +1,5 @@ package buttondevteam.discordplugin.playerfaker; -import lombok.Getter; -import lombok.Setter; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.entity.HumanEntity; @@ -16,8 +14,7 @@ import java.util.stream.IntStream; public class DiscordInventory implements Inventory { private ItemStack[] items = new ItemStack[27]; private List itemStacks = Arrays.asList(items); - @Getter - @Setter + public int maxStackSize; private static ItemStack emptyStack = new ItemStack(Material.AIR, 0); @@ -26,6 +23,16 @@ public class DiscordInventory implements Inventory { return items.length; } + @Override + public int getMaxStackSize() { + return maxStackSize; + } + + @Override + public void setMaxStackSize(int maxStackSize) { + this.maxStackSize = maxStackSize; + } + @Override public String getName() { return "Discord inventory"; diff --git a/src/main/scala/buttondevteam/discordplugin/playerfaker/ServerWatcher.scala b/src/main/scala/buttondevteam/discordplugin/playerfaker/ServerWatcher.scala new file mode 100644 index 0000000..8ab2f74 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/playerfaker/ServerWatcher.scala @@ -0,0 +1,94 @@ +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.entity.Player +import org.bukkit.{Bukkit, Server} +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 = Option.empty[DiscordConnectedPlayer] + method.getName match { + case "getPlayer" => + if (pc == 1 && (method.getParameterTypes()(0) == classOf[UUID])) + player = MCChatUtils.LoggedInPlayers.get(invocation.getArgument[UUID](0)) + case "getPlayerExact" => + if (pc == 1) { + val argument = invocation.getArgument(0) + player = MCChatUtils.LoggedInPlayers.values.find(_.getName.equalsIgnoreCase(argument)) + } + + /*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 Option.empty + if (player.isEmpty && name != null) + player = MCChatUtils.LoggedInPlayers.values.find(_.getName.equalsIgnoreCase(name)) + if (player.nonEmpty) + return new CraftPlayerProfile(player.get.getUniqueId, player.get.getName) + } + } + if (player.nonEmpty) return player.get + 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/scala/buttondevteam/discordplugin/playerfaker/VCMDWrapper.scala b/src/main/scala/buttondevteam/discordplugin/playerfaker/VCMDWrapper.scala new file mode 100644 index 0000000..15a3653 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/playerfaker/VCMDWrapper.scala @@ -0,0 +1,54 @@ +package buttondevteam.discordplugin.playerfaker + +import buttondevteam.discordplugin.mcchat.MinecraftChatModule +import buttondevteam.discordplugin.{DiscordSenderBase, IMCPlayer} +import buttondevteam.lib.TBMCCoreAPI +import org.bukkit.Bukkit +import org.bukkit.entity.Player + +object VCMDWrapper { + /** + * This constructor will only send raw vanilla messages to the sender in plain text. + * + * @param player The Discord sender player (the wrapper) + */ + def createListener[T <: DiscordSenderBase with IMCPlayer[T]](player: T, module: MinecraftChatModule): AnyRef = + createListener(player, null, module) + + /** + * This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player. + * + * @param player The Discord sender player (the wrapper) + * @param bukkitplayer The Bukkit player to send the raw message to + * @param module The Minecraft chat module + */ + def createListener[T <: DiscordSenderBase with IMCPlayer[T]](player: T, bukkitplayer: Player, module: MinecraftChatModule): AnyRef = try { + var ret: AnyRef = null + val mcpackage = Bukkit.getServer.getClass.getPackage.getName + if (mcpackage.contains("1_12")) ret = new VanillaCommandListener[T](player, bukkitplayer) + else if (mcpackage.contains("1_14")) ret = new VanillaCommandListener14[T](player, bukkitplayer) + else if (mcpackage.contains("1_15") || mcpackage.contains("1_16")) ret = VanillaCommandListener15.create(player, bukkitplayer) //bukkitplayer may be null but that's fine + else ret = null + if (ret == null) compatWarning(module) + ret + } catch { + case e@(_: NoClassDefFoundError | _: Exception) => + compatWarning(module) + TBMCCoreAPI.SendException("Failed to create vanilla command listener", e, module) + null + } + + private def compatWarning(module: MinecraftChatModule): Unit = + module.logWarn("Vanilla commands won't be available from Discord due to a compatibility error. Disable vanilla command support to remove this message.") + + private[playerfaker] def compatResponse(dsender: DiscordSenderBase) = { + dsender.sendMessage("Vanilla commands are not supported on this Minecraft version.") + true + } +} + +class VCMDWrapper(private val listener: AnyRef) { + @javax.annotation.Nullable def getListener: AnyRef = listener + + //Needed to mock the player @Nullable +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/playerfaker/VanillaCommandListener.scala b/src/main/scala/buttondevteam/discordplugin/playerfaker/VanillaCommandListener.scala new file mode 100644 index 0000000..3622871 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/playerfaker/VanillaCommandListener.scala @@ -0,0 +1,84 @@ +package buttondevteam.discordplugin.playerfaker + +import buttondevteam.discordplugin.{DiscordSenderBase, IMCPlayer} +import net.minecraft.server.v1_12_R1.* +import org.bukkit.Bukkit +import org.bukkit.craftbukkit.v1_12_R1.command.VanillaCommandWrapper +import org.bukkit.craftbukkit.v1_12_R1.entity.CraftPlayer +import org.bukkit.craftbukkit.v1_12_R1.{CraftServer, CraftWorld} +import org.bukkit.entity.Player + +import java.util + +object VanillaCommandListener { + def runBukkitOrVanillaCommand(dsender: DiscordSenderBase, cmdstr: String): Boolean = { + val cmd = Bukkit.getServer.asInstanceOf[CraftServer].getCommandMap.getCommand(cmdstr.split(" ")(0).toLowerCase) + if (!dsender.isInstanceOf[Player] || !cmd.isInstanceOf[VanillaCommandWrapper]) + return Bukkit.dispatchCommand(dsender, cmdstr) // Unconnected users are treated well in vanilla cmds + if (!dsender.isInstanceOf[IMCPlayer[_]]) + throw new ClassCastException("dsender needs to implement IMCPlayer to use vanilla commands as it implements Player.") + val sender = dsender.asInstanceOf[IMCPlayer[_]] + val vcmd = cmd.asInstanceOf[VanillaCommandWrapper] + if (!vcmd.testPermission(sender)) return true + val icommandlistener = sender.getVanillaCmdListener.getListener.asInstanceOf[ICommandListener] + if (icommandlistener == null) return VCMDWrapper.compatResponse(dsender) + var args = cmdstr.split(" ") + args = util.Arrays.copyOfRange(args, 1, args.length) + try vcmd.dispatchVanillaCommand(sender, icommandlistener, args) + catch { + case commandexception: CommandException => + // Taken from CommandHandler + val chatmessage = new ChatMessage(commandexception.getMessage, commandexception.getArgs) + chatmessage.getChatModifier.setColor(EnumChatFormat.RED) + icommandlistener.sendMessage(chatmessage) + } + true + } +} + +class VanillaCommandListener[T <: DiscordSenderBase with IMCPlayer[T]] extends ICommandListener { + def getPlayer: T = this.player + + private var player: T = null.asInstanceOf + private var bukkitplayer: Player = null + + /** + * This constructor will only send raw vanilla messages to the sender in plain text. + * + * @param player The Discord sender player (the wrapper) + */ + def this(player: T) = { + this() + this.player = player + this.bukkitplayer = null + } + + /** + * This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player. + * + * @param player The Discord sender player (the wrapper) + * @param bukkitplayer The Bukkit player to send the raw message to + */ + def this(player: T, bukkitplayer: Player) = { + this() + this.player = player + this.bukkitplayer = bukkitplayer + if (bukkitplayer != null && !bukkitplayer.isInstanceOf[CraftPlayer]) + throw new ClassCastException("bukkitplayer must be a Bukkit player!") + } + + override def C_(): MinecraftServer = Bukkit.getServer.asInstanceOf[CraftServer].getServer + + override def a(oplevel: Int, cmd: String): Boolean = { //return oplevel <= 2; // Value from CommandBlockListenerAbstract, found what it is in EntityPlayer - Wait, that'd always allow OP commands + oplevel == 0 || player.isOp + } + + override def getName: String = player.getName + + override def getWorld: World = player.getWorld.asInstanceOf[CraftWorld].getHandle + + override def sendMessage(arg0: IChatBaseComponent): Unit = { + player.sendMessage(arg0.toPlainText) + if (bukkitplayer != null) bukkitplayer.asInstanceOf[CraftPlayer].getHandle.sendMessage(arg0) + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/playerfaker/VanillaCommandListener14.scala b/src/main/scala/buttondevteam/discordplugin/playerfaker/VanillaCommandListener14.scala new file mode 100644 index 0000000..e7062c8 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/playerfaker/VanillaCommandListener14.scala @@ -0,0 +1,84 @@ +package buttondevteam.discordplugin.playerfaker + +import buttondevteam.discordplugin.{DiscordSenderBase, IMCPlayer} +import net.minecraft.server.v1_14_R1.* +import org.bukkit.Bukkit +import org.bukkit.command.CommandSender +import org.bukkit.craftbukkit.v1_14_R1.command.{ProxiedNativeCommandSender, VanillaCommandWrapper} +import org.bukkit.craftbukkit.v1_14_R1.entity.CraftPlayer +import org.bukkit.craftbukkit.v1_14_R1.{CraftServer, CraftWorld} +import org.bukkit.entity.Player + +import java.util + +object VanillaCommandListener14 { + def runBukkitOrVanillaCommand(dsender: DiscordSenderBase, cmdstr: String): Boolean = { + val cmd = Bukkit.getServer.asInstanceOf[CraftServer].getCommandMap.getCommand(cmdstr.split(" ")(0).toLowerCase) + if (!dsender.isInstanceOf[Player] || !cmd.isInstanceOf[VanillaCommandWrapper]) + return Bukkit.dispatchCommand(dsender, cmdstr) // Unconnected users are treated well in vanilla cmds + if (!dsender.isInstanceOf[IMCPlayer[_]]) + throw new ClassCastException("dsender needs to implement IMCPlayer to use vanilla commands as it implements Player.") + val sender = dsender.asInstanceOf[IMCPlayer[_]] // Don't use val on recursive interfaces :P + val vcmd = cmd.asInstanceOf[VanillaCommandWrapper] + if (!vcmd.testPermission(sender)) return true + val world = Bukkit.getWorlds.get(0).asInstanceOf[CraftWorld].getHandle + val icommandlistener = sender.getVanillaCmdListener.getListener.asInstanceOf[ICommandListener] + if (icommandlistener == null) return VCMDWrapper.compatResponse(dsender) + val wrapper = new CommandListenerWrapper(icommandlistener, new Vec3D(0, 0, 0), new Vec2F(0, 0), world, 0, sender.getName, new ChatComponentText(sender.getName), world.getMinecraftServer, null) + val pncs = new ProxiedNativeCommandSender(wrapper, sender, sender) + var args = cmdstr.split(" ") + args = util.Arrays.copyOfRange(args, 1, args.length) + try return vcmd.execute(pncs, cmd.getLabel, args) + catch { + case commandexception: CommandException => + // Taken from CommandHandler + val chatmessage = new ChatMessage(commandexception.getMessage, commandexception.a) + chatmessage.getChatModifier.setColor(EnumChatFormat.RED) + icommandlistener.sendMessage(chatmessage) + } + true + } +} + +class VanillaCommandListener14[T <: DiscordSenderBase with IMCPlayer[T]] extends ICommandListener { + def getPlayer: T = this.player + + private var player: T = null.asInstanceOf + private var bukkitplayer: Player = null + + /** + * This constructor will only send raw vanilla messages to the sender in plain text. + * + * @param player The Discord sender player (the wrapper) + */ + def this(player: T) = { + this() + this.player = player + this.bukkitplayer = null + } + + /** + * This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player. + * + * @param player The Discord sender player (the wrapper) + * @param bukkitplayer The Bukkit player to send the raw message to + */ + def this(player: T, bukkitplayer: Player) = { + this() + this.player = player + this.bukkitplayer = bukkitplayer + if (bukkitplayer != null && !bukkitplayer.isInstanceOf[CraftPlayer]) throw new ClassCastException("bukkitplayer must be a Bukkit player!") + } + + override def sendMessage(arg0: IChatBaseComponent): scala.Unit = { + player.sendMessage(arg0.getString) + if (bukkitplayer != null) bukkitplayer.asInstanceOf[CraftPlayer].getHandle.sendMessage(arg0) + } + + override def shouldSendSuccess = true + + override def shouldSendFailure = true + + override def shouldBroadcastCommands = true //Broadcast to in-game admins + override def getBukkitSender(commandListenerWrapper: CommandListenerWrapper): CommandSender = player +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/playerfaker/VanillaCommandListener15.scala b/src/main/scala/buttondevteam/discordplugin/playerfaker/VanillaCommandListener15.scala new file mode 100644 index 0000000..7bc187e --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/playerfaker/VanillaCommandListener15.scala @@ -0,0 +1,123 @@ +package buttondevteam.discordplugin.playerfaker + +import buttondevteam.discordplugin.{DiscordSenderBase, IMCPlayer} +import org.bukkit.Bukkit +import org.bukkit.command.{CommandSender, SimpleCommandMap} +import org.bukkit.entity.Player +import org.mockito.{Answers, Mockito} + +import java.lang.reflect.Modifier +import java.util + +/** + * Same as {@link VanillaCommandListener14} but with reflection + */ +object VanillaCommandListener15 { + private var vcwcl: Class[_] = null + private var nms: String = null + + /** + * This method will only send raw vanilla messages to the sender in plain text. + * + * @param player The Discord sender player (the wrapper) + */ + @throws[Exception] + def create[T <: DiscordSenderBase with IMCPlayer[T]](player: T): VanillaCommandListener15[T] = create(player, null) + + /** + * This method will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player. + * + * @param player The Discord sender player (the wrapper) + * @param bukkitplayer The Bukkit player to send the raw message to + */ + @SuppressWarnings(Array("unchecked")) + @throws[Exception] + def create[T <: DiscordSenderBase with IMCPlayer[T]](player: T, bukkitplayer: Player): VanillaCommandListener15[T] = { + if (vcwcl == null) { + val pkg = Bukkit.getServer.getClass.getPackage.getName + vcwcl = Class.forName(pkg + ".command.VanillaCommandWrapper") + } + if (nms == null) { + val server = Bukkit.getServer + nms = server.getClass.getMethod("getServer").invoke(server).getClass.getPackage.getName //org.mockito.codegen + } + val iclcl = Class.forName(nms + ".ICommandListener") + Mockito.mock(classOf[VanillaCommandListener15[T]], + Mockito.withSettings.stubOnly.useConstructor(player, bukkitplayer) + .extraInterfaces(iclcl).defaultAnswer(invocation => { + if (invocation.getMethod.getName == "sendMessage") { + val icbc = invocation.getArgument(0) + player.sendMessage(icbc.getClass.getMethod("getString").invoke(icbc).asInstanceOf[String]) + if (bukkitplayer != null) { + val handle = bukkitplayer.getClass.getMethod("getHandle").invoke(bukkitplayer) + handle.getClass.getMethod("sendMessage", icbc.getClass).invoke(handle, icbc) + } + null + } + else if (!Modifier.isAbstract(invocation.getMethod.getModifiers)) invocation.callRealMethod + else if (invocation.getMethod.getReturnType eq classOf[Boolean]) true //shouldSend... shouldBroadcast... + else if (invocation.getMethod.getReturnType eq classOf[CommandSender]) player + else Answers.RETURNS_DEFAULTS.answer(invocation) + })) + } + + @throws[Exception] + def runBukkitOrVanillaCommand(dsender: DiscordSenderBase, cmdstr: String): Boolean = { + val server = Bukkit.getServer + val cmap = server.getClass.getMethod("getCommandMap").invoke(server).asInstanceOf[SimpleCommandMap] + val cmd = cmap.getCommand(cmdstr.split(" ")(0).toLowerCase) + if (!dsender.isInstanceOf[Player] || cmd == null || !vcwcl.isAssignableFrom(cmd.getClass)) + return Bukkit.dispatchCommand(dsender, cmdstr) // Unconnected users are treated well in vanilla cmds + if (!dsender.isInstanceOf[IMCPlayer[_]]) + throw new ClassCastException("dsender needs to implement IMCPlayer to use vanilla commands as it implements Player.") + val sender = dsender.asInstanceOf[IMCPlayer[_]] // Don't use val on recursive interfaces :P + if (!vcwcl.getMethod("testPermission", classOf[CommandSender]).invoke(cmd, sender).asInstanceOf[Boolean]) + return true + val cworld = Bukkit.getWorlds.get(0) + val world = cworld.getClass.getMethod("getHandle").invoke(cworld) + val icommandlistener = sender.getVanillaCmdListener.getListener + if (icommandlistener == null) return VCMDWrapper.compatResponse(dsender) + val clwcl = Class.forName(nms + ".CommandListenerWrapper") + val v3dcl = Class.forName(nms + ".Vec3D") + val v2fcl = Class.forName(nms + ".Vec2F") + val icbcl = Class.forName(nms + ".IChatBaseComponent") + val mcscl = Class.forName(nms + ".MinecraftServer") + val ecl = Class.forName(nms + ".Entity") + val cctcl = Class.forName(nms + ".ChatComponentText") + val iclcl = Class.forName(nms + ".ICommandListener") + val wrapper = clwcl.getConstructor(iclcl, v3dcl, v2fcl, world.getClass, classOf[Int], classOf[String], icbcl, mcscl, ecl) + .newInstance(icommandlistener, v3dcl.getConstructor(classOf[Double], classOf[Double], classOf[Double]) + .newInstance(0, 0, 0), v2fcl.getConstructor(classOf[Float], classOf[Float]) + .newInstance(0, 0), world, 0, sender.getName, cctcl.getConstructor(classOf[String]) + .newInstance(sender.getName), world.getClass.getMethod("getMinecraftServer").invoke(world), null) + /*val wrapper = new CommandListenerWrapper(icommandlistener, new Vec3D(0, 0, 0), + new Vec2F(0, 0), world, 0, sender.getName(), + new ChatComponentText(sender.getName()), world.getMinecraftServer(), null);*/ + val pncscl = Class.forName(vcwcl.getPackage.getName + ".ProxiedNativeCommandSender") + val pncs = pncscl.getConstructor(clwcl, classOf[CommandSender], classOf[CommandSender]) + .newInstance(wrapper, sender, sender) + var args = cmdstr.split(" ") + args = util.Arrays.copyOfRange(args, 1, args.length) + try return cmd.execute(pncs.asInstanceOf[CommandSender], cmd.getLabel, args) + catch { + case commandexception: Exception => + if (!(commandexception.getClass.getSimpleName == "CommandException")) throw commandexception + // Taken from CommandHandler + val cmcl = Class.forName(nms + ".ChatMessage") + val chatmessage = cmcl.getConstructor(classOf[String], classOf[Array[AnyRef]]) + .newInstance(commandexception.getMessage, Array[AnyRef](commandexception.getClass.getMethod("a").invoke(commandexception))) + val modifier = cmcl.getMethod("getChatModifier").invoke(chatmessage) + val ecfcl = Class.forName(nms + ".EnumChatFormat") + modifier.getClass.getMethod("setColor", ecfcl).invoke(modifier, ecfcl.getField("RED").get(null)) + icommandlistener.getClass.getMethod("sendMessage", icbcl).invoke(icommandlistener, chatmessage) + } + true + } +} + +class VanillaCommandListener15[T <: DiscordSenderBase with IMCPlayer[T]] protected(var player: T, val bukkitplayer: Player) { + if (bukkitplayer != null && !bukkitplayer.getClass.getSimpleName.endsWith("CraftPlayer")) + throw new ClassCastException("bukkitplayer must be a Bukkit player!") + + def getPlayer: T = this.player +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java b/src/main/scala/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java similarity index 84% rename from src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java rename to src/main/scala/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java index 055c5e8..31ff53e 100644 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java +++ b/src/main/scala/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java @@ -1,40 +1,9 @@ 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; -import me.lucko.luckperms.bukkit.inject.permissible.DummyPermissibleBase; -import me.lucko.luckperms.bukkit.inject.permissible.LuckPermsPermissible; -import me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener; -import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.locale.Message; -import me.lucko.luckperms.common.locale.TranslationManager; -import me.lucko.luckperms.common.model.User; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import org.bukkit.Bukkit; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerLoginEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.permissions.PermissibleBase; -import org.bukkit.permissions.PermissionAttachment; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; public final class LPInjector implements Listener { //Disable login event for LuckPerms - private final LPBukkitPlugin plugin; + /*private final LPBukkitPlugin plugin; private final BukkitConnectionListener connectionListener; private final Set deniedLogin; private final Field detectedCraftBukkitOfflineMode; @@ -86,11 +55,11 @@ public final class LPInjector implements Listener { //Disable login event for Lu //Code copied from LuckPerms - me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerLogin(PlayerLoginEvent e) { + public void onPlayerLogin(PlayerLoginEvent e) {*/ /* Called when the player starts logging into the server. At this point, the users data should be present and loaded. */ - if (!(e.getPlayer() instanceof DiscordConnectedPlayer)) + /*if (!(e.getPlayer() instanceof DiscordConnectedPlayer)) return; //Normal players must be handled by the plugin final DiscordConnectedPlayer player = (DiscordConnectedPlayer) e.getPlayer(); @@ -102,7 +71,7 @@ public final class LPInjector implements Listener { //Disable login event for Lu final User user = plugin.getUserManager().getIfLoaded(player.getUniqueId()); /* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */ - if (user == null) { + /*if (user == null) { deniedLogin.add(player.getUniqueId()); if (!plugin.getConnectionListener().getUniqueConnections().contains(player.getUniqueId())) { @@ -234,5 +203,5 @@ public final class LPInjector implements Listener { //Disable login event for Lu player.setPerm(DummyPermissibleBase.INSTANCE); } - } + }*/ } diff --git a/src/main/scala/buttondevteam/discordplugin/role/GameRoleModule.scala b/src/main/scala/buttondevteam/discordplugin/role/GameRoleModule.scala new file mode 100644 index 0000000..ad8faa1 --- /dev/null +++ b/src/main/scala/buttondevteam/discordplugin/role/GameRoleModule.scala @@ -0,0 +1,114 @@ +package buttondevteam.discordplugin.role + +import buttondevteam.core.ComponentManager +import buttondevteam.discordplugin.DPUtils.{FluxExtensions, MonoExtensions} +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.scala.publisher.SMono + +import java.util.Collections +import scala.jdk.CollectionConverters.SeqHasAsJava + +/** + * 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 = (_: Role).getGuildId.asLong != DiscordPlugin.mainServer.getId.asLong + roleEvent match { + case roleCreateEvent: RoleCreateEvent => Bukkit.getScheduler.runTaskLaterAsynchronously(DiscordPlugin.plugin, () => { + val role = roleCreateEvent.getRole + if (!notMainServer(role)) { + grm.isGameRole(role).flatMap(b => { + if (!b) SMono.empty //Deleted or not a game role + else { + GameRoles.add(role.getName) + if (logChannel != null) + logChannel.flatMap(_.createMessage("Added " + role.getName + " as game role." + + " If you don't want this, change the role's color from the game role color.").^^()) + else + SMono.empty + } + }).subscribe() + () + } + }, 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(_.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(isGameRole => { + if (!isGameRole) + if (GameRoles.remove(or.getName) && logChannel != null) + logChannel.flatMap(_.createMessage("Removed " + or.getName + " as a game role because its color changed.").^^()) + else + SMono.empty + else if (GameRoles.contains(or.getName) && or.getName == cr.getName) + SMono.empty + else { + 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) + logChannel.flatMap((ch: MessageChannel) => ch.createMessage("Changed game role from " + or.getName + " to " + cr.getName + ".").^^()) + else + logChannel.flatMap((ch: MessageChannel) => ch.createMessage("Added " + cr.getName + " as game role because it has the color of one.").^^()) + else + SMono.empty + } + }).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).collectSeq().block().asJava + } + + 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): SMono[Boolean] = { + if (r.getGuildId.asLong != DiscordPlugin.mainServer.getId.asLong) return SMono.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(_.hasHigherRoles(Collections.singleton(r.getId)).^^().asInstanceOf).defaultIfEmpty(false) //Below one of our roles + else SMono.just(false) + } +} \ No newline at end of file diff --git a/src/main/scala/buttondevteam/discordplugin/role/RoleCommand.scala b/src/main/scala/buttondevteam/discordplugin/role/RoleCommand.scala new file mode 100644 index 0000000..71711e9 --- /dev/null +++ b/src/main/scala/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( + "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( + "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/scala/buttondevteam/discordplugin/util/DPState.scala b/src/main/scala/buttondevteam/discordplugin/util/DPState.scala new file mode 100644 index 0000000..34bde89 --- /dev/null +++ b/src/main/scala/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/scala/buttondevteam/discordplugin/util/Timings.scala b/src/main/scala/buttondevteam/discordplugin/util/Timings.scala new file mode 100644 index 0000000..58702a0 --- /dev/null +++ b/src/main/scala/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/test/java/buttondevteam/DiscordPlugin/AppTest.java b/src/test/java/buttondevteam/DiscordPlugin/AppTest.java deleted file mode 100755 index 4b8bbb9..0000000 --- a/src/test/java/buttondevteam/DiscordPlugin/AppTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package buttondevteam.DiscordPlugin; - -import buttondevteam.discordplugin.DiscordConnectedPlayer; -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; -import org.bukkit.attribute.Attribute; -import org.bukkit.entity.Player; - -/** - * Unit test for simple App. - */ -public class AppTest extends TestCase { - /** - * Create the test case - * - * @param testName - * name of the test case - */ - public AppTest(String testName) { - super(testName); - } - - /** - * @return the suite of tests being tested - */ - public static Test suite() { - return new TestSuite(AppTest.class); - } - - /** - * Rigourous Test :-) - */ - public void testApp() { - Player dcp = DiscordConnectedPlayer.createTest(); - - double h = dcp.getAttribute(Attribute.GENERIC_MAX_HEALTH).getDefaultValue(); ; ; - System.out.println(h); - } -}