diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 4b9e6ec..0000000 --- a/.editorconfig +++ /dev/null @@ -1,19 +0,0 @@ -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = false -indent_style = space -indent_size = 4 - -[*.json] -indent_style = space -indent_size = 2 - -[*.java] -indent_style = tab -tab_width = 4 - -[{*.yml, *.yaml}] -indent_style = space -indent_size = 2 - diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 0ed954a..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,9 +0,0 @@ -**Description:** - -**Steps to Reproduce:** - -1. - -**Result:** - -**Expected Result:** diff --git a/.travis.yml b/.travis.yml index 7a60b95..808d9b0 100755 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ before_install: | # Wget BuildTools and run if cached folder not found fi language: java jdk: - - oraclejdk11 + - oraclejdk8 sudo: true deploy: # deploy develop to the staging environment diff --git a/README.md b/README.md index 59aa0de..701765f 100755 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # DiscordPlugin -A plugin that controls the ChromaBot Discord bot and provides Minecraft chat functionality and other features. +A plugin that controls the Chroma Bot Discord bot. ## Features ### Announce new posts from /r/ChromaGamers If it's a (distinguished) moderator post, it'll be posted to the \#announcements channel, otherwise it'll be posted and pinned to \#general. ### Announce server restarts -It announces server starts/stops and restarts, as well as if the server shut down unexpectedly. +Currently it only says when the server starts up but that's about to change soon... -**For more, see:** http://chromapedia.wikia.com/wiki/ChromaBot +## Planned and work-in-progress features +See [here](https://github.com/TBMCPlugins/DiscordPlugin/projects/1). diff --git a/deploy.sh b/deploy.sh index 82c8e92..a40b486 100755 --- a/deploy.sh +++ b/deploy.sh @@ -6,5 +6,5 @@ if [ $1 = 'production' ]; then echo Production mode echo $UPLOAD_KEY > upload_key chmod 400 upload_key -yes | scp -B -i upload_key -o StrictHostKeyChecking=no $FILENAME travis@server.figytuna.com:/minecraft/main/TBMC/pluginupdates +yes | scp -B -i upload_key -o StrictHostKeyChecking=no $FILENAME travis@server.figytuna.com:/minecraft/main/pluginupdates fi diff --git a/lombok.config b/lombok.config deleted file mode 100644 index 4f55f03..0000000 --- a/lombok.config +++ /dev/null @@ -1 +0,0 @@ -lombok.var.flagUsage = ALLOW \ No newline at end of file diff --git a/pom.xml b/pom.xml index 299d605..a6e0b14 100755 --- a/pom.xml +++ b/pom.xml @@ -1,271 +1,221 @@ - - 4.0.0 - - - com.github.TBMCPlugins.ButtonCore - CorePOM - master-SNAPSHOT - - - com.github.TBMCPlugins - DiscordPlugin - master-SNAPSHOT - jar - - DiscordPlugin - http://maven.apache.org - - - - src/main/java - - - src - - **/*.java - - - - src/main/resources - - *.properties - *.yml - *.csv - *.txt - - true - - - DiscordPlugin - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.1 - - - package - - shade - - - - - org.spigotmc:spigot-api - com.github.TBMCPlugins.ButtonCore:ButtonCore - net.ess3:Essentials - - - true - - - io.netty - btndvtm.dp.io.netty - - - - - - - - - - org.apache.maven.plugins - maven-resources-plugin - 3.0.1 - - - copy - compile - - copy-resources - - - target - - - resources - - - - - - - - - maven-surefire-plugin - 2.4.2 - - false - - - - - - - - UTF-8 - - master - - - - - - 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 - - - - - - - - junit - junit - 3.8.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.discord4j - discord4j-core - 3.0.12 - - - - org.slf4j - slf4j-jdk14 - 1.7.21 - - - com.github.TBMCPlugins.ButtonCore - ButtonCore - ${branch}-SNAPSHOT - provided - - - com.github.milkbowl - VaultAPI - master-SNAPSHOT - provided - - - net.ess3 - Essentials - 2.13.1 - provided - - - - org.projectlombok - lombok - 1.18.10 - provided - - - - - org.objenesis - objenesis - 2.6 - test - - - com.vdurmont - emoji-java - 4.0.0 - - - - - com.github.lucko - LuckPerms - v4.4 - provided - - - - - - ci - - - env.TRAVIS_BRANCH - - - - - ${env.TRAVIS_BRANCH} - - - - + + 4.0.0 + + com.github.TBMCPlugins + DiscordPlugin + master-SNAPSHOT + jar + + DiscordPlugin + http://maven.apache.org + + + + src/main/java + + + src + + **/*.java + + + + src/main/resources + + *.properties + *.yml + *.csv + *.txt + + true + + + DiscordPlugin + + + maven-compiler-plugin + 3.6.2 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.2 + + + package + + shade + + + + + org.spigotmc:spigot-api + com.github.TBMCPlugins.ButtonCore:ButtonCore + net.ess3:Essentials + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.0.1 + + + copy + compile + + copy-resources + + + target + + + resources + + + + + + + + + + + + UTF-8 + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + jcenter + http://jcenter.bintray.com + + + jitpack.io + https://jitpack.io + + + vault-repo + http://nexus.hc.to/content/repositories/pub_releases + + + Essentials + http://repo.ess3.net/content/repositories/essrel/ + + + projectlombok.org + http://projectlombok.org/mavenrepo + + + pex-repo + http://pex-repo.aoeu.xyz + + + + + + junit + junit + 3.8.1 + test + + + org.spigotmc + spigot-api + 1.12-R0.1-SNAPSHOT + provided + + + org.spigotmc + spigot + 1.12.2-R0.1-SNAPSHOT + provided + + + + com.discord4j + Discord4J + 2.10.1 + + + + org.slf4j + slf4j-jdk14 + 1.7.21 + + + com.github.TBMCPlugins.ButtonCore + ButtonCore + master-SNAPSHOT + provided + + + com.github.milkbowl + VaultAPI + master-SNAPSHOT + provided + + + net.ess3 + Essentials + 2.13.1 + provided + + + com.github.xaanit + D4J-OAuth + master-SNAPSHOT + + + + org.projectlombok + lombok + 1.16.16 + provided + + + ru.tehkode + PermissionsEx + 1.23.1 + provided + + + org.bukkit + bukkit + + + + + + org.objenesis + objenesis + 2.6 + test + + + com.vdurmont + emoji-java + 4.0.0 + + + 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 index 6f74671..e597095 100755 --- a/src/main/java/buttondevteam/discordplugin/ChromaBot.java +++ b/src/main/java/buttondevteam/discordplugin/ChromaBot.java @@ -1,14 +1,17 @@ package buttondevteam.discordplugin; -import buttondevteam.discordplugin.mcchat.MCChatUtils; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.MessageChannel; +import buttondevteam.discordplugin.listeners.MCChatListener; import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitScheduler; -import reactor.core.publisher.Mono; +import sx.blah.discord.api.internal.json.objects.EmbedObject; +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.util.EmbedBuilder; -import javax.annotation.Nullable; -import java.util.function.Function; +import java.awt.*; +import java.util.Arrays; +import java.util.stream.Collectors; public class ChromaBot { /** @@ -33,26 +36,111 @@ public class ChromaBot { } /** - * Send a message to the chat channels and private chats. - * + * Send a message to the chat channel and private chats. + * * @param message - * The message to send, duh (use {@link MessageChannel#createMessage(String)}) + * The message to send, duh */ - public void sendMessage(Function, Mono> message) { - MCChatUtils.forAllMCChat(ch -> message.apply(ch).subscribe()); + public void sendMessage(String message) { + MCChatListener.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message)); } - /** - * 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(ch -> message.apply(ch).subscribe(), toggle, false); - } + /** + * Send a message to the chat channels and private chats. + * + * @param message + * The message to send, duh + * @param embed + * Custom fancy stuff, use {@link EmbedBuilder} to create one + */ + public void sendMessage(String message, EmbedObject embed) { + MCChatListener.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, embed)); + } + + /** + * Send a message to an arbitrary channel. This will not send it to the private chats. + * + * @param channel + * The channel to send to, use the channel variables in {@link DiscordPlugin} + * @param message + * The message to send, duh + * @param embed + * Custom fancy stuff, use {@link EmbedBuilder} to create one + */ + public void sendMessage(IChannel channel, String message, EmbedObject embed) { + DiscordPlugin.sendMessageToChannel(channel, message, embed); + } + + /** + * Send a fancy message to the chat channels. This will show a bold text with a colored line. + * + * @param message + * The message to send, duh + * @param color + * The color of the line before the text + */ + public void sendMessage(String message, Color color) { + MCChatListener.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, + new EmbedBuilder().withTitle(message).withColor(color).build())); + } + + /** + * Send a fancy message to the chat channels. This will show a bold text with a colored line. + * + * @param message + * The message to send, duh + * @param color + * The color of the line before the text + * @param mcauthor + * The name of the Minecraft player who is the author of this message + */ + public void sendMessage(String message, Color color, String mcauthor) { + MCChatListener.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, + DPUtils.embedWithHead(new EmbedBuilder().withTitle(message).withColor(color), mcauthor).build())); + } + + /** + * Send a fancy message to the chat channels. This will show a bold text with a colored line. + * + * @param message + * The message to send, duh + * @param color + * The color of the line before the text + * @param authorname + * The name of the author of this message + * @param authorimg + * The URL of the avatar image for this message's author + */ + public void sendMessage(String message, Color color, String authorname, String authorimg) { + MCChatListener.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, new EmbedBuilder() + .withTitle(message).withColor(color).withAuthorName(authorname).withAuthorIcon(authorimg).build())); + } + + /** + * Send a message to the chat channels. This will show a bold text with a colored line. + * + * @param message + * The message to send, duh + * @param color + * The color of the line before the text + * @param sender + * The player who sends this message + */ + public void sendMessage(String message, Color color, Player sender) { + MCChatListener.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, DPUtils + .embedWithHead(new EmbedBuilder().withTitle(message).withColor(color), sender.getName()).build())); + } public void updatePlayerList() { - MCChatUtils.updatePlayerList(); + DPUtils.performNoWait(() -> { + String[] s = DiscordPlugin.chatchannel.getTopic().split("\\n----\\n"); + if (s.length < 3) + return; + s[0] = Bukkit.getOnlinePlayers().size() + " player" + (Bukkit.getOnlinePlayers().size() != 1 ? "s" : "") + + " online"; + s[s.length - 1] = "Players: " + Bukkit.getOnlinePlayers().stream() + .map(p -> DPUtils.sanitizeString(p.getDisplayName())).collect(Collectors.joining(", ")); + DiscordPlugin.chatchannel.changeTopic(Arrays.stream(s).collect(Collectors.joining("\n----\n"))); + }); } } diff --git a/src/main/java/buttondevteam/discordplugin/DPUtils.java b/src/main/java/buttondevteam/discordplugin/DPUtils.java index d118672..abc1f8f 100755 --- a/src/main/java/buttondevteam/discordplugin/DPUtils.java +++ b/src/main/java/buttondevteam/discordplugin/DPUtils.java @@ -1,219 +1,92 @@ 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.core.object.entity.Guild; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.MessageChannel; -import discord4j.core.object.entity.Role; -import discord4j.core.object.util.Snowflake; -import discord4j.core.spec.EmbedCreateSpec; -import lombok.val; -import reactor.core.publisher.Mono; +import org.bukkit.Bukkit; +import sx.blah.discord.util.EmbedBuilder; +import sx.blah.discord.util.RequestBuffer; +import sx.blah.discord.util.RequestBuffer.IRequest; +import sx.blah.discord.util.RequestBuffer.IVoidRequest; import 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; +import java.util.concurrent.TimeUnit; public final class DPUtils { - public static final Pattern URL_PATTERN = Pattern.compile("https?://\\S*"); - public 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"); + public static EmbedBuilder embedWithHead(EmbedBuilder builder, String playername) { + return builder.withAuthorIcon("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) - */ + /** Removes §[char] colour codes from strings */ 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(); + String sanitizedString = ""; boolean random = false; for (int i = 0; i < string.length(); i++) { if (string.charAt(i) == '§') { i++;// Skips the data value, the 4 in "§4Alisolarflare" - random = string.charAt(i) == 'k'; + if (string.charAt(i) == 'k') + random = true; + else + random = false; } else { if (!random) // Skip random/obfuscated characters - sanitizedString.append(string.charAt(i)); + sanitizedString += 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)); + return sanitizedString; } /** - * Needs to be a {@link ConfigData} for checking if it's set + * Performs Discord actions, retrying when ratelimited. May return null if action fails too many times or in safe mode. */ - 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); - } + @Nullable + public static T perform(IRequest action, long timeout, TimeUnit unit) { + if (DiscordPlugin.SafeMode) + return null; + if (Thread.currentThread() == DiscordPlugin.mainThread) // TODO: Ignore shutdown message <-- + // throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag."); + Bukkit.getLogger().warning("Waiting for a Discord request on the main thread!"); + try { + return RequestBuffer.request(action).get(timeout, unit); // Let the pros handle this + } catch (Exception e) { + TBMCCoreAPI.SendException("Couldn't perform Discord action!", e); + return null; + } + } - public static ReadOnlyConfigData snowflakeData(IHaveConfig config, String key, long defID) { - return config.getReadOnlyDataPrimDef(key, defID, id -> Snowflake.of((long) id), Snowflake::asLong); - } + /** + * Performs Discord actions, retrying when ratelimited. May return null if action fails too many times or in safe mode. + */ + @Nullable + public static T perform(IRequest action) { + if (DiscordPlugin.SafeMode) + return null; + if (Thread.currentThread() == DiscordPlugin.mainThread) // TODO: Ignore shutdown message <-- + // throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag."); + Bukkit.getLogger().warning("Waiting for a Discord request on the main thread!"); + return RequestBuffer.request(action).get(); // Let the pros handle this + } /** - * Mentions the bot channel. Useful for help texts. - * - * @return The string for mentioning the channel + * Performs Discord actions, retrying when ratelimited. */ - public static String botmention() { - if (DiscordPlugin.plugin == null) return "#bot"; - return channelMention(DiscordPlugin.plugin.commandChannel().get()); + public static Void perform(IVoidRequest action) { + if (DiscordPlugin.SafeMode) + return null; + if (Thread.currentThread() == DiscordPlugin.mainThread) + throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag."); + return RequestBuffer.request(action).get(); // Let the pros handle this } - /** - * 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; + public static void performNoWait(IVoidRequest action) { + if (DiscordPlugin.SafeMode) + return; + RequestBuffer.request(action); } - /** - * Disables the component if one of the given configs return null. Useful for channel/role configs. - * - * @param component The component to disable if needed - * @param config The (snowflake) config to check for null - * @param result The result of getting the value - * @return Whether the component got disabled and a warning logged - */ - public static boolean disableIfConfigErrorRes(@Nullable Component component, ConfigData config, Object result) { - //noinspection ConstantConditions - if (result == null || (result instanceof Mono && !((Mono) result).hasElement().block())) { - String path = null; - try { - if (component != null) - Component.setComponentEnabled(component, false); - path = config.getPath(); - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to disable component after config error!", e); - } - getLogger().warning("The config value " + path + " isn't set correctly " + (component == null ? "in global settings!" : "for component " + component.getClass().getSimpleName() + "!")); - getLogger().warning("Set the correct ID in the config" + (component == null ? "" : " or disable this component") + " to remove this message."); - return true; - } - return false; - } - - /** - * 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()); + public static void performNoWait(IRequest action) { + if (DiscordPlugin.SafeMode) + return; + RequestBuffer.request(action); } } diff --git a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java old mode 100644 new mode 100755 index ee0aa81..f5b779f --- a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java @@ -1,258 +1,20 @@ package buttondevteam.discordplugin; -import buttondevteam.discordplugin.mcchat.MinecraftChatModule; -import buttondevteam.discordplugin.playerfaker.DiscordInventory; -import buttondevteam.discordplugin.playerfaker.VCMDWrapper; -import discord4j.core.object.entity.MessageChannel; -import discord4j.core.object.entity.User; +import buttondevteam.discordplugin.playerfaker.DiscordFakePlayer; +import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Delegate; -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 sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IUser; -import java.lang.reflect.Modifier; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; import java.util.UUID; -import static org.mockito.Answers.RETURNS_DEFAULTS; +public class DiscordConnectedPlayer extends DiscordFakePlayer implements IMCPlayer { + private static int nextEntityId = 10000; + private @Getter VanillaCommandListener vanillaCmdListener; -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; - try { - vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this)); - if (vanillaCmdListener.getListener() == null) - DPUtils.getLogger().warning("Vanilla commands won't be available from Discord due to a compatibility error."); - } catch (NoClassDefFoundError e) { - DPUtils.getLogger().warning("Vanilla commands won't be available from Discord due to a compatibility error."); - } + public DiscordConnectedPlayer(IUser user, IChannel channel, UUID uuid, String mcname) { + super(user, channel, nextEntityId++, uuid, mcname); + vanillaCmdListener = new VanillaCommandListener<>(this); } - /** - * 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 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; - } - - 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); - } - }); - } } diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java b/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java index d748433..ec7863c 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java @@ -1,28 +1,28 @@ -package buttondevteam.discordplugin; - -import buttondevteam.discordplugin.mcchat.MCChatPrivate; -import buttondevteam.lib.player.ChromaGamerBase; -import buttondevteam.lib.player.UserClass; - -@UserClass(foldername = "discord") -public class DiscordPlayer extends ChromaGamerBase { - private String did; - // private @Getter @Setter boolean minecraftChatEnabled; - - public DiscordPlayer() { - } - - public String getDiscordID() { - if (did == null) - did = plugindata.getString(getFolder() + "_id"); - return did; - } - - /** - * Returns true if player has the private Minecraft chat enabled. For setting the value, see - * {@link MCChatPrivate#privateMCChat(sx.blah.discord.handle.obj.MessageChannel, boolean, sx.blah.discord.handle.obj.User, DiscordPlayer)} - */ - public boolean isMinecraftChatEnabled() { - return MCChatPrivate.isMinecraftChatEnabled(this); - } -} +package buttondevteam.discordplugin; + +import buttondevteam.discordplugin.listeners.MCChatListener; +import buttondevteam.lib.player.ChromaGamerBase; +import buttondevteam.lib.player.UserClass; + +@UserClass(foldername = "discord") +public class DiscordPlayer extends ChromaGamerBase { + private String did; + // private @Getter @Setter boolean minecraftChatEnabled; + + public DiscordPlayer() { + } + + public String getDiscordID() { + if (did == null) + did = plugindata.getString(getFolder() + "_id"); + return did; + } + + /** + * Returns true if player has the private Minecraft chat enabled. For setting the value, see + * {@link MCChatListener#privateMCChat(sx.blah.discord.handle.obj.IChannel, boolean, sx.blah.discord.handle.obj.IUser, DiscordPlayer)} + */ + public boolean isMinecraftChatEnabled() { + return MCChatListener.isMinecraftChatEnabled(this); + } +} diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java index 1e94b14..2c10314 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java @@ -1,8 +1,6 @@ package buttondevteam.discordplugin; -import buttondevteam.discordplugin.playerfaker.VCMDWrapper; -import discord4j.core.object.entity.MessageChannel; -import discord4j.core.object.entity.User; +import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; import lombok.Getter; import org.bukkit.*; import org.bukkit.advancement.Advancement; @@ -28,6 +26,8 @@ import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.scoreboard.Scoreboard; import org.bukkit.util.Vector; +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IUser; import java.net.InetSocketAddress; import java.util.*; @@ -36,18 +36,12 @@ import java.util.*; public class DiscordPlayerSender extends DiscordSenderBase implements IMCPlayer { protected Player player; - private @Getter VCMDWrapper vanillaCmdListener; + private @Getter VanillaCommandListener vanillaCmdListener; - public DiscordPlayerSender(User user, MessageChannel channel, Player player) { + public DiscordPlayerSender(IUser user, IChannel channel, Player player) { super(user, channel); this.player = player; - try { - vanillaCmdListener = new VCMDWrapper(VCMDWrapper.createListener(this, player)); - if (vanillaCmdListener.getListener() == null) - DPUtils.getLogger().warning("Vanilla commands won't be available from Discord due to a compatibility error."); - } catch (NoClassDefFoundError e) { - DPUtils.getLogger().warning("Vanilla commands won't be available from Discord due to a compatibility error."); - } + vanillaCmdListener = new VanillaCommandListener(this); } @Override @@ -304,6 +298,10 @@ public class DiscordPlayerSender extends DiscordSenderBase implements IMCPlayer< return player.addAttachment(plugin); } + public Block getTargetBlock(HashSet transparent, int maxDistance) { + return player.getTargetBlock(transparent, maxDistance); + } + public World getWorld() { return player.getWorld(); } @@ -352,6 +350,10 @@ public class DiscordPlayerSender extends DiscordSenderBase implements IMCPlayer< player.setCompassTarget(loc); } + public List getLastTwoTargetBlocks(HashSet transparent, int maxDistance) { + return player.getLastTwoTargetBlocks(transparent, maxDistance); + } + public Location getCompassTarget() { return player.getCompassTarget(); } @@ -1090,21 +1092,11 @@ public class DiscordPlayerSender extends DiscordSenderBase implements IMCPlayer< } public void hidePlayer(Player player) { - this.player.hidePlayer(player); - } - - @Override - public void hidePlayer(Plugin plugin, Player player) { - this.player.hidePlayer(plugin, player); + player.hidePlayer(player); } public void showPlayer(Player player) { - this.player.showPlayer(player); - } - - @Override - public void showPlayer(Plugin plugin, Player player) { - this.player.showPlayer(plugin, player); + player.showPlayer(player); } public boolean canSee(Player player) { diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java index a38389a..170c789 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java @@ -1,303 +1,429 @@ -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.MCChatPrivate; -import buttondevteam.discordplugin.mcchat.MCChatUtils; -import buttondevteam.discordplugin.mcchat.MinecraftChatModule; -import buttondevteam.discordplugin.mccommands.DiscordMCCommand; -import buttondevteam.discordplugin.role.GameRoleModule; -import buttondevteam.discordplugin.util.Timings; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.architecture.ButtonPlugin; -import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.architecture.ConfigData; -import buttondevteam.lib.architecture.IHaveConfig; -import buttondevteam.lib.player.ChromaGamerBase; -import com.google.common.io.Files; -import discord4j.core.DiscordClient; -import discord4j.core.DiscordClientBuilder; -import discord4j.core.event.domain.guild.GuildCreateEvent; -import discord4j.core.event.domain.lifecycle.ReadyEvent; -import discord4j.core.object.entity.Guild; -import discord4j.core.object.entity.Role; -import discord4j.core.object.presence.Activity; -import discord4j.core.object.presence.Presence; -import discord4j.core.object.reaction.ReactionEmoji; -import discord4j.core.object.util.Snowflake; -import discord4j.store.jdk.JdkStoreService; -import lombok.Getter; -import lombok.val; -import net.milkbowl.vault.permission.Permission; -import org.bukkit.Bukkit; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.plugin.RegisteredServiceProvider; -import reactor.core.publisher.Mono; - -import java.awt.*; -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -@ButtonPlugin.ConfigOpts(disableConfigGen = true) -public class DiscordPlugin extends ButtonPlugin { - public static DiscordClient dc; - public static DiscordPlugin plugin; - public static boolean SafeMode = true; - @Getter - private Command2DC manager; - - /** - * The prefix to use with Discord commands like /role. It only works in the bot channel. - */ - private ConfigData prefix() { - return getIConfig().getData("prefix", '/', str -> ((String) str).charAt(0), Object::toString); - } - - public static char getPrefix() { - if (plugin == null) return '/'; - return plugin.prefix().get(); - } - - /** - * 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() { - return 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() { - return DPUtils.roleData(getIConfig(), "modRole", "Moderator"); - } - - /** - * The invite link to show by /discord invite. If empty, it defaults to the first invite if the bot has access. - */ - public ConfigData inviteLink() { - return getIConfig().getData("inviteLink", ""); - } - - @Override - public void pluginEnable() { - try { - getLogger().info("Initializing..."); - plugin = this; - manager = new Command2DC(); - getCommand2MC().registerCommand(new DiscordMCCommand()); //Register so that the reset 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 reset"); - 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; - } - } - val cb = new DiscordClientBuilder(token); - cb.setInitialPresence(Presence.doNotDisturb(Activity.playing("booting"))); - cb.setStoreService(new JdkStoreService()); //The default doesn't work for some reason - it's waaay faster now - dc = cb.build(); - dc.getEventDispatcher().on(ReadyEvent.class) // Listen for ReadyEvent(s) - .map(event -> event.getGuilds().size()) // Get how many guilds the bot is in - .flatMap(size -> dc.getEventDispatcher() - .on(GuildCreateEvent.class) // Listen for GuildCreateEvent(s) - .take(size) // Take only the first `size` GuildCreateEvent(s) to be received - .collectList()) // Take all received GuildCreateEvents and make it a List - .subscribe(this::handleReady); /* All guilds have been received, client is fully connected */ - //dc.getEventDispatcher().on(DisconnectEvent.class); - dc.login().subscribe(); - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to enable the Discord plugin!", e); - getLogger().severe("You may be able to reset the plugin using /discord reset"); - } - } - - public static Guild mainServer; - - private void handleReady(List 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 reset"); - 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; - DPUtils.disableIfConfigErrorRes(null, commandChannel(), DPUtils.getMessageChannel(commandChannel())); - //Won't disable, just prints the warning here - - Component.registerComponent(this, new GeneralEventBroadcasterModule()); - Component.registerComponent(this, new MinecraftChatModule()); - Component.registerComponent(this, new ExceptionListenerModule()); - Component.registerComponent(this, new GameRoleModule()); //Needs the mainServer to be set - Component.registerComponent(this, new AnnouncerModule()); - Component.registerComponent(this, new FunModule()); - new ChromaBot(this).updatePlayerList(); //Initialize ChromaBot - The MCCHatModule is tested to be enabled - - getManager().registerCommand(new VersionCommand()); - getManager().registerCommand(new UserinfoCommand()); - getManager().registerCommand(new HelpCommand()); - getManager().registerCommand(new DebugCommand()); - getManager().registerCommand(new ConnectCommand()); - if (DiscordMCCommand.resetting) //These will only execute if the chat is enabled - ChromaBot.getInstance().sendMessageCustomAsWell(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(Color.CYAN) - .setTitle("Discord plugin restarted - chat connected."))), ChannelconBroadcast.RESTART); //Really important to note the chat, hmm - else if (getConfig().getBoolean("serverup", false)) { - ChromaBot.getInstance().sendMessageCustomAsWell(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(Color.YELLOW) - .setTitle("Server recovered from a crash - chat connected."))), ChannelconBroadcast.RESTART); - val thr = new Throwable( - "The server shut down unexpectedly. See the log of the previous run for more details."); - thr.setStackTrace(new StackTraceElement[0]); - TBMCCoreAPI.SendException("The server crashed!", thr); - } else - ChromaBot.getInstance().sendMessageCustomAsWell(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> ecs.setColor(Color.GREEN) - .setTitle("Server started - chat connected."))), ChannelconBroadcast.RESTART); - - DiscordMCCommand.resetting = false; //This is the last event handling this flag - - getConfig().set("serverup", true); - saveConfig(); - TBMCCoreAPI.SendUnsentExceptions(); - TBMCCoreAPI.SendUnsentDebugMessages(); - - CommonListeners.register(dc.getEventDispatcher()); - TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this); - TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class); - ChromaGamerBase.addConverter(sender -> Optional.ofNullable(sender instanceof DiscordSenderBase - ? ((DiscordSenderBase) sender).getChromaUser() : null)); - setupProviders(); - - IHaveConfig.pregenConfig(this, null); - if (!TBMCCoreAPI.IsTestServer()) { - dc.updatePresence(Presence.online(Activity.playing("Minecraft"))).subscribe(); - } else { - dc.updatePresence(Presence.online(Activity.playing("testing"))).subscribe(); - } - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occurred while enabling DiscordPlugin!", e); - } - } - - /** - * Always true, except when running "stop" from console - */ - public static boolean Restart; - - @Override - public void pluginPreDisable() { - if (ChromaBot.getInstance() == null) return; //Failed to load - Timings timings = new Timings(); - timings.printElapsed("Disable start"); - MCChatUtils.forCustomAndAllMCChat(chan -> chan.flatMap(ch -> ch.createEmbed(ecs -> { - timings.printElapsed("Sending message to " + ch.getMention()); - if (DiscordMCCommand.resetting) - ecs.setColor(Color.ORANGE).setTitle("Discord plugin restarting"); - else - ecs.setColor(Restart ? Color.ORANGE : Color.RED) - .setTitle(Restart ? "Server restarting" : "Server stopping") - .setDescription( - Bukkit.getOnlinePlayers().size() > 0 - ? (DPUtils - .sanitizeString(Bukkit.getOnlinePlayers().stream() - .map(Player::getDisplayName).collect(Collectors.joining(", "))) - + (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ") - + "kicked the hell out.") //TODO: Make configurable - : ""); //If 'restart' is disabled then this isn't shown even if joinleave is enabled - })).subscribe(), ChannelconBroadcast.RESTART, false); - timings.printElapsed("Updating player list"); - ChromaBot.getInstance().updatePlayerList(); - timings.printElapsed("Done"); - } - - @Override - public void pluginDisable() { - Timings timings = new Timings(); - timings.printElapsed("Actual disable start (logout)"); - MCChatPrivate.logoutAll(); - timings.printElapsed("Config setup"); - getConfig().set("serverup", false); - if (ChromaBot.getInstance() == null) return; //Failed to load - - saveConfig(); - try { - SafeMode = true; // Stop interacting with Discord - ChromaBot.delete(); - //timings.printElapsed("Updating presence..."); - //dc.updatePresence(Presence.idle(Activity.playing("logging out"))).block(); //No longer using the same account for testing - 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); - } - } - - public static final ReactionEmoji DELIVERED_REACTION = ReactionEmoji.unicode("✅"); - - public static Permission perms; - - private boolean setupProviders() { - try { - Class.forName("net.milkbowl.vault.permission.Permission"); - Class.forName("net.milkbowl.vault.chat.Chat"); - } catch (ClassNotFoundException e) { - return false; - } - - RegisteredServiceProvider permsProvider = Bukkit.getServer().getServicesManager() - .getRegistration(Permission.class); - perms = permsProvider.getProvider(); - return perms != null; - } -} +package buttondevteam.discordplugin; + +import buttondevteam.discordplugin.commands.DiscordCommandBase; +import buttondevteam.discordplugin.listeners.*; +import buttondevteam.discordplugin.mccommands.DiscordMCCommandBase; +import buttondevteam.lib.TBMCCoreAPI; +import buttondevteam.lib.chat.Channel; +import buttondevteam.lib.chat.TBMCChatAPI; +import buttondevteam.lib.player.ChromaGamerBase; +import com.google.common.io.Files; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.val; +import net.milkbowl.vault.permission.Permission; +import org.bukkit.Bukkit; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; +import sx.blah.discord.api.ClientBuilder; +import sx.blah.discord.api.IDiscordClient; +import sx.blah.discord.api.events.IListener; +import sx.blah.discord.api.internal.json.objects.EmbedObject; +import sx.blah.discord.handle.impl.events.ReadyEvent; +import sx.blah.discord.handle.impl.obj.ReactionEmoji; +import sx.blah.discord.handle.obj.*; +import sx.blah.discord.util.EmbedBuilder; +import sx.blah.discord.util.RequestBuffer; + +import java.awt.*; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class DiscordPlugin extends JavaPlugin implements IListener { + private static final String SubredditURL = "https://www.reddit.com/r/ChromaGamers"; + private static boolean stop = false; + static Thread mainThread; + public static IDiscordClient dc; + public static DiscordPlugin plugin; + public static boolean SafeMode = true; + public static List GameRoles; + public static boolean hooked = false; + + @SuppressWarnings("unchecked") + @Override + public void onEnable() { + try { + Bukkit.getLogger().info("Initializing DiscordPlugin..."); + try { + PlayerListWatcher.hookUp(); + hooked = true; + Bukkit.getLogger().info("Finished hooking into the player list"); + } catch (Throwable e) { + e.printStackTrace(); + Bukkit.getLogger().warning("Couldn't hook into the player list!"); + } + plugin = this; + lastannouncementtime = getConfig().getLong("lastannouncementtime"); + lastseentime = getConfig().getLong("lastseentime"); + ClientBuilder cb = new ClientBuilder(); + cb.withToken(Files.readFirstLine(new File("TBMC", "Token.txt"), StandardCharsets.UTF_8)); + dc = cb.login(); + dc.getDispatcher().registerListener(this); + mainThread = Thread.currentThread(); + } catch (Exception e) { + e.printStackTrace(); + Bukkit.getPluginManager().disablePlugin(this); + } + } + + public static IChannel botchannel; + public static IChannel annchannel; + public static IChannel genchannel; + public static IChannel chatchannel; + public static IChannel botroomchannel; + public static IChannel modlogchannel; + /** + * Don't send messages, just receive, the same channel is used when testing + */ + public static IChannel officechannel; + public static IChannel updatechannel; + public static IChannel devofficechannel; + public static IGuild mainServer; + public static IGuild devServer; + + private static volatile BukkitTask task; + private static volatile boolean sent = false; + + @Override + public void handle(ReadyEvent event) { + try { + dc.changePresence(StatusType.DND, ActivityType.PLAYING, "booting"); + task = Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> { + if (mainServer == null || devServer == null) { + mainServer = event.getClient().getGuildByID(125813020357165056L); + devServer = event.getClient().getGuildByID(219529124321034241L); + } + if (mainServer == null || devServer == null) + return; // Retry + if (!TBMCCoreAPI.IsTestServer()) { + botchannel = mainServer.getChannelByID(209720707188260864L); // bot + annchannel = mainServer.getChannelByID(126795071927353344L); // announcements + genchannel = mainServer.getChannelByID(125813020357165056L); // general + chatchannel = mainServer.getChannelByID(249663564057411596L); // minecraft_chat + botroomchannel = devServer.getChannelByID(239519012529111040L); // bot-room + officechannel = devServer.getChannelByID(219626707458457603L); // developers-office + updatechannel = devServer.getChannelByID(233724163519414272L); // server-updates + devofficechannel = officechannel; // developers-office + modlogchannel = mainServer.getChannelByID(283840717275791360L); // modlog + dc.changePresence(StatusType.ONLINE, ActivityType.PLAYING, "Chromacraft"); + } else { + botchannel = devServer.getChannelByID(239519012529111040L); // bot-room + annchannel = botchannel; // bot-room + genchannel = botchannel; // bot-room + botroomchannel = botchannel;// bot-room + chatchannel = botchannel;// bot-room + officechannel = devServer.getChannelByID(219626707458457603L); // developers-office + updatechannel = botchannel; + devofficechannel = botchannel;// bot-room + modlogchannel = botchannel; // bot-room + dc.changePresence(StatusType.ONLINE, ActivityType.PLAYING, "testing"); + } + if (botchannel == null || annchannel == null || genchannel == null || botroomchannel == null + || chatchannel == null || officechannel == null || updatechannel == null) + return; // Retry + SafeMode = false; + if (task != null) + task.cancel(); + if (!sent) { + new ChromaBot(this).updatePlayerList(); + GameRoles = mainServer.getRoles().stream().filter(this::isGameRole).map(IRole::getName).collect(Collectors.toList()); + + val chcons = getConfig().getConfigurationSection("chcons"); + if (chcons != null) { + val chconkeys = chcons.getKeys(false); + for (val chconkey : chconkeys) { + val chcon = chcons.getConfigurationSection(chconkey); + val mcch = Channel.getChannels().stream().filter(ch -> ch.ID.equals(chcon.getString("mcchid"))).findAny(); + val ch = dc.getChannelByID(chcon.getLong("chid")); + val did = chcon.getLong("did"); + val dp = DiscordPlayer.getUser(Long.toString(did), DiscordPlayer.class); + val user = dc.fetchUser(did); + val dcp = new DiscordConnectedPlayer(user, ch, UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname")); + val groupid = chcon.getString("groupid"); + if (!mcch.isPresent() || ch == null || user == null || groupid == null) + continue; + MCChatListener.addCustomChat(ch, groupid, mcch.get(), dp, user, dcp); + } + } + + DiscordCommandBase.registerCommands(); + if (getConfig().getBoolean("serverup", false)) { + ChromaBot.getInstance().sendMessage("", new EmbedBuilder().withColor(Color.YELLOW) + .withTitle("Server recovered from a crash - chat connected.").build()); + val thr = new Throwable( + "The server shut down unexpectedly. See the log of the previous run for more details."); + thr.setStackTrace(new StackTraceElement[0]); + TBMCCoreAPI.SendException("The server crashed!", thr); + } else + ChromaBot.getInstance().sendMessage("", new EmbedBuilder().withColor(Color.GREEN) + .withTitle("Server started - chat connected.").build()); + getConfig().set("serverup", true); + saveConfig(); + DPUtils.performNoWait(() -> { + try { + List msgs = genchannel.getPinnedMessages(); + for (int i = msgs.size() - 1; i >= 10; i--) { // Unpin all pinned messages except the newest 10 + genchannel.unpin(msgs.get(i)); + Thread.sleep(10); + } + } catch (InterruptedException ignore) { + } + }); + sent = true; + if (TBMCCoreAPI.IsTestServer() && !dc.getOurUser().getName().toLowerCase().contains("test")) { + TBMCCoreAPI.SendException( + "Won't load because we're in testing mode and not using the separate account.", + new Exception( + "The plugin refuses to load until you change the token to the testing account.")); + Bukkit.getPluginManager().disablePlugin(this); + } + TBMCCoreAPI.SendUnsentExceptions(); + TBMCCoreAPI.SendUnsentDebugMessages(); + /*if (!TBMCCoreAPI.IsTestServer()) { + final Calendar currentCal = Calendar.getInstance(); + final Calendar newCal = Calendar.getInstance(); + currentCal.set(currentCal.get(Calendar.YEAR), currentCal.get(Calendar.MONTH), + currentCal.get(Calendar.DAY_OF_MONTH), 4, 10); + if (currentCal.get(Calendar.DAY_OF_MONTH) % 9 == 0 && currentCal.before(newCal)) { + Random rand = new Random(); + sendMessageToChannel(dc.getChannels().get(rand.nextInt(dc.getChannels().size())), + "You could make a religion out of this"); + } + }*/ + } + }, 0, 10); + for (IListener listener : CommandListener.getListeners()) + dc.getDispatcher().registerListener(listener); + MCChatListener mcchat = new MCChatListener(); + dc.getDispatcher().registerListener(mcchat); + TBMCCoreAPI.RegisterEventsForExceptions(mcchat, this); + TBMCCoreAPI.RegisterEventsForExceptions(new AutoUpdaterListener(), this); + Bukkit.getPluginManager().registerEvents(new ExceptionListener(), this); + TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this); + TBMCChatAPI.AddCommands(this, DiscordMCCommandBase.class); + TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class); + new Thread(this::AnnouncementGetterThreadMethod).start(); + setupProviders(); + /* + * IDiscordOAuth doa = new DiscordOAuthBuilder(dc).withClientID("226443037893591041") .withClientSecret(getConfig().getString("appsecret")) .withRedirectUrl("https://" + + * (TBMCCoreAPI.IsTestServer() ? "localhost" : "server.figytuna.com") + ":8081/callback") .withScopes(Scope.IDENTIFY).withHttpServerOptions(new HttpServerOptions().setPort(8081)) + * .withSuccessHandler((rc, user) -> { rc.response().headers().add("Location", "https://" + (TBMCCoreAPI.IsTestServer() ? "localhost" : "server.figytuna.com") + ":8080/login?type=discord&" + * + rc.request().query()); rc.response().setStatusCode(303); rc.response().end("Redirecting"); rc.response().close(); }).withFailureHandler(rc -> { rc.response().headers().add("Location", + * "https://" + (TBMCCoreAPI.IsTestServer() ? "localhost" : "server.figytuna.com") + ":8080/login?type=discord&" + rc.request().query()); rc.response().setStatusCode(303); + * rc.response().end("Redirecting"); rc.response().close(); }).build(); getLogger().info("Auth URL: " + doa.buildAuthUrl()); + */ + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occured while enabling DiscordPlugin!", e); + } + } + + public boolean isGameRole(IRole r) { + if (r.getGuild().getLongID() != mainServer.getLongID()) + return false; //Only allow on the main server + val rc = new Color(149, 165, 166, 0); + return r.getColor().equals(rc) + && r.getPosition() < mainServer.getRoleByID(234343495735836672L).getPosition(); //Below the ChromaBot role + } + + /** + * Always true, except when running "stop" from console + */ + public static boolean Restart; + + @Override + public void onDisable() { + stop = true; + for (val entry : MCChatListener.ConnectedSenders.entrySet()) + MCListener.callEventExcludingSome(new PlayerQuitEvent(entry.getValue(), "")); + getConfig().set("lastannouncementtime", lastannouncementtime); + getConfig().set("lastseentime", lastseentime); + getConfig().set("serverup", false); + + val chcons = MCChatListener.getCustomChats(); + val chconsc = getConfig().createSection("chcons"); + for (val chcon : chcons) { + val chconc = chconsc.createSection(chcon.channel.getStringID()); + chconc.set("mcchid", chcon.mcchannel.ID); + chconc.set("chid", chcon.channel.getLongID()); + chconc.set("did", chcon.user.getLongID()); + chconc.set("mcuid", chcon.dcp.getUniqueId().toString()); + chconc.set("mcname", chcon.dcp.getName()); + chconc.set("groupid", chcon.groupID); + } + + saveConfig(); + MCChatListener.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannelWait(ch, "", + new EmbedBuilder().withColor(Restart ? Color.ORANGE : Color.RED) + .withTitle(Restart ? "Server restarting" : "Server stopping") + .withDescription( + Bukkit.getOnlinePlayers().size() > 0 + ? (DPUtils + .sanitizeString(Bukkit.getOnlinePlayers().stream() + .map(p -> p.getDisplayName()).collect(Collectors.joining(", "))) + + (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ") + + "asked *politely* to leave the server for a bit.") + : "") + .build(), 5, TimeUnit.SECONDS)); + ChromaBot.getInstance().updatePlayerList(); + try { + SafeMode = true; // Stop interacting with Discord + MCChatListener.stop(); + ChromaBot.delete(); + dc.changePresence(StatusType.IDLE, ActivityType.PLAYING, "Chromacraft"); //No longer using the same account for testing + dc.logout(); + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e); + } + } + + private long lastannouncementtime = 0; + private long lastseentime = 0; + public static final ReactionEmoji DELIVERED_REACTION = ReactionEmoji.of("✅"); + + private void AnnouncementGetterThreadMethod() { + while (!stop) { + try { + if (SafeMode) { + Thread.sleep(10000); + continue; + } + String body = TBMCCoreAPI.DownloadString(SubredditURL + "/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; + 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) + lastseentime = date; + else if (date > lastannouncementtime) { + 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) + genchannel.pin(sendMessageToChannelWait(genchannel, msgsb.toString())); + if (modmsgsb.length() > 0) + sendMessageToChannel(annchannel, modmsgsb.toString()); + if (lastannouncementtime != lastanntime) { + lastannouncementtime = lastanntime; // If sending succeeded + getConfig().set("lastannouncementtime", lastannouncementtime); + getConfig().set("lastseentime", lastseentime); + saveConfig(); + } + } catch (Exception e) { + e.printStackTrace(); + } + try { + Thread.sleep(10000); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + + public static void sendMessageToChannel(IChannel channel, String message) { + sendMessageToChannel(channel, message, null); + } + + public static void sendMessageToChannel(IChannel channel, String message, EmbedObject embed) { + sendMessageToChannel(channel, message, embed, false); + } + + public static IMessage sendMessageToChannelWait(IChannel channel, String message) { + return sendMessageToChannelWait(channel, message, null); + } + + public static IMessage sendMessageToChannelWait(IChannel channel, String message, EmbedObject embed) { + return sendMessageToChannel(channel, message, embed, true); + } + + public static IMessage sendMessageToChannelWait(IChannel channel, String message, EmbedObject embed, long timeout, TimeUnit unit) { + return sendMessageToChannel(channel, message, embed, true, timeout, unit); + } + + private static IMessage sendMessageToChannel(IChannel channel, String message, EmbedObject embed, boolean wait) { + return sendMessageToChannel(channel, message, embed, wait, -1, null); + } + + private static IMessage sendMessageToChannel(IChannel channel, String message, EmbedObject embed, boolean wait, long timeout, TimeUnit unit) { + if (message.length() > 1980) { + message = message.substring(0, 1980); + Bukkit.getLogger() + .warning("Message was too long to send to discord and got truncated. In " + channel.getName()); + } + try { + if (channel == chatchannel) + MCChatListener.resetLastMessage(); // If this is a chat message, it'll be set again + else if (channel.isPrivate()) + MCChatListener.resetLastMessage(channel); + final String content = message; + RequestBuffer.IRequest r = () -> embed == null ? channel.sendMessage(content) + : channel.sendMessage(content, embed, false); + if (wait) { + if (unit != null) + return DPUtils.perform(r, timeout, unit); + else + return DPUtils.perform(r); + } else { + if (unit != null) + plugin.getLogger().warning("Tried to set timeout for non-waiting call."); + else + DPUtils.performNoWait(r); + return null; + } + } catch (Exception e) { + Bukkit.getLogger().warning( + "Failed to deliver message to Discord! Channel: " + channel.getName() + " Message: " + message); + throw new RuntimeException(e); + } + } + + public static Permission perms; + + public boolean setupProviders() { + try { + Class.forName("net.milkbowl.vault.permission.Permission"); + Class.forName("net.milkbowl.vault.chat.Chat"); + } catch (ClassNotFoundException e) { + return false; + } + + RegisteredServiceProvider permsProvider = Bukkit.getServer().getServicesManager() + .getRegistration(Permission.class); + perms = permsProvider.getProvider(); + return perms != null; + } +} diff --git a/src/main/java/buttondevteam/discordplugin/DiscordRunnable.java b/src/main/java/buttondevteam/discordplugin/DiscordRunnable.java new file mode 100755 index 0000000..fb27234 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/DiscordRunnable.java @@ -0,0 +1,10 @@ +package buttondevteam.discordplugin; + +import sx.blah.discord.util.DiscordException; +import sx.blah.discord.util.MissingPermissionsException; +import sx.blah.discord.util.RateLimitException; + +@FunctionalInterface +public interface DiscordRunnable { + public abstract void run() throws DiscordException, RateLimitException, MissingPermissionsException; +} diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSender.java b/src/main/java/buttondevteam/discordplugin/DiscordSender.java index 0381e66..f2f829b 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordSender.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordSender.java @@ -1,9 +1,5 @@ package buttondevteam.discordplugin; -import discord4j.core.object.entity.Member; -import discord4j.core.object.entity.MessageChannel; -import discord4j.core.object.entity.User; -import lombok.val; import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.command.CommandSender; @@ -12,23 +8,21 @@ 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 sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IUser; import java.util.Set; public class DiscordSender extends DiscordSenderBase implements CommandSender { private PermissibleBase perm = new PermissibleBase(this); - private String name; + private String name = null; - public DiscordSender(User user, MessageChannel channel) { + public DiscordSender(IUser user, IChannel 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) { + public DiscordSender(IUser user, IChannel channel, String name) { super(user, channel); this.name = name; } @@ -91,12 +85,12 @@ public class DiscordSender extends DiscordSenderBase implements CommandSender { } @Override - public boolean isOp() { + public boolean isOp() { // TODO: Connect with TBMC acc return false; } @Override - public void setOp(boolean value) { + public void setOp(boolean value) { // TODO: Connect with TBMC acc } @Override @@ -106,7 +100,7 @@ public class DiscordSender extends DiscordSenderBase implements CommandSender { @Override public String getName() { - return name; + return name == null ? user == null ? "Discord user" : user.getDisplayName(DiscordPlugin.mainServer) : name; } @Override diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java b/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java index 103a350..0a6e392 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java @@ -1,20 +1,28 @@ package buttondevteam.discordplugin; import buttondevteam.lib.TBMCCoreAPI; -import discord4j.core.object.entity.MessageChannel; -import discord4j.core.object.entity.User; +import buttondevteam.lib.chat.Channel; +import buttondevteam.lib.chat.IDiscordSender; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; import org.bukkit.scheduler.BukkitTask; +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IUser; -public abstract class DiscordSenderBase implements CommandSender { +import java.util.Arrays; +import java.util.stream.Collectors; + +public abstract class DiscordSenderBase implements IDiscordSender { /** * May be null. */ - protected User user; - protected MessageChannel channel; + protected IUser user; + protected IChannel channel; + private @Getter @Setter @NonNull Channel mcchannel = Channel.GlobalChat; - protected DiscordSenderBase(User user, MessageChannel channel) { + protected DiscordSenderBase(IUser user, IChannel channel) { this.user = user; this.channel = channel; } @@ -27,41 +35,29 @@ public abstract class DiscordSenderBase implements CommandSender { * * @return The user or null. */ - public User getUser() { + public IUser getUser() { return user; } - public MessageChannel getChannel() { + public IChannel 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 && DiscordPlugin.hooked) - TODO: What should happen if unhooked if (broadcast) return; final String sendmsg = DPUtils.sanitizeString(message); msgtosend += "\n" + sendmsg; if (sendtask == null) sendtask = Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> { - channel.createMessage((!broadcast && user != null ? user.getMention() + "\n" : "") + msgtosend.trim()).subscribe(); + DiscordPlugin.sendMessageToChannel(channel, + (!broadcast && user != null ? user.mention() + "\n" : "") + msgtosend.trim()); sendtask = null; msgtosend = ""; - }, 4); // Waits a 0.2 second to gather all/most of the different messages + }, 10); // Waits a half second to gather all/most of the different messages } catch (Exception e) { TBMCCoreAPI.SendException("An error occured while sending message to DiscordSender", e); } @@ -69,6 +65,6 @@ public abstract class DiscordSenderBase implements CommandSender { @Override public void sendMessage(String[] messages) { - sendMessage(String.join("\n", messages)); + sendMessage(Arrays.stream(messages).collect(Collectors.joining("\n"))); } } diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSupplier.java b/src/main/java/buttondevteam/discordplugin/DiscordSupplier.java new file mode 100755 index 0000000..e2fb570 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/DiscordSupplier.java @@ -0,0 +1,11 @@ +package buttondevteam.discordplugin; + +import sx.blah.discord.handle.obj.IDiscordObject; +import sx.blah.discord.util.DiscordException; +import sx.blah.discord.util.MissingPermissionsException; +import sx.blah.discord.util.RateLimitException; + +@FunctionalInterface +public interface DiscordSupplier> { + public abstract T get() throws DiscordException, RateLimitException, MissingPermissionsException; +} diff --git a/src/main/java/buttondevteam/discordplugin/IMCPlayer.java b/src/main/java/buttondevteam/discordplugin/IMCPlayer.java index c2ee28e..854db2b 100755 --- a/src/main/java/buttondevteam/discordplugin/IMCPlayer.java +++ b/src/main/java/buttondevteam/discordplugin/IMCPlayer.java @@ -1,8 +1,8 @@ package buttondevteam.discordplugin; -import buttondevteam.discordplugin.playerfaker.VCMDWrapper; +import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; import org.bukkit.entity.Player; -public interface IMCPlayer extends Player { - VCMDWrapper getVanillaCmdListener(); +public interface IMCPlayer> extends Player { + VanillaCommandListener getVanillaCmdListener(); } diff --git a/src/main/java/buttondevteam/discordplugin/PlayerListWatcher.java b/src/main/java/buttondevteam/discordplugin/PlayerListWatcher.java new file mode 100755 index 0000000..e056737 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/PlayerListWatcher.java @@ -0,0 +1,352 @@ +package buttondevteam.discordplugin; + +import buttondevteam.discordplugin.listeners.MCChatListener; +import buttondevteam.lib.TBMCCoreAPI; +import com.mojang.authlib.GameProfile; +import lombok.val; +import net.minecraft.server.v1_12_R1.*; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.craftbukkit.v1_12_R1.CraftServer; +import org.bukkit.craftbukkit.v1_12_R1.util.CraftChatMessage; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; +import org.objenesis.ObjenesisStd; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.UUID; + +public class PlayerListWatcher extends DedicatedPlayerList { + private DedicatedPlayerList plist; + + public PlayerListWatcher(DedicatedServer minecraftserver) { + super(minecraftserver); // <-- Does some init stuff and calls Bukkit.setServer() so we have to use Objenesis + } + + public void sendAll(Packet packet) { + plist.sendAll(packet); + try { // Some messages get sent by directly constructing a packet + if (packet instanceof PacketPlayOutChat) { + Field msgf = PacketPlayOutChat.class.getDeclaredField("a"); + msgf.setAccessible(true); + MCChatListener.sendSystemMessageToChat(((IChatBaseComponent) msgf.get(packet)).toPlainText()); + } + } catch (Exception e) { + TBMCCoreAPI.SendException("Failed to broadcast message sent to all players - hacking failed.", e); + } + } + + @Override + public void sendMessage(IChatBaseComponent ichatbasecomponent, boolean flag) { // Needed so it calls the overriden method + plist.getServer().sendMessage(ichatbasecomponent); + ChatMessageType chatmessagetype = flag ? ChatMessageType.SYSTEM : ChatMessageType.CHAT; + + // CraftBukkit start - we run this through our processor first so we can get web links etc + this.sendAll(new PacketPlayOutChat(CraftChatMessage.fixComponent(ichatbasecomponent), chatmessagetype)); + // CraftBukkit end + } + + @Override + public void sendMessage(IChatBaseComponent ichatbasecomponent) { // Needed so it calls the overriden method + this.sendMessage(ichatbasecomponent, true); + } + + @Override + public void sendMessage(IChatBaseComponent[] iChatBaseComponents) { // Needed so it calls the overriden method + for (IChatBaseComponent component : iChatBaseComponents) { + sendMessage(component, true); + } + } + + public static void hookUp() { + try { + Field conf = CraftServer.class.getDeclaredField("console"); + conf.setAccessible(true); + val server = (MinecraftServer) conf.get(Bukkit.getServer()); + val plw = new ObjenesisStd().newInstance(PlayerListWatcher.class); // Cannot call super constructor + plw.plist = (DedicatedPlayerList) server.getPlayerList(); + plw.maxPlayers = plw.plist.getMaxPlayers(); + Field plf = plw.getClass().getField("players"); + plf.setAccessible(true); + Field modf = plf.getClass().getDeclaredField("modifiers"); + modf.setAccessible(true); + modf.set(plf, plf.getModifiers() & ~Modifier.FINAL); + plf.set(plw, plw.plist.players); + server.a(plw); + Field pllf = CraftServer.class.getDeclaredField("playerList"); + pllf.setAccessible(true); + pllf.set(Bukkit.getServer(), plw); + } catch (Exception e) { + TBMCCoreAPI.SendException("Error while hacking the player list!", e); + } + } + + public void a(EntityHuman entityhuman, IChatBaseComponent ichatbasecomponent) { + plist.a(entityhuman, ichatbasecomponent); + } + + public void a(EntityPlayer entityplayer, int i) { + plist.a(entityplayer, i); + } + + public void a(EntityPlayer entityplayer, WorldServer worldserver) { + plist.a(entityplayer, worldserver); + } + + public NBTTagCompound a(EntityPlayer entityplayer) { + return plist.a(entityplayer); + } + + public void a(int i) { + plist.a(i); + } + + public void a(NetworkManager networkmanager, EntityPlayer entityplayer) { + plist.a(networkmanager, entityplayer); + } + + public void a(Packet packet, int i) { + plist.a(packet, i); + } + + public EntityPlayer a(UUID uuid) { + return plist.a(uuid); + } + + public void addOp(GameProfile gameprofile) { + plist.addOp(gameprofile); + } + + public void addWhitelist(GameProfile gameprofile) { + plist.addWhitelist(gameprofile); + } + + public EntityPlayer attemptLogin(LoginListener loginlistener, GameProfile gameprofile, String hostname) { + return plist.attemptLogin(loginlistener, gameprofile, hostname); + } + + public String b(boolean flag) { + return plist.b(flag); + } + + public void b(EntityHuman entityhuman, IChatBaseComponent ichatbasecomponent) { + plist.b(entityhuman, ichatbasecomponent); + } + + public void b(EntityPlayer entityplayer, WorldServer worldserver) { + plist.b(entityplayer, worldserver); + } + + public List b(String s) { + return plist.b(s); + } + + public Location calculateTarget(Location enter, World target) { + return plist.calculateTarget(enter, target); + } + + public void changeDimension(EntityPlayer entityplayer, int i, TeleportCause cause) { + plist.changeDimension(entityplayer, i, cause); + } + + public void changeWorld(Entity entity, int i, WorldServer worldserver, WorldServer worldserver1) { + plist.changeWorld(entity, i, worldserver, worldserver1); + } + + public int d() { + return plist.d(); + } + + public void d(EntityPlayer entityplayer) { + plist.d(entityplayer); + } + + public String disconnect(EntityPlayer entityplayer) { + return plist.disconnect(entityplayer); + } + + public boolean equals(Object obj) { + return plist.equals(obj); + } + + public String[] f() { + return plist.f(); + } + + public void f(EntityPlayer entityplayer) { + plist.f(entityplayer); + } + + public boolean f(GameProfile gameprofile) { + return plist.f(gameprofile); + } + + public GameProfile[] g() { + return plist.g(); + } + + public boolean getHasWhitelist() { + return plist.getHasWhitelist(); + } + + public IpBanList getIPBans() { + return plist.getIPBans(); + } + + public int getMaxPlayers() { + return plist.getMaxPlayers(); + } + + public OpList getOPs() { + return plist.getOPs(); + } + + public EntityPlayer getPlayer(String s) { + return plist.getPlayer(s); + } + + public int getPlayerCount() { + return plist.getPlayerCount(); + } + + public GameProfileBanList getProfileBans() { + return plist.getProfileBans(); + } + + public String[] getSeenPlayers() { + return plist.getSeenPlayers(); + } + + public DedicatedServer getServer() { + return plist.getServer(); + } + + public WhiteList getWhitelist() { + return plist.getWhitelist(); + } + + public String[] getWhitelisted() { + return plist.getWhitelisted(); + } + + public AdvancementDataPlayer h(EntityPlayer entityplayer) { + return plist.h(entityplayer); + } + + public int hashCode() { + return plist.hashCode(); + } + + public boolean isOp(GameProfile gameprofile) { + return plist.isOp(gameprofile); + } + + public boolean isWhitelisted(GameProfile gameprofile) { + return plist.isWhitelisted(gameprofile); + } + + public EntityPlayer moveToWorld(EntityPlayer entityplayer, int i, boolean flag, Location location, + boolean avoidSuffocation) { + return plist.moveToWorld(entityplayer, i, flag, location, avoidSuffocation); + } + + public EntityPlayer moveToWorld(EntityPlayer entityplayer, int i, boolean flag) { + return plist.moveToWorld(entityplayer, i, flag); + } + + public String[] n() { + return plist.n(); + } + + public void onPlayerJoin(EntityPlayer entityplayer, String joinMessage) { + plist.onPlayerJoin(entityplayer, joinMessage); + } + + public EntityPlayer processLogin(GameProfile gameprofile, EntityPlayer player) { + return plist.processLogin(gameprofile, player); + } + + public void reload() { + plist.reload(); + } + + public void reloadWhitelist() { + plist.reloadWhitelist(); + } + + public void removeOp(GameProfile gameprofile) { + plist.removeOp(gameprofile); + } + + public void removeWhitelist(GameProfile gameprofile) { + plist.removeWhitelist(gameprofile); + } + + public void repositionEntity(Entity entity, Location exit, boolean portal) { + plist.repositionEntity(entity, exit, portal); + } + + public int s() { + return plist.s(); + } + + public void savePlayers() { + plist.savePlayers(); + } + + @SuppressWarnings("rawtypes") + public void sendAll(Packet packet, EntityHuman entityhuman) { + plist.sendAll(packet, entityhuman); + } + + @SuppressWarnings("rawtypes") + public void sendAll(Packet packet, World world) { + plist.sendAll(packet, world); + } + + public void sendPacketNearby(EntityHuman entityhuman, double d0, double d1, double d2, double d3, int i, + Packet packet) { + plist.sendPacketNearby(entityhuman, d0, d1, d2, d3, i, packet); + } + + public void sendScoreboard(ScoreboardServer scoreboardserver, EntityPlayer entityplayer) { + plist.sendScoreboard(scoreboardserver, entityplayer); + } + + public void setHasWhitelist(boolean flag) { + plist.setHasWhitelist(flag); + } + + public void setPlayerFileData(WorldServer[] aworldserver) { + plist.setPlayerFileData(aworldserver); + } + + public NBTTagCompound t() { + return plist.t(); + } + + public void tick() { + plist.tick(); + } + + public String toString() { + return plist.toString(); + } + + public void u() { + plist.u(); + } + + public void updateClient(EntityPlayer entityplayer) { + plist.updateClient(entityplayer); + } + + public List v() { + return plist.v(); + } + + public ServerStatisticManager getStatisticManager(EntityPlayer entityhuman) { + return plist.getStatisticManager(entityhuman); + } +} 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 2b68de0..0000000 --- a/src/main/java/buttondevteam/discordplugin/announcer/AnnouncerModule.java +++ /dev/null @@ -1,144 +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.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 ReadOnlyConfigData> channel() { - return DPUtils.channelData(getConfig(), "channel"); - } - - /** - * Channel where distinguished (moderator) posts go. - */ - public ReadOnlyConfigData> modChannel() { - return DPUtils.channelData(getConfig(), "modChannel"); - } - - /** - * Automatically unpins all messages except the last few. Set to 0 or >50 to disable - */ - public ConfigData keepPinned() { - return getConfig().getData("keepPinned", (short) 40); - } - - private ConfigData lastAnnouncementTime() { - return getConfig().getData("lastAnnouncementTime", 0L); - } - - private ConfigData lastSeenTime() { - return getConfig().getData("lastSeenTime", 0L); - } - - /** - * The subreddit to pull the posts from - */ - private ConfigData subredditURL() { - return 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 keepPinned = keepPinned().get(); - if (keepPinned == 0) return; - Flux msgs = channel().get().flatMapMany(MessageChannel::getPinnedMessages); - msgs.subscribe(Message::unpin); - new Thread(this::AnnouncementGetterThreadMethod).start(); - } - - @Override - protected void disable() { - stop = true; - } - - private void AnnouncementGetterThreadMethod() { - while (!stop) { - try { - if (!isEnabled()) { - 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()) { - 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 { - 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 5a0317e..0000000 --- a/src/main/java/buttondevteam/discordplugin/broadcaster/GeneralEventBroadcasterModule.java +++ /dev/null @@ -1,44 +0,0 @@ -package buttondevteam.discordplugin.broadcaster; - -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.architecture.Component; -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. - */ -public class GeneralEventBroadcasterModule extends Component { - private static @Getter boolean hooked = false; - - @Override - protected void enable() { - try { - PlayerListWatcher.hookUpDown(true); - DPUtils.getLogger().info("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); - } catch (NoClassDefFoundError e) { - DPUtils.getLogger().warning("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)) - DPUtils.getLogger().info("Finished unhooking the player list!"); - else - DPUtils.getLogger().info("Didn't have the player list hooked."); - hooked = false; - } catch (Exception e) { - TBMCCoreAPI.SendException("Error while hacking the player list!", e); - } 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 a424555..0000000 --- a/src/main/java/buttondevteam/discordplugin/broadcaster/PlayerListWatcher.java +++ /dev/null @@ -1,166 +0,0 @@ -package buttondevteam.discordplugin.broadcaster; - -import buttondevteam.discordplugin.mcchat.MCChatUtils; -import buttondevteam.lib.TBMCCoreAPI; -import lombok.val; -import org.bukkit.Bukkit; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - -public class PlayerListWatcher { - private static Object plist; - private static Object mock; - - /*public PlayerListWatcher(DedicatedServer minecraftserver) { - super(minecraftserver); // <-- Does some init stuff and calls Bukkit.setServer() so we have to use Objenesis - } - - public void sendAll(Packet packet) { - plist.sendAll(packet); - try { // Some messages get sent by directly constructing a packet - if (packet instanceof PacketPlayOutChat) { - Field msgf = PacketPlayOutChat.class.getDeclaredField("a"); - msgf.setAccessible(true); - MCChatUtils.forAllMCChat(MCChatUtils.send(((IChatBaseComponent) msgf.get(packet)).toPlainText())); - } - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to broadcast message sent to all players - hacking failed.", e); - } - } - - @Override - public void sendMessage(IChatBaseComponent ichatbasecomponent, boolean flag) { // Needed so it calls the overridden method - plist.getServer().sendMessage(ichatbasecomponent); - ChatMessageType chatmessagetype = flag ? ChatMessageType.SYSTEM : ChatMessageType.CHAT; - - // CraftBukkit start - we run this through our processor first so we can get web links etc - this.sendAll(new PacketPlayOutChat(CraftChatMessage.fixComponent(ichatbasecomponent), chatmessagetype)); - // CraftBukkit end - } - - @Override - public void sendMessage(IChatBaseComponent ichatbasecomponent) { // Needed so it calls the overriden method - this.sendMessage(ichatbasecomponent, true); - } - - @Override - public void sendMessage(IChatBaseComponent[] iChatBaseComponents) { // Needed so it calls the overridden method - for (IChatBaseComponent component : iChatBaseComponents) { - sendMessage(component, true); - } - }*/ - - static boolean hookUpDown(boolean up) 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) { - val icbcl = Class.forName(nms + ".IChatBaseComponent"); - val sendMessage = server.getClass().getMethod("sendMessage", icbcl); - 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"); - val ppocC = Class.forName(nms + ".PacketPlayOutChat").getConstructor(icbcl, cmtcl); - 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; - mock = Mockito.mock(dplc, 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; - } - 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.")); - 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")); - return null; - } - - private void sendMessage(Object chatComponent, boolean system) { - try { //Converted to use reflection - 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 - this.sendAll(ppocC.newInstance(fixComponent.invoke(null, chatComponent), chatmessagetype)); - // CraftBukkit end - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occurred while passing a vanilla message through the player list", e); - } - } - - 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.forAllMCChat(MCChatUtils.send((String) toPlainText.invoke(msgf.get(packet)))); - } - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to broadcast message sent to all players - hacking failed.", e); - } - } - }); - 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/ChannelconCommand.java b/src/main/java/buttondevteam/discordplugin/commands/ChannelconCommand.java new file mode 100644 index 0000000..d9a1c8f --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/commands/ChannelconCommand.java @@ -0,0 +1,83 @@ +package buttondevteam.discordplugin.commands; + +import buttondevteam.discordplugin.DiscordConnectedPlayer; +import buttondevteam.discordplugin.DiscordPlayer; +import buttondevteam.discordplugin.listeners.MCChatListener; +import buttondevteam.lib.TBMCChannelConnectFakeEvent; +import buttondevteam.lib.chat.Channel; +import buttondevteam.lib.player.TBMCPlayer; +import lombok.val; +import org.bukkit.Bukkit; +import sx.blah.discord.handle.obj.IMessage; +import sx.blah.discord.handle.obj.Permissions; +import sx.blah.discord.util.PermissionUtils; + +import java.util.Arrays; + +public class ChannelconCommand extends DiscordCommandBase { + @Override + public String getCommandName() { + return "channelcon"; + } + + @Override + public boolean run(IMessage message, String args) { + if (args.length() == 0) + return false; + if (!PermissionUtils.hasPermissions(message.getChannel(), message.getAuthor(), Permissions.MANAGE_CHANNEL)) { + message.reply("you need to have manage permissions for this channel!"); + return true; + } + if (MCChatListener.hasCustomChat(message.getChannel())) { + if (args.equalsIgnoreCase("remove")) { + if (MCChatListener.removeCustomChat(message.getChannel())) + message.reply("channel connection removed."); + else + message.reply("wait what, couldn't remove channel connection."); + return true; + } + message.reply("this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it."); + return true; + } + val chan = Channel.getChannels().stream().filter(ch -> ch.ID.equalsIgnoreCase(args) || (ch.IDs != null && Arrays.stream(ch.IDs).anyMatch(cid -> cid.equalsIgnoreCase(args)))).findAny(); + if (!chan.isPresent()) { //TODO: Red embed that disappears over time (kinda like the highlight messages in OW) + message.reply("MC channel with ID '" + args + "' not found! The ID is the command for it without the /."); + return true; + } + val dp = DiscordPlayer.getUser(message.getAuthor().getStringID(), DiscordPlayer.class); + val chp = dp.getAs(TBMCPlayer.class); + if (chp == null) { + message.reply("you need to connect your Minecraft account. On our server in #bot do /connect "); + return true; + } + DiscordConnectedPlayer dcp = new DiscordConnectedPlayer(message.getAuthor(), message.getChannel(), chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName()); + val ev = new TBMCChannelConnectFakeEvent(dcp, chan.get()); + //Using a fake player with no login/logout, should be fine for this event + String groupid = ev.getGroupID(ev.getSender()); //We're not trying to send in a specific group, we want to know which group the user belongs to (so not getGroupID()) + if (groupid == null) { + message.reply("sorry, that didn't work. You cannot use that Minecraft channel."); + return true; + } + if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) { + message.reply("sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm."); + return true; + } + MCChatListener.addCustomChat(message.getChannel(), groupid, ev.getChannel(), dp, message.getAuthor(), dcp); + message.reply("alright, connection made to group `" + groupid + "`!"); + return true; + } + + @Override + public String[] getHelpText() { + 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 #bot use /connect .", // + "Call this command from the channel you want to use. Usage: @ChromaBot channelcon ", // + "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: " // + }; + } +} 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 index afa4cfb..670b522 100755 --- a/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java +++ b/src/main/java/buttondevteam/discordplugin/commands/ConnectCommand.java @@ -1,64 +1,76 @@ -package buttondevteam.discordplugin.commands; - -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.lib.TBMCCoreAPI; -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 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; - } - try (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; - } - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while connecting a Discord account!", e); - channel.createMessage("An internal error occured!\n" + e).subscribe(); - } - WaitingToConnect.put(p.getName(), author.getId().asString()); - channel.createMessage( - "Alright! Now accept the connection in Minecraft from the account " + Minecraftname - + " before the next server restart. You can also adjust the Minecraft name you want to connect to with the same command.").subscribe(); - if (p.isOnline()) - ((Player) p).sendMessage("§bTo connect with the Discord account " + author.getUsername() + "#" - + author.getDiscriminator() + " do /discord accept"); - return true; - } - -} +package buttondevteam.discordplugin.commands; + +import buttondevteam.discordplugin.DiscordPlayer; +import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.lib.TBMCCoreAPI; +import buttondevteam.lib.player.TBMCPlayer; +import buttondevteam.lib.player.TBMCPlayerBase; +import com.google.common.collect.HashBiMap; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import sx.blah.discord.handle.obj.IMessage; + +public class ConnectCommand extends DiscordCommandBase { + + @Override + public String getCommandName() { + return "connect"; + } + + /** + * Key: Minecraft name
+ * Value: Discord ID + */ + public static HashBiMap WaitingToConnect = HashBiMap.create(); + + @Override + public boolean run(IMessage message, String args) { + if (args.length() == 0) + return true; + if (args.contains(" ")) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "Too many arguments.\nUsage: connect "); + return true; + } + if (WaitingToConnect.inverse().containsKey(message.getAuthor().getStringID())) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "Replacing " + WaitingToConnect.inverse().get(message.getAuthor().getStringID()) + " with " + args); + WaitingToConnect.inverse().remove(message.getAuthor().getStringID()); + } + @SuppressWarnings("deprecation") + OfflinePlayer p = Bukkit.getOfflinePlayer(args); + if (p == null) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), "The specified Minecraft player cannot be found"); + return true; + } + try (TBMCPlayer pl = TBMCPlayerBase.getPlayer(p.getUniqueId(), TBMCPlayer.class)) { + DiscordPlayer dp = pl.getAs(DiscordPlayer.class); + if (dp != null && message.getAuthor().getStringID().equals(dp.getDiscordID())) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), "You already have this account connected."); + return true; + } + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occured while connecting a Discord account!", e); + DiscordPlugin.sendMessageToChannel(message.getChannel(), "An internal error occured!\n" + e); + } + WaitingToConnect.put(p.getName(), message.getAuthor().getStringID()); + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "Alright! Now accept the connection in Minecraft from the account " + args + + " before the next server restart. You can also adjust the Minecraft name you want to connect to with the same command."); + if (p.isOnline()) + ((Player) p).sendMessage("§bTo connect with the Discord account " + message.getAuthor().getName() + "#" + + message.getAuthor().getDiscriminator() + " do /discord accept"); + return true; + } + + @Override + public String[] getHelpText() { + return new String[] { // + "---- Connect command ----", // + "This commands let's you connect your acoount with a Minecraft account. This'd allow using the Minecraft chat and other things.", // + "Usage: connect " // + }; + } + +} diff --git a/src/main/java/buttondevteam/discordplugin/commands/DebugCommand.java b/src/main/java/buttondevteam/discordplugin/commands/DebugCommand.java index 65e0663..1b6a605 100644 --- a/src/main/java/buttondevteam/discordplugin/commands/DebugCommand.java +++ b/src/main/java/buttondevteam/discordplugin/commands/DebugCommand.java @@ -1,30 +1,26 @@ 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; +import buttondevteam.discordplugin.listeners.CommandListener; +import sx.blah.discord.handle.obj.IMessage; -@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; - } +public class DebugCommand extends DiscordCommandBase { + @Override + public String getCommandName() { + return "debug"; + } + + @Override + public boolean run(IMessage message, String args) { + if (message.getAuthor().hasRole(DiscordPlugin.mainServer.getRoleByID(126030201472811008L))) + message.reply("Debug " + (CommandListener.debug() ? "enabled" : "disabled")); + else + message.reply("You need to be a moderator to use this command."); + return true; + } + + @Override + public String[] getHelpText() { + return new String[]{"Switches debug mode."}; + } } diff --git a/src/main/java/buttondevteam/discordplugin/commands/DiscordCommandBase.java b/src/main/java/buttondevteam/discordplugin/commands/DiscordCommandBase.java new file mode 100755 index 0000000..8fd3145 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/commands/DiscordCommandBase.java @@ -0,0 +1,57 @@ +package buttondevteam.discordplugin.commands; + +import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.lib.TBMCCoreAPI; +import sx.blah.discord.handle.obj.IMessage; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.stream.Collectors; + +import static buttondevteam.discordplugin.listeners.CommandListener.debug; + +public abstract class DiscordCommandBase { + public abstract String getCommandName(); + + public abstract boolean run(IMessage message, String args); + + public abstract String[] getHelpText(); + + static final HashMap commands = new HashMap(); + + public static void registerCommands() { + commands.put("connect", new ConnectCommand()); // TODO: API for adding commands? + commands.put("userinfo", new UserinfoCommand()); + commands.put("help", new HelpCommand()); + commands.put("role", new RoleCommand()); + commands.put("mcchat", new MCChatCommand()); + commands.put("channelcon", new ChannelconCommand()); + commands.put("debug", new DebugCommand()); + } + + public static void runCommand(String cmd, String args, IMessage message) { + debug("F"); //Not sure if needed + DiscordCommandBase command = commands.get(cmd); + if (command == null) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "Unknown command: " + cmd + " with args: " + args + "\nDo '" + + (message.getChannel().isPrivate() ? "" : message.getClient().getOurUser().mention() + " ") + + "help' for help"); + return; + } + debug("G"); + try { + if (!command.run(message, args)) + DiscordPlugin.sendMessageToChannel(message.getChannel(), Arrays.stream(command.getHelpText()).collect(Collectors.joining("\n"))); + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occured while executing command " + cmd + "!", e); + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "An internal error occured while executing this command. For more technical details see the server-issues channel on the dev Discord."); + } + debug("H"); + } + + protected String[] splitargs(String args) { + return args.split("\\s+"); + } +} diff --git a/src/main/java/buttondevteam/discordplugin/commands/HelpCommand.java b/src/main/java/buttondevteam/discordplugin/commands/HelpCommand.java index 546d4ee..29aff28 100755 --- a/src/main/java/buttondevteam/discordplugin/commands/HelpCommand.java +++ b/src/main/java/buttondevteam/discordplugin/commands/HelpCommand.java @@ -1,24 +1,39 @@ -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; - } -} +package buttondevteam.discordplugin.commands; + +import buttondevteam.discordplugin.DiscordPlugin; +import sx.blah.discord.handle.obj.IMessage; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public class HelpCommand extends DiscordCommandBase { + + @Override + public String getCommandName() { + return "help"; + } + + @Override + public boolean run(IMessage message, String args) { + DiscordCommandBase argdc; + if (args.length() == 0) + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "Available commands:\n" + DiscordCommandBase.commands.values().stream() + .map(dc -> dc.getCommandName()).collect(Collectors.joining("\n"))); + else + DiscordPlugin.sendMessageToChannel(message.getChannel(), + (argdc = DiscordCommandBase.commands.get(args)) == null ? "Command not found: " + args + : Arrays.stream(argdc.getHelpText()).collect(Collectors.joining("\n"))); + return true; + } + + @Override + public String[] getHelpText() { + return new String[] { // + "---- Help command ----", // + "Shows some info about a command or lists the available commands.", // + "Usage: help [command]"// + }; + } + +} 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/MCChatCommand.java b/src/main/java/buttondevteam/discordplugin/commands/MCChatCommand.java new file mode 100755 index 0000000..e2ca2ca --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/commands/MCChatCommand.java @@ -0,0 +1,44 @@ +package buttondevteam.discordplugin.commands; + +import buttondevteam.discordplugin.DiscordPlayer; +import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.discordplugin.listeners.MCChatListener; +import buttondevteam.lib.TBMCCoreAPI; +import sx.blah.discord.handle.obj.IMessage; + +public class MCChatCommand extends DiscordCommandBase { + + @Override + public String getCommandName() { + return "mcchat"; + } + + @Override + public boolean run(IMessage message, String args) { + if (!message.getChannel().isPrivate()) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "This command can only be issued in a direct message with the bot."); + return true; + } + try (final DiscordPlayer user = DiscordPlayer.getUser(message.getAuthor().getStringID(), DiscordPlayer.class)) { + boolean mcchat = !user.isMinecraftChatEnabled(); + MCChatListener.privateMCChat(message.getChannel(), mcchat, message.getAuthor(), user); + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "Minecraft chat " + (mcchat // + ? "enabled. Use '/mcchat' again to turn it off." // + : "disabled.")); + } catch (Exception e) { + TBMCCoreAPI.SendException("Error while setting mcchat for user" + message.getAuthor().getName(), e); + } + return true; + } + + @Override + public String[] getHelpText() { + return new String[] { // + "mcchat 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 a private channel." // + }; // TODO: Pin channel switching to indicate the current channel + } + +} diff --git a/src/main/java/buttondevteam/discordplugin/commands/RoleCommand.java b/src/main/java/buttondevteam/discordplugin/commands/RoleCommand.java new file mode 100755 index 0000000..dbe8988 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/commands/RoleCommand.java @@ -0,0 +1,93 @@ +package buttondevteam.discordplugin.commands; + +import buttondevteam.discordplugin.DPUtils; +import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.lib.TBMCCoreAPI; +import sx.blah.discord.handle.obj.IMessage; +import sx.blah.discord.handle.obj.IRole; + +import java.util.List; +import java.util.stream.Collectors; + +public class RoleCommand extends DiscordCommandBase { + + @Override + public String getCommandName() { + return "role"; + } + + @Override + public boolean run(IMessage message, String args) { + if (args.length() == 0) + return false; + String[] argsa = splitargs(args); + if (argsa[0].equalsIgnoreCase("add")) { + final IRole role = checkAndGetRole(message, argsa, "This command adds a game role to your account."); + if (role == null) + return true; + try { + DPUtils.perform(() -> message.getAuthor().addRole(role)); + DiscordPlugin.sendMessageToChannel(message.getChannel(), "Added game role."); + } catch (Exception e) { + TBMCCoreAPI.SendException("Error while adding role!", e); + DiscordPlugin.sendMessageToChannel(message.getChannel(), "An error occured while adding the role."); + } + } else if (argsa[0].equalsIgnoreCase("remove")) { + final IRole role = checkAndGetRole(message, argsa, "This command removes a game role from your account."); + if (role == null) + return true; + try { + DPUtils.perform(() -> message.getAuthor().removeRole(role)); + DiscordPlugin.sendMessageToChannel(message.getChannel(), "Removed game role."); + } catch (Exception e) { + TBMCCoreAPI.SendException("Error while removing role!", e); + DiscordPlugin.sendMessageToChannel(message.getChannel(), "An error occured while removing the role."); + } + } else if (argsa[0].equalsIgnoreCase("list")) { + listRoles(message); + } else return false; + return true; + } + + private void listRoles(IMessage message) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "List of game roles:\n" + DiscordPlugin.GameRoles.stream().sorted().collect(Collectors.joining("\n"))); + } + + private IRole checkAndGetRole(IMessage message, String[] argsa, String usage) { + if (argsa.length < 2) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), usage + "\nUsage: " + argsa[0] + " "); + return null; + } + String rolename = argsa[1]; + for (int i = 2; i < argsa.length; i++) + rolename += " " + argsa[i]; + if (!DiscordPlugin.GameRoles.contains(rolename)) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), "That game role cannot be found."); + listRoles(message); + return null; + } + final List roles = DiscordPlugin.mainServer.getRolesByName(rolename); + if (roles.size() == 0) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "The specified role cannot be found on Discord! Removing from the list."); + DiscordPlugin.GameRoles.remove(rolename); + return null; + } + if (roles.size() > 1) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "There are more roles with this name. Why are there more roles with this name?"); + return null; + } + return roles.get(0); + } + + @Override + public String[] getHelpText() { + return new String[]{ // + "Add or remove game roles from yourself.", // + "Usage: role add|remove or role list", // + }; + } + +} diff --git a/src/main/java/buttondevteam/discordplugin/commands/UserinfoCommand.java b/src/main/java/buttondevteam/discordplugin/commands/UserinfoCommand.java index 40f98cb..b0608ed 100755 --- a/src/main/java/buttondevteam/discordplugin/commands/UserinfoCommand.java +++ b/src/main/java/buttondevteam/discordplugin/commands/UserinfoCommand.java @@ -1,93 +1,98 @@ -package buttondevteam.discordplugin.commands; - -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -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 { - @SuppressWarnings("OptionalGetWithoutIsPresent") final User firstmention = message.getUserMentions() - .filter(m -> !m.getId().asString().equals(DiscordPlugin.dc.getSelfId().get().asString())).blockFirst(); - if (firstmention != null) - target = firstmention; - else if (user.contains("#")) { - String[] targettag = user.split("#"); - final List targets = getUsers(message, targettag[0]); - 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; - } - try (DiscordPlayer dp = ChromaGamerBase.getUser(target.getId().asString(), DiscordPlayer.class)) { - StringBuilder uinfo = new StringBuilder("User info for ").append(target.getUsername()).append(":\n"); - uinfo.append(dp.getInfo(InfoTarget.Discord)); - channel.createMessage(uinfo.toString()).subscribe(); - } catch (Exception e) { - channel.createMessage("An error occured while getting the user!").subscribe(); - TBMCCoreAPI.SendException("Error while getting info about " + target.getUsername() + "!", e); - } - return true; - } - - private List getUsers(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; - } - -} +package buttondevteam.discordplugin.commands; + +import buttondevteam.discordplugin.DiscordPlayer; +import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.lib.TBMCCoreAPI; +import buttondevteam.lib.player.ChromaGamerBase; +import buttondevteam.lib.player.ChromaGamerBase.InfoTarget; +import sx.blah.discord.handle.obj.IMessage; +import sx.blah.discord.handle.obj.IUser; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class UserinfoCommand extends DiscordCommandBase { + + @Override + public String getCommandName() { + return "userinfo"; + } + + @Override + public boolean run(IMessage message, String args) { + IUser target = null; + if (args.length() == 0) + target = message.getAuthor(); + else { + final Optional firstmention = message.getMentions().stream() + .filter(m -> !m.getStringID().equals(DiscordPlugin.dc.getOurUser().getStringID())).findFirst(); + if (firstmention.isPresent()) + target = firstmention.get(); + else if (args.contains("#")) { + String[] targettag = args.split("#"); + final List targets = getUsers(message, targettag[0]); + if (targets.size() == 0) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "The user cannot be found (by name): " + args); + return true; + } + for (IUser ptarget : targets) { + if (ptarget.getDiscriminator().equalsIgnoreCase(targettag[1])) { + target = ptarget; + break; + } + } + if (target == null) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "The user cannot be found (by discriminator): " + args + "(Found " + targets.size() + + " users with the name.)"); + return true; + } + } else { + final List targets = getUsers(message, args); + if (targets.size() == 0) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "The user cannot be found on Discord: " + args); + return true; + } + if (targets.size() > 1) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), + "Multiple users found with that (nick)name. Please specify the whole tag, like ChromaBot#6338 or use a ping."); + return true; + } + target = targets.get(0); + } + } + try (DiscordPlayer dp = ChromaGamerBase.getUser(target.getStringID(), DiscordPlayer.class)) { + StringBuilder uinfo = new StringBuilder("User info for ").append(target.getName()).append(":\n"); + uinfo.append(dp.getInfo(InfoTarget.Discord)); + DiscordPlugin.sendMessageToChannel(message.getChannel(), uinfo.toString()); + } catch (Exception e) { + DiscordPlugin.sendMessageToChannel(message.getChannel(), "An error occured while getting the user!"); + TBMCCoreAPI.SendException("Error while getting info about " + target.getName() + "!", e); + } + return true; + } + + private List getUsers(IMessage message, String args) { + final List targets; + if (message.getChannel().isPrivate()) + targets = DiscordPlugin.dc.getUsers().stream().filter(u -> u.getName().equalsIgnoreCase(args)) + .collect(Collectors.toList()); + else + targets = message.getGuild().getUsersByName(args, true); + return targets; + } + + @Override + public String[] getHelpText() { + return new String[] { // + "---- 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.", // + "Usage: userinfo [username/nickname[#tag]/ping]\nExamples:\nuserinfo ChromaBot\nuserinfo ChromaBot#6338\nuserinfo @ChromaBot#6338" // + }; + } + +} 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/ExceptionListenerModule.java b/src/main/java/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.java deleted file mode 100755 index 6263e85..0000000 --- a/src/main/java/buttondevteam/discordplugin/exceptions/ExceptionListenerModule.java +++ /dev/null @@ -1,116 +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.GuildChannel; -import discord4j.core.object.entity.MessageChannel; -import discord4j.core.object.entity.Role; -import org.apache.commons.lang.exception.ExceptionUtils; -import org.bukkit.Bukkit; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import 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 List lastthrown = new ArrayList<>(); - private List lastsourcemsg = new ArrayList<>(); - - @EventHandler - public void onException(TBMCExceptionEvent e) { - if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(getClass())) - return; - if (lastthrown.stream() - .anyMatch(ex -> Arrays.equals(e.getException().getStackTrace(), ex.getStackTrace()) - && (e.getException().getMessage() == null ? ex.getMessage() == null - : e.getException().getMessage().equals(ex.getMessage()))) // e.Exception.Message==ex.Message - && lastsourcemsg.contains(e.getSourceMessage())) - return; - SendException(e.getException(), e.getSourceMessage()); - if (lastthrown.size() >= 10) - lastthrown.remove(0); - if (lastsourcemsg.size() >= 10) - lastsourcemsg.remove(0); - lastthrown.add(e.getException()); - lastsourcemsg.add(e.getSourceMessage()); - e.setHandled(); - } - - 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 ReadOnlyConfigData> channel() { - return 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 189a026..0000000 --- a/src/main/java/buttondevteam/discordplugin/fun/FunModule.java +++ /dev/null @@ -1,163 +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.*; -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 ConfigData serverReady() { - return getConfig().getData("serverReady", () -> new String[]{"when will the server be open", - "when will the server be ready", "when will the server be done", "when will the server be complete", - "when will the server be finished", "when's the server ready", "when's the server open", - "vhen vill ze server be open?"}); - } - - /** - * Answers for a recognized question. Selected randomly. - */ - private ConfigData> serverReadyAnswers() { - return getConfig().getData("serverReadyAnswers", () -> Lists.newArrayList(serverReadyStrings)); - } - - 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().orElse("").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 ReadOnlyConfigData> fullHouseChannel() { - return 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/AutoUpdaterListener.java b/src/main/java/buttondevteam/discordplugin/listeners/AutoUpdaterListener.java new file mode 100755 index 0000000..a6d0150 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/listeners/AutoUpdaterListener.java @@ -0,0 +1,24 @@ +package buttondevteam.discordplugin.listeners; + +import buttondevteam.discordplugin.DPUtils; +import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.lib.PluginUpdater; +import buttondevteam.lib.TBMCCoreAPI; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +public class AutoUpdaterListener implements Listener { + @EventHandler + public void handle(PluginUpdater.UpdatedEvent event) { + if (DiscordPlugin.SafeMode) + return; + try { + DPUtils.performNoWait(() -> DiscordPlugin.officechannel.getMessageHistory(10).stream() + .filter(m -> m.getWebhookLongID() == 239123781401051138L && m.getEmbeds().get(0).getTitle() + .contains(event.getData().get("repository").getAsJsonObject().get("name").getAsString())) + .findFirst().get().addReaction(DiscordPlugin.DELIVERED_REACTION)); + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occured while reacting to plugin update!", e); + } + } +} diff --git a/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java b/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java old mode 100644 new mode 100755 index 1892e99..76ae457 --- a/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java +++ b/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java @@ -1,99 +1,227 @@ -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.core.object.entity.Message; -import discord4j.core.object.entity.MessageChannel; -import discord4j.core.object.entity.PrivateChannel; -import discord4j.core.object.entity.Role; -import lombok.val; -import reactor.core.publisher.Mono; - -import java.util.concurrent.atomic.AtomicBoolean; - -public class CommandListener { - /** - * 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, MessageChannel commandChannel, boolean mentionedonly) { - Timings timings = CommonListeners.timings; - Mono ret = Mono.just(true); - if (!message.getContent().isPresent()) - return ret; //Pin messages and such, let the mcchat listener deal with it - val content = message.getContent().get(); - timings.printElapsed("A"); - return message.getChannel().flatMap(channel -> { - Mono tmp = ret; - if (!mentionedonly) { //mentionedonly conditions are in CommonListeners - timings.printElapsed("B"); - if (!(channel instanceof PrivateChannel) - && !(content.charAt(0) == DiscordPlugin.getPrefix() - && channel.getId().asLong() == commandChannel.getId().asLong())) // - return ret; - timings.printElapsed("C"); - tmp = ret.then(channel.type()).thenReturn(true); // Fun (this true is ignored - x) - } - final StringBuilder cmdwithargs = new StringBuilder(content); - val gotmention = new AtomicBoolean(); - timings.printElapsed("Before self"); - return tmp.flatMapMany(x -> - DiscordPlugin.dc.getSelf().flatMap(self -> self.asMember(DiscordPlugin.mainServer.getId())) - .flatMapMany(self -> { - timings.printElapsed("D"); - gotmention.set(checkanddeletemention(cmdwithargs, self.getMention(), message)); - gotmention.set(checkanddeletemention(cmdwithargs, self.getNicknameMention(), message) || gotmention.get()); - val mentions = message.getRoleMentions(); - return self.getRoles().filterWhen(r -> mentions.any(rr -> rr.getName().equals(r.getName()))) - .map(Role::getMention); - }).map(mentionRole -> { - timings.printElapsed("E"); - gotmention.set(checkanddeletemention(cmdwithargs, mentionRole, message) || gotmention.get()); // Delete all mentions - return !mentionedonly || gotmention.get(); //Stops here if false - }).switchIfEmpty(Mono.fromSupplier(() -> !mentionedonly || gotmention.get()))) - .filter(b -> b).last(false).filter(b -> b).doOnNext(b -> channel.type().subscribe()).flatMap(b -> { - String cmdwithargsString = cmdwithargs.toString(); - try { - timings.printElapsed("F"); - if (!DiscordPlugin.plugin.getManager().handleCommand(new Command2DCSender(message), cmdwithargsString)) - return DPUtils.reply(message, channel, "unknown command. Do " + DiscordPlugin.getPrefix() + "help for help.\n" + cmdwithargsString) - .map(m -> false); - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e); - } - return Mono.just(false); //If the command succeeded or there was an error, return false - }).defaultIfEmpty(true); - }); - } - - private static boolean checkanddeletemention(StringBuilder cmdwithargs, String mention, Message message) { - final char prefix = DiscordPlugin.getPrefix(); - if (message.getContent().orElse("").startsWith(mention)) // TODO: Resolve mentions: Compound arguments, either a mention or text - if (cmdwithargs.length() > mention.length() + 1) { - int i = cmdwithargs.indexOf(" ", mention.length()); - if (i == -1) - 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, cmdwithargs.length(), prefix + "help"); - else if (cmdwithargs.charAt(0) != prefix) - cmdwithargs.insert(0, prefix); - return false; //Don't treat / as mention, mentions can be used in public mcchat - } - return true; - } -} +package buttondevteam.discordplugin.listeners; + +import buttondevteam.discordplugin.DiscordPlayer; +import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.discordplugin.commands.DiscordCommandBase; +import buttondevteam.lib.TBMCCoreAPI; +import lombok.val; +import org.bukkit.Bukkit; +import sx.blah.discord.api.events.IListener; +import sx.blah.discord.handle.impl.events.guild.channel.message.MentionEvent; +import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent; +import sx.blah.discord.handle.impl.events.guild.role.RoleCreateEvent; +import sx.blah.discord.handle.impl.events.guild.role.RoleDeleteEvent; +import sx.blah.discord.handle.impl.events.guild.role.RoleUpdateEvent; +import sx.blah.discord.handle.impl.events.user.PresenceUpdateEvent; +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IMessage; +import sx.blah.discord.handle.obj.StatusType; +import sx.blah.discord.util.EmbedBuilder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +public class CommandListener { + + 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 + }; + + private static final String[] serverReadyQuestions = new String[]{"when will the server be open", + "when will the server be ready", "when will the server be done", "when will the server be complete", + "when will the server be finished", "when's the server ready", "when's the server open", + "Vhen vill ze server be open?"}; + + private static final Random serverReadyRandom = new Random(); + private static final ArrayList usableServerReadyStrings = new ArrayList(serverReadyStrings.length) { + private static final long serialVersionUID = 2213771460909848770L; + + { + createUsableServerReadyStrings(this); + } + }; + + private static void createUsableServerReadyStrings(ArrayList list) { + for (short i = 0; i < serverReadyStrings.length; i++) + list.add(i); + } + + private static long lasttime = 0; + + public static IListener[] getListeners() { + return new IListener[]{new IListener() { + @Override + public void handle(MentionEvent event) { + if (DiscordPlugin.SafeMode) + return; + if (event.getMessage().getAuthor().isBot()) + return; + final IChannel channel = event.getMessage().getChannel(); + if (!channel.getStringID().equals(DiscordPlugin.botchannel.getStringID()) + && (!event.getMessage().getContent().contains("channelcon") || MCChatListener.hasCustomChat(channel))) //Allow channelcon in other servers but avoid double handling when it's enabled + return; + if (channel.getStringID().equals(DiscordPlugin.chatchannel.getStringID())) + return; // The chat code already handles this - Right now while testing botchannel is the same as chatchannel + event.getMessage().getChannel().setTypingStatus(true); // Fun + runCommand(event.getMessage(), true); + } + }, new IListener() { + @Override + public void handle(MessageReceivedEvent event) { + if (DiscordPlugin.SafeMode) + return; + final String msglowercase = event.getMessage().getContent().toLowerCase(); + if (!TBMCCoreAPI.IsTestServer() + && Arrays.stream(serverReadyQuestions).anyMatch(s -> msglowercase.contains(s))) { + int next; + if (usableServerReadyStrings.size() == 0) + createUsableServerReadyStrings(usableServerReadyStrings); + next = usableServerReadyStrings.remove(serverReadyRandom.nextInt(usableServerReadyStrings.size())); + DiscordPlugin.sendMessageToChannel(event.getMessage().getChannel(), serverReadyStrings[next]); + return; + } + if (!event.getMessage().getChannel().isPrivate() + && !(event.getMessage().getContent().startsWith("/") + && event.getChannel().getStringID().equals(DiscordPlugin.botchannel.getStringID()))) // + return; + if (DiscordPlayer.getUser(event.getAuthor().getStringID(), DiscordPlayer.class) + .isMinecraftChatEnabled()) + if (!event.getMessage().getContent().equalsIgnoreCase("mcchat")) + return; + if (event.getMessage().getAuthor().isBot()) + return; + runCommand(event.getMessage(), false); + } + }, new IListener() { + @Override + public void handle(PresenceUpdateEvent event) { + if (DiscordPlugin.SafeMode) + return; + val devrole = DiscordPlugin.devServer.getRolesByName("Developer").get(0); + if (event.getOldPresence().getStatus().equals(StatusType.OFFLINE) + && !event.getNewPresence().getStatus().equals(StatusType.OFFLINE) + && event.getUser().getRolesForGuild(DiscordPlugin.devServer).stream() + .anyMatch(r -> r.getLongID() == devrole.getLongID()) + && DiscordPlugin.devServer.getUsersByRole(devrole).stream() + .noneMatch(u -> u.getPresence().getStatus().equals(StatusType.OFFLINE)) + && lasttime + 10 < TimeUnit.NANOSECONDS.toHours(System.nanoTime()) + && Calendar.getInstance().get(Calendar.DAY_OF_MONTH) % 5 == 0) { + DiscordPlugin.sendMessageToChannel(DiscordPlugin.devofficechannel, "Full house!", + new EmbedBuilder() + .withImage( + "https://cdn.discordapp.com/attachments/249295547263877121/249687682618359808/poker-hand-full-house-aces-kings-playing-cards-15553791.png") + .build()); + lasttime = TimeUnit.NANOSECONDS.toHours(System.nanoTime()); + } + } + }, (IListener) event -> { + Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> { + if (event.getRole().isDeleted() || !DiscordPlugin.plugin.isGameRole(event.getRole())) + return; //Deleted or not a game role + DiscordPlugin.GameRoles.add(event.getRole().getName()); + DiscordPlugin.sendMessageToChannel(DiscordPlugin.modlogchannel, "Added " + event.getRole().getName() + " as game role. If you don't want this, change the role's color from the default."); + }, 100); + }, (IListener) event -> { + if (DiscordPlugin.GameRoles.remove(event.getRole().getName())) + DiscordPlugin.sendMessageToChannel(DiscordPlugin.modlogchannel, "Removed " + event.getRole().getName() + " as a game role."); + }, (IListener) event -> { //Role update event + if (!DiscordPlugin.plugin.isGameRole(event.getNewRole())) { + if (DiscordPlugin.GameRoles.remove(event.getOldRole().getName())) + DiscordPlugin.sendMessageToChannel(DiscordPlugin.modlogchannel, "Removed " + event.getOldRole().getName() + " as a game role because it's color changed."); + } else { + boolean removed = DiscordPlugin.GameRoles.remove(event.getOldRole().getName()); //Regardless of whether it was a game role + DiscordPlugin.GameRoles.add(event.getNewRole().getName()); //Add it because it has no color + if (removed) + DiscordPlugin.sendMessageToChannel(DiscordPlugin.modlogchannel, "Changed game role from " + event.getOldRole().getName() + " to " + event.getNewRole().getName() + "."); + else + DiscordPlugin.sendMessageToChannel(DiscordPlugin.modlogchannel, "Added " + event.getNewRole().getName() + " as game role because it has the default color."); + } + }}; + } + + /** + * Runs a ChromaBot command. + * + * @param message The Discord message + * @param mentionedonly Only run the command if ChromaBot is mentioned at the start of the message + * @return Whether it ran the command (always true if mentionedonly is false) + */ + public static boolean runCommand(IMessage message, boolean mentionedonly) { + debug("A"); + if (DiscordPlugin.SafeMode) + return true; + debug("B"); + final StringBuilder cmdwithargs = new StringBuilder(message.getContent()); + final String mention = DiscordPlugin.dc.getOurUser().mention(false); + final String mentionNick = DiscordPlugin.dc.getOurUser().mention(true); + boolean gotmention = checkanddeletemention(cmdwithargs, mention, message); + gotmention = checkanddeletemention(cmdwithargs, mentionNick, message) || gotmention; + for (String mentionRole : (Iterable) message.getRoleMentions().stream().filter(r -> DiscordPlugin.dc.getOurUser().hasRole(r)).map(r -> r.mention())::iterator) + gotmention = checkanddeletemention(cmdwithargs, mentionRole, message) || gotmention; // Delete all mentions + debug("C"); + if (mentionedonly && !gotmention) { + message.getChannel().setTypingStatus(false); + return false; + } + debug("D"); + message.getChannel().setTypingStatus(true); + String cmdwithargsString = cmdwithargs.toString().trim(); //Remove spaces between mention and command + int index = cmdwithargsString.indexOf(" "); + String cmd; + String args; + if (index == -1) { + cmd = cmdwithargsString; + args = ""; + } else { + cmd = cmdwithargsString.substring(0, index); + args = cmdwithargsString.substring(index + 1).trim(); //In case there are multiple spaces + } + debug("E"); + DiscordCommandBase.runCommand(cmd.toLowerCase(), args, message); + message.getChannel().setTypingStatus(false); + return true; + } + + private static boolean debug = false; + + public static void debug(String debug) { + if (CommandListener.debug) //Debug + System.out.println(debug); + } + + public static boolean debug() { + return debug = !debug; + } + + private static boolean checkanddeletemention(StringBuilder cmdwithargs, String mention, IMessage message) { + if (message.getContent().startsWith(mention)) // TODO: Resolve mentions: Compound arguments, either a mention or text + if (cmdwithargs.length() > mention.length() + 1) + cmdwithargs = cmdwithargs.delete(0, + cmdwithargs.charAt(mention.length()) == ' ' ? mention.length() + 1 : mention.length()); + else + cmdwithargs.replace(0, cmdwithargs.length(), "help"); + else { + if (cmdwithargs.length() > 0 && cmdwithargs.charAt(0) == '/') + cmdwithargs.deleteCharAt(0); //Don't treat / as mention, mentions can be used in public mcchat + return false; + } + if (cmdwithargs.length() == 0) + cmdwithargs.replace(0, cmdwithargs.length(), "help"); + 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 510e71a..0000000 --- a/src/main/java/buttondevteam/discordplugin/listeners/CommonListeners.java +++ /dev/null @@ -1,89 +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.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(); - val commandCh = DPUtils.getMessageChannel(DiscordPlugin.plugin.commandChannel()); - return commandCh.filterWhen(ch -> event.getMessage().getChannel().map(mch -> - (commandChannel != null && mch.getId().asLong() == commandChannel.asLong()) //If mentioned, that's higher than chat - || mch instanceof PrivateChannel - || event.getMessage().getContent().orElse("").contains("channelcon")) //Only 'channelcon' is allowed in other channels - .flatMap(shouldRun -> { //Only continue if this doesn't handle the event - if (!shouldRun) - return Mono.just(true); //The condition is only for the first command execution, not mcchat - timings.printElapsed("Run command 1"); - return CommandListener.runCommand(event.getMessage(), ch, true); //#bot is handled here - })).filterWhen(ch -> { - timings.printElapsed("mcchat"); - val mcchat = Component.getComponents().get(MinecraftChatModule.class); - if (mcchat != null && mcchat.isEnabled()) //ComponentManager.isEnabled() searches the component again - return ((MinecraftChatModule) mcchat).getListener().handleDiscord(event); //Also runs Discord commands in chat channels - return Mono.empty(); //Wasn't handled, continue - }).filterWhen(ch -> { - timings.printElapsed("Run command 2"); - return CommandListener.runCommand(event.getMessage(), ch, false); - }); - }).onErrorContinue((err, obj) -> TBMCCoreAPI.SendException("An error occured while handling a message!", err)) - .subscribe(); - dispatcher.on(PresenceUpdateEvent.class).subscribe(event -> { - if (DiscordPlugin.SafeMode) - return; - FunModule.handleFullHouse(event); - }); - dispatcher.on(RoleCreateEvent.class).subscribe(GameRoleModule::handleRoleEvent); - dispatcher.on(RoleDeleteEvent.class).subscribe(GameRoleModule::handleRoleEvent); - dispatcher.on(RoleUpdateEvent.class).subscribe(GameRoleModule::handleRoleEvent); - - } - - private static boolean debug = false; - - 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/exceptions/DebugMessageListener.java b/src/main/java/buttondevteam/discordplugin/listeners/DebugMessageListener.java similarity index 56% rename from src/main/java/buttondevteam/discordplugin/exceptions/DebugMessageListener.java rename to src/main/java/buttondevteam/discordplugin/listeners/DebugMessageListener.java index 0f05934..1e163c4 100755 --- a/src/main/java/buttondevteam/discordplugin/exceptions/DebugMessageListener.java +++ b/src/main/java/buttondevteam/discordplugin/listeners/DebugMessageListener.java @@ -1,36 +1,31 @@ -package buttondevteam.discordplugin.exceptions; - -import buttondevteam.core.ComponentManager; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.TBMCDebugMessageEvent; -import discord4j.core.object.entity.MessageChannel; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import reactor.core.publisher.Mono; - -public class DebugMessageListener implements Listener { - @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(); - } - } - -} +package buttondevteam.discordplugin.listeners; + +import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.lib.TBMCDebugMessageEvent; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +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) + return; + try { + StringBuilder sb = new StringBuilder(); + sb.append("```").append("\n"); + if (message.length() > 2000) + message = message.substring(0, 2000); + sb.append(message).append("\n"); + sb.append("```"); + DiscordPlugin.sendMessageToChannel(DiscordPlugin.botroomchannel, sb.toString()); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + +} diff --git a/src/main/java/buttondevteam/discordplugin/listeners/DiscordListener.java b/src/main/java/buttondevteam/discordplugin/listeners/DiscordListener.java deleted file mode 100644 index 292a1a1..0000000 --- a/src/main/java/buttondevteam/discordplugin/listeners/DiscordListener.java +++ /dev/null @@ -1,4 +0,0 @@ -package buttondevteam.discordplugin.listeners; - -public interface DiscordListener { -} diff --git a/src/main/java/buttondevteam/discordplugin/listeners/ExceptionListener.java b/src/main/java/buttondevteam/discordplugin/listeners/ExceptionListener.java new file mode 100755 index 0000000..549bb97 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/listeners/ExceptionListener.java @@ -0,0 +1,62 @@ +package buttondevteam.discordplugin.listeners; + +import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.lib.TBMCCoreAPI; +import buttondevteam.lib.TBMCExceptionEvent; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import sx.blah.discord.handle.obj.IRole; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class ExceptionListener implements Listener { + private List lastthrown = new ArrayList<>(); + private List lastsourcemsg = new ArrayList<>(); + + @EventHandler + public void onException(TBMCExceptionEvent e) { + if (DiscordPlugin.SafeMode) + 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 IRole coderRole; + + private static void SendException(Throwable e, String sourcemessage) { + try { + if (coderRole == null) + coderRole = DiscordPlugin.devServer.getRolesByName("Coder").get(0); + StringBuilder sb = TBMCCoreAPI.IsTestServer() ? new StringBuilder() + : new StringBuilder(coderRole.mention()).append("\n"); + sb.append(sourcemessage).append("\n"); + sb.append("```").append("\n"); + String stackTrace = Arrays.stream(ExceptionUtils.getStackTrace(e).split("\\n")) + .filter(s -> !s.contains("\tat ") || s.contains("\tat buttondevteam.")) + .collect(Collectors.joining("\n")); + if (stackTrace.length() > 1800) + stackTrace = stackTrace.substring(0, 1800); + sb.append(stackTrace).append("\n"); + sb.append("```"); + DiscordPlugin.sendMessageToChannel(DiscordPlugin.botroomchannel, sb.toString()); + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} diff --git a/src/main/java/buttondevteam/discordplugin/listeners/MCChatListener.java b/src/main/java/buttondevteam/discordplugin/listeners/MCChatListener.java new file mode 100755 index 0000000..d4a3ce9 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/listeners/MCChatListener.java @@ -0,0 +1,572 @@ +package buttondevteam.discordplugin.listeners; + +import buttondevteam.discordplugin.*; +import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; +import buttondevteam.lib.TBMCChatEvent; +import buttondevteam.lib.TBMCChatPreprocessEvent; +import buttondevteam.lib.TBMCCoreAPI; +import buttondevteam.lib.TBMCSystemChatEvent; +import buttondevteam.lib.chat.Channel; +import buttondevteam.lib.chat.ChatRoom; +import buttondevteam.lib.chat.TBMCChatAPI; +import buttondevteam.lib.player.TBMCPlayer; +import com.vdurmont.emoji.EmojiParser; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +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.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitTask; +import sx.blah.discord.api.events.IListener; +import sx.blah.discord.api.internal.json.objects.EmbedObject; +import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent; +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IMessage; +import sx.blah.discord.handle.obj.IPrivateChannel; +import sx.blah.discord.handle.obj.IUser; +import sx.blah.discord.util.DiscordException; +import sx.blah.discord.util.EmbedBuilder; +import sx.blah.discord.util.MissingPermissionsException; + +import java.awt.*; +import java.time.Instant; +import java.util.*; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class MCChatListener implements Listener, IListener { + private BukkitTask sendtask; + private LinkedBlockingQueue> sendevents = new LinkedBlockingQueue<>(); + private Runnable sendrunnable; + private static Thread sendthread; + + @EventHandler // Minecraft + public void onMCChat(TBMCChatEvent ev) { + if (ev.isCancelled()) + return; + sendevents.add(new AbstractMap.SimpleEntry<>(ev, Instant.now())); + if (sendtask != null) + return; + sendrunnable = () -> { + sendthread = Thread.currentThread(); + processMCToDiscord(); + if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down + sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); + }; + sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); + } + + private void processMCToDiscord() { + try { + TBMCChatEvent e; + Instant time; + try { + val se = sendevents.take(); // Wait until an element is available + e = se.getKey(); + time = se.getValue(); + } catch (InterruptedException ex) { + sendtask.cancel(); + sendtask = null; + return; + } + final String authorPlayer = "[" + DPUtils.sanitizeString(e.getChannel().DisplayName) + "] " // + + (e.getSender() instanceof DiscordSenderBase ? "[D]" : "") // + + (DPUtils.sanitizeString(e.getSender() instanceof Player // + ? ((Player) e.getSender()).getDisplayName() // + : e.getSender().getName())); + final EmbedBuilder embed = new EmbedBuilder().withAuthorName(authorPlayer) + .withDescription(e.getMessage()).withColor(new Color(e.getChannel().color.getRed(), + e.getChannel().color.getGreen(), e.getChannel().color.getBlue())); + // embed.appendField("Channel", ((e.getSender() instanceof DiscordSenderBase ? "d|" : "") + // + DiscordPlugin.sanitizeString(e.getChannel().DisplayName)), false); + if (e.getSender() instanceof Player) + DPUtils.embedWithHead( + embed.withAuthorUrl("https://tbmcplugins.github.io/profile.html?type=minecraft&id=" + + ((Player) e.getSender()).getUniqueId()), + e.getSender().getName()); + else if (e.getSender() instanceof DiscordSenderBase) + embed.withAuthorIcon(((DiscordSenderBase) e.getSender()).getUser().getAvatarURL()) + .withAuthorUrl("https://tbmcplugins.github.io/profile.html?type=discord&id=" + + ((DiscordSenderBase) e.getSender()).getUser().getStringID()); // TODO: Constant/method to get URLs like this + // embed.withFooterText(e.getChannel().DisplayName); + embed.withTimestamp(time); + final long nanoTime = System.nanoTime(); + Consumer doit = lastmsgdata -> { + final EmbedObject embedObject = embed.build(); + if (lastmsgdata.message == null || lastmsgdata.message.isDeleted() + || !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().getName()) + || lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120 + || !lastmsgdata.mcchannel.ID.equals(e.getChannel().ID)) { + lastmsgdata.message = DiscordPlugin.sendMessageToChannelWait(lastmsgdata.channel, "", + embedObject); // TODO Use ChromaBot API + lastmsgdata.time = nanoTime; + lastmsgdata.mcchannel = e.getChannel(); + lastmsgdata.content = embedObject.description; + } else + try { + lastmsgdata.content = embedObject.description = lastmsgdata.content + "\n" + + embedObject.description;// The message object doesn't get updated + final LastMsgData _lastmsgdata = lastmsgdata; + DPUtils.perform(() -> _lastmsgdata.message.edit("", embedObject)); + } catch (MissingPermissionsException | DiscordException e1) { + TBMCCoreAPI.SendException("An error occurred while editing chat message!", e1); + } + }; + // Checks if the given channel is different than where the message was sent from + // Or if it was from MC + Predicate isdifferentchannel = ch -> !(e.getSender() instanceof DiscordSenderBase) + || ((DiscordSenderBase) e.getSender()).getChannel().getLongID() != ch.getLongID(); + + if ((e.getChannel() == Channel.GlobalChat || e.getChannel().ID.equals("rp")) + && (e.isFromcmd() || isdifferentchannel.test(DiscordPlugin.chatchannel))) + doit.accept(lastmsgdata == null + ? lastmsgdata = new LastMsgData(DiscordPlugin.chatchannel, null, null) + : lastmsgdata); + + for (LastMsgData data : lastmsgPerUser) { + if (data.dp.isMinecraftChatEnabled() && (e.isFromcmd() || isdifferentchannel.test(data.channel)) + && e.shouldSendTo(getSender(data.channel, data.user))) + doit.accept(data); + } + + val iterator = lastmsgCustom.iterator(); + while (iterator.hasNext()) { //TODO: Add cmd to fix mcchat + val lmd = iterator.next(); + if ((e.isFromcmd() || isdifferentchannel.test(lmd.channel)) //Test if msg is from Discord + && e.getChannel().ID.equals(lmd.mcchannel.ID)) //If it's from a command, the command msg has been deleted, so we need to send it + if (e.shouldSendTo(lmd.dcp) && e.getGroupID().equals(lmd.groupID)) //Check original user's permissions + doit.accept(lmd); + else { + iterator.remove(); //If the user no longer has permission, remove the connection + DiscordPlugin.sendMessageToChannel(lmd.channel, "The user no longer has permission to view the channel, connection removed."); + } + } + } catch (Exception ex) { + TBMCCoreAPI.SendException("Error while sending message to Discord!", ex); + } + } + + @RequiredArgsConstructor + public static class LastMsgData { + public IMessage message; + public long time; + public String content; + public final IChannel channel; + public Channel mcchannel; + public final IUser user; + public final DiscordPlayer dp; + } + + public static class CustomLMD extends LastMsgData { + public final String groupID; + public final Channel mcchannel; + public final DiscordConnectedPlayer dcp; + + public CustomLMD(@NonNull IChannel channel, @NonNull IUser user, @NonNull DiscordPlayer dp, + @NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp) { + super(channel, user, dp); + groupID = groupid; + this.mcchannel = mcchannel; + this.dcp = dcp; + } + } + + @EventHandler + public void onChatPreprocess(TBMCChatPreprocessEvent event) { + int start = -1; + while ((start = event.getMessage().indexOf('@', start + 1)) != -1) { + int mid = event.getMessage().indexOf('#', start + 1); + if (mid == -1) + return; + int end_ = event.getMessage().indexOf(' ', mid + 1); + if (end_ == -1) + end_ = event.getMessage().length(); + final int end = end_; + final int startF = start; + DiscordPlugin.dc.getUsersByName(event.getMessage().substring(start + 1, mid)).stream() + .filter(u -> u.getDiscriminator().equals(event.getMessage().substring(mid + 1, end))).findAny() + .ifPresent(user -> event.setMessage(event.getMessage().substring(0, startF) + "@" + user.getName() + + (event.getMessage().length() > end ? event.getMessage().substring(end) : ""))); // TODO: Add formatting + start = end; // Skip any @s inside the mention + } + } + + private static final String[] UnconnectedCmds = new String[]{"list", "u", "shrug", "tableflip", "unflip", "mwiki", + "yeehaw", "lenny", "rp", "plugins"}; + + private static LastMsgData lastmsgdata; + private static short lastlist = 0; + private static short lastlistp = 0; + /** + * Used for messages in PMs (mcchat). + */ + private static ArrayList lastmsgPerUser = new ArrayList(); + /** + * Used for town or nation chats or anything else + */ + private static ArrayList lastmsgCustom = new ArrayList<>(); + + public static boolean privateMCChat(IChannel channel, boolean start, IUser user, DiscordPlayer dp) { + TBMCPlayer mcp = dp.getAs(TBMCPlayer.class); + if (mcp != null) { // If the accounts aren't connected, can't make a connected sender + val p = Bukkit.getPlayer(mcp.getUUID()); + val op = Bukkit.getOfflinePlayer(mcp.getUUID()); + if (start) { + val sender = new DiscordConnectedPlayer(user, channel, mcp.getUUID(), op.getName()); + ConnectedSenders.put(user.getStringID(), sender); + if (p == null)// Player is offline - If the player is online, that takes precedence + MCListener.callEventExcludingSome(new PlayerJoinEvent(sender, "")); + } else { + val sender = ConnectedSenders.remove(user.getStringID()); + if (p == null)// Player is offline - If the player is online, that takes precedence + MCListener.callEventExcludingSome(new PlayerQuitEvent(sender, "")); + } + } + return start // + ? lastmsgPerUser.add(new LastMsgData(channel, user, dp)) // Doesn't support group DMs + : lastmsgPerUser.removeIf(lmd -> lmd.channel.getLongID() == channel.getLongID()); + } + + // ......................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 + + public static boolean isMinecraftChatEnabled(DiscordPlayer dp) { + return lastmsgPerUser.stream().anyMatch( + lmd -> ((IPrivateChannel) lmd.channel).getRecipient().getStringID().equals(dp.getDiscordID())); + } + + public static boolean isMinecraftChatEnabled(String did) { // Don't load the player data just for this + return lastmsgPerUser.stream() + .anyMatch(lmd -> ((IPrivateChannel) lmd.channel).getRecipient().getStringID().equals(did)); + } + + public static void addCustomChat(IChannel channel, String groupid, Channel mcchannel, DiscordPlayer dp, IUser user, DiscordConnectedPlayer dcp) { + val lmd = new CustomLMD(channel, user, dp, groupid, mcchannel, dcp); + lastmsgCustom.add(lmd); + } + + public static boolean hasCustomChat(IChannel channel) { + return lastmsgCustom.stream().anyMatch(lmd -> lmd.channel.getLongID() == channel.getLongID()); + } + + public static CustomLMD getCustomChat(IChannel channel) { + return lastmsgCustom.stream().filter(lmd -> lmd.channel.getLongID() == channel.getLongID()).findAny().orElse(null); + } + + public static boolean removeCustomChat(IChannel channel) { + return lastmsgCustom.removeIf(lmd -> lmd.channel.getLongID() == channel.getLongID()); + } + + public static List getCustomChats() { + return Collections.unmodifiableList(lastmsgCustom); + } + + /** + * May contain P<DiscordID> as key for public chat + */ + public static final HashMap UnconnectedSenders = new HashMap<>(); + public static final HashMap ConnectedSenders = new HashMap<>(); + /** + * May contain P<DiscordID> as key for public chat + */ + public static final HashMap OnlineSenders = new HashMap<>(); + public static short ListC = 0; + + public static void resetLastMessage() { + (lastmsgdata == null ? lastmsgdata = new LastMsgData(DiscordPlugin.chatchannel, null, null) + : lastmsgdata).message = null; + } // Don't set the whole object to null, the player and channel information should be preserved + + public static void resetLastMessage(IChannel channel) { + for (LastMsgData data : lastmsgPerUser) + if (data.channel.getLongID() == channel.getLongID()) + data.message = null; // Since only private channels are stored, only those will work anyways + } + + public static void resetLastMessageCustom(IChannel channel) { + for (LastMsgData data : lastmsgCustom) + if (data.channel.getLongID() == channel.getLongID()) + data.message = null; + } + + /** + * This overload sends it to the global chat. + */ + public static void sendSystemMessageToChat(String msg) { + forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, DPUtils.sanitizeString(msg))); + } + + public static void sendSystemMessageToChat(TBMCSystemChatEvent event) { + forAllowedMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, DPUtils.sanitizeString(event.getMessage())), + event); + } + + public static void forAllMCChat(Consumer action) { + action.accept(DiscordPlugin.chatchannel); + for (LastMsgData data : lastmsgPerUser) + action.accept(data.channel); + lastmsgCustom.forEach(cc -> action.accept(cc.channel)); + } + + private static void forAllowedMCChat(Consumer action, TBMCSystemChatEvent event) { + if (Channel.GlobalChat.ID.equals(event.getChannel().ID)) + action.accept(DiscordPlugin.chatchannel); + for (LastMsgData data : lastmsgPerUser) + if (event.shouldSendTo(getSender(data.channel, data.user))) + action.accept(data.channel); + lastmsgCustom.stream().filter(data -> event.shouldSendTo(data.dcp)) + .map(data -> data.channel).forEach(action); + } + + public static void stop() { + if (sendthread != null) sendthread.interrupt(); + if (recthread != null) recthread.interrupt(); + } + + private BukkitTask rectask; + private LinkedBlockingQueue recevents = new LinkedBlockingQueue<>(); + private IMessage lastmsgfromd; // Last message sent by a Discord user, used for clearing checkmarks + private Runnable recrun; + private static Thread recthread; + + @Override // Discord + public void handle(MessageReceivedEvent ev) { + if (DiscordPlugin.SafeMode) + return; + val author = ev.getMessage().getAuthor(); + if (author.isBot()) + return; + final boolean hasCustomChat = hasCustomChat(ev.getChannel()); + if (!ev.getMessage().getChannel().getStringID().equals(DiscordPlugin.chatchannel.getStringID()) + && !(ev.getMessage().getChannel().isPrivate() && isMinecraftChatEnabled(author.getStringID())) + && !hasCustomChat) + return; + if (ev.getMessage().getContent().equalsIgnoreCase("mcchat")) + return; // Race condition: If it gets here after it enabled mcchat it says it - I might as well allow disabling with this (CommandListener) + if (CommandListener.runCommand(ev.getMessage(), true)) + return; + if (!ev.getMessage().getChannel().isPrivate()) + resetLastMessage(); + else if (hasCustomChat) + resetLastMessageCustom(ev.getChannel()); + else + resetLastMessage(ev.getMessage().getChannel()); + lastlist++; + recevents.add(ev); + if (rectask != null) + return; + recrun = () -> { //Don't return in a while loop next time + recthread = Thread.currentThread(); + processDiscordToMC(); + if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down + rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Continue message processing + }; + rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Start message processing + } + + private void processDiscordToMC() { + @val + sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent event; + try { + event = recevents.take(); + } catch (InterruptedException e1) { + rectask.cancel(); + return; + } + val sender = event.getMessage().getAuthor(); + val user = DiscordPlayer.getUser(sender.getStringID(), DiscordPlayer.class); + String dmessage = event.getMessage().getContent(); + try { + final DiscordSenderBase dsender = getSender(event.getMessage().getChannel(), sender); + + for (IUser u : event.getMessage().getMentions()) { + dmessage = dmessage.replace(u.mention(false), "@" + u.getName()); // TODO: IG Formatting + final String nick = u.getNicknameForGuild(DiscordPlugin.mainServer); + dmessage = dmessage.replace(u.mention(true), "@" + (nick != null ? nick : u.getName())); + } + + dmessage = EmojiParser.parseToAliases(dmessage, EmojiParser.FitzpatrickAction.PARSE); //Converts emoji to text- TODO: Add option to disable (resource pack?) + dmessage = dmessage.replaceAll(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:"); //Convert to Discord's format so it still shows up + + Function getChatMessage = msg -> // + msg + (event.getMessage().getAttachments().size() > 0 ? "\n" + event.getMessage() + .getAttachments().stream().map(a -> a.getUrl()).collect(Collectors.joining("\n")) + : ""); + + CustomLMD clmd = getCustomChat(event.getChannel()); + + boolean react = false; + + if (dmessage.startsWith("/")) { // Ingame command + DPUtils.perform(() -> { + if (!event.getMessage().isDeleted() && !event.getChannel().isPrivate()) + event.getMessage().delete(); + }); + //preprocessChat(dsender, dmessage); - Same is done below + final String cmdlowercased = dmessage.substring(1).toLowerCase(); + if (dsender instanceof DiscordSender && Arrays.stream(UnconnectedCmds) + .noneMatch(s -> cmdlowercased.equals(s) || cmdlowercased.startsWith(s + " "))) { + // Command not whitelisted + dsender.sendMessage("Sorry, you can only access these commands:\n" + + Arrays.stream(UnconnectedCmds).map(uc -> "/" + uc) + .collect(Collectors.joining(", ")) + + (user.getConnectedID(TBMCPlayer.class) == null + ? "\nTo access your commands, first please connect your accounts, using /connect in " + + DiscordPlugin.botchannel.mention() + + "\nThen y" + : "\nY") + + "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!"); + return; + } + if (lastlist > 5) { + ListC = 0; + lastlist = 0; + } + if (cmdlowercased.equals("list") && Bukkit.getOnlinePlayers().size() == lastlistp && ListC++ > 2) // Lowered already + { + dsender.sendMessage("Stop it. You know the answer."); + lastlist = 0; + } else { + int spi = cmdlowercased.indexOf(' '); + final String topcmd = spi == -1 ? cmdlowercased : cmdlowercased.substring(0, spi); + Optional ch = Channel.getChannels().stream() + .filter(c -> c.ID.equalsIgnoreCase(topcmd) + || (c.IDs != null && c.IDs.length > 0 + && Arrays.stream(c.IDs).anyMatch(id -> id.equalsIgnoreCase(topcmd)))).findAny(); + if (!ch.isPresent()) + Bukkit.getScheduler().runTask(DiscordPlugin.plugin, + () -> { + VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmdlowercased); + Bukkit.getLogger().info(dsender.getName() + " issued command from Discord: /" + cmdlowercased); + }); + else { + Channel chc = ch.get(); + if (!chc.ID.equals(Channel.GlobalChat.ID) && !chc.ID.equals("rp") && !event.getMessage().getChannel().isPrivate()) + dsender.sendMessage( + "You can only talk in global in the public chat. DM `mcchat` to enable private chat to talk in the other channels."); + else { + if (spi == -1) // Switch channels + { + val oldch = dsender.getMcchannel(); + if (oldch instanceof ChatRoom) + ((ChatRoom) oldch).leaveRoom(dsender); + if (!oldch.ID.equals(chc.ID)) { + dsender.setMcchannel(chc); + if (chc instanceof ChatRoom) + ((ChatRoom) chc).joinRoom(dsender); + } else + dsender.setMcchannel(Channel.GlobalChat); + dsender.sendMessage("You're now talking in: " + + DPUtils.sanitizeString(dsender.getMcchannel().DisplayName)); + } else { // Send single message + final String msg = event.getMessage().getContent().substring(spi + 2); + if (clmd == null) + TBMCChatAPI.SendChatMessage(chc, dsender, getChatMessage.apply(msg), true); + else + TBMCChatAPI.SendChatMessageDontCheckSender(chc, dsender, getChatMessage.apply(msg), true, clmd.dcp); + react = true; + } + } + } + } + lastlistp = (short) Bukkit.getOnlinePlayers().size(); + } else {// Not a command + if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0 + && !event.getChannel().isPrivate() && event.getMessage().isSystemMessage()) + TBMCChatAPI.SendSystemMessage(Channel.GlobalChat, 0, "everyone", + (dsender instanceof Player ? ((Player) dsender).getDisplayName() + : dsender.getName()) + " pinned a message on Discord."); + else { + if (clmd != null) + TBMCChatAPI.SendChatMessageDontCheckSender(clmd.mcchannel, dsender, getChatMessage.apply(dmessage), false, clmd.dcp); + else + TBMCChatAPI.SendChatMessage(dsender.getMcchannel(), dsender, getChatMessage.apply(dmessage)); + react = true; + } + } + if (react) { + try { + if (lastmsgfromd != null) { + DPUtils.perform(() -> lastmsgfromd.removeReaction(DiscordPlugin.dc.getOurUser(), + DiscordPlugin.DELIVERED_REACTION)); // Remove it no matter what, we know it's there 99.99% of the time + } + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occured while removing reactions from chat!", e); + } + lastmsgfromd = event.getMessage(); + DPUtils.perform(() -> event.getMessage().addReaction(DiscordPlugin.DELIVERED_REACTION)); + } + } catch (Exception e) { + TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e); + } + } + + private boolean preprocessChat(DiscordSenderBase dsender, String dmessage) { + if (dmessage.length() < 2) + return false; + int index = dmessage.indexOf(" "); + String cmd; + if (index == -1) { // Only the command is run + cmd = dmessage; + for (Channel channel : Channel.getChannels()) { + if (cmd.equalsIgnoreCase(channel.ID) || (channel.IDs != null && Arrays.stream(channel.IDs).anyMatch(cmd::equalsIgnoreCase))) { + Channel oldch = dsender.getMcchannel(); + if (oldch instanceof ChatRoom) + ((ChatRoom) oldch).leaveRoom(dsender); + if (oldch.equals(channel)) + dsender.setMcchannel(Channel.GlobalChat); + else { + dsender.setMcchannel(channel); + if (channel instanceof ChatRoom) + ((ChatRoom) channel).joinRoom(dsender); + } + dsender.sendMessage("You are now talking in: " + dsender.getMcchannel().DisplayName); + return true; + } + } + } else { // We have arguments + cmd = dmessage.substring(0, index); + for (Channel channel : Channel.getChannels()) { + if (cmd.equalsIgnoreCase(channel.ID) || (channel.IDs != null && Arrays.stream(channel.IDs).anyMatch(cmd::equalsIgnoreCase))) { + TBMCChatAPI.SendChatMessage(channel, dsender, dmessage.substring(index + 1)); + return true; + } + } + // TODO: Target selectors + } + return false; + } + + /** + * 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 static DiscordSenderBase getSender(IChannel channel, final IUser author) { + val key = (channel.isPrivate() ? "" : "P") + author.getStringID(); + return Stream.>>of( // https://stackoverflow.com/a/28833677/2703239 + () -> Optional.ofNullable(OnlineSenders.get(key)), // Find first non-null + () -> Optional.ofNullable(ConnectedSenders.get(key)), // This doesn't support the public chat, but it'll always return null for it + () -> Optional.ofNullable(UnconnectedSenders.get(key)), () -> { + val dsender = new DiscordSender(author, channel); + UnconnectedSenders.put(key, dsender); + return Optional.of(dsender); + }).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get(); + } +} diff --git a/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java b/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java index 3afc038..74ee6a2 100755 --- a/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java +++ b/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java @@ -1,58 +1,219 @@ -package buttondevteam.discordplugin.listeners; - -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.commands.ConnectCommand; -import buttondevteam.lib.player.TBMCPlayerGetInfoEvent; -import buttondevteam.lib.player.TBMCPlayerJoinEvent; -import discord4j.core.object.entity.Member; -import discord4j.core.object.entity.User; -import discord4j.core.object.util.Snowflake; -import lombok.val; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.server.ServerCommandEvent; -import reactor.core.publisher.Mono; - -public class MCListener implements Listener { - @EventHandler - public void onPlayerJoin(TBMCPlayerJoinEvent e) { - if (ConnectCommand.WaitingToConnect.containsKey(e.GetPlayer().PlayerName().get())) { - @SuppressWarnings("ConstantConditions") User user = DiscordPlugin.dc - .getUserById(Snowflake.of(ConnectCommand.WaitingToConnect.get(e.GetPlayer().PlayerName().get()))).block(); - if (user == null) return; - e.getPlayer().sendMessage("§bTo connect with the Discord account @" + user.getUsername() + "#" + user.getDiscriminator() - + " do /discord accept"); - e.getPlayer().sendMessage("§bIf it wasn't you, do /discord decline"); - } - } - - @EventHandler - public void onGetInfo(TBMCPlayerGetInfoEvent e) { - if (DiscordPlugin.SafeMode) - return; - DiscordPlayer dp = e.getPlayer().getAs(DiscordPlayer.class); - if (dp == null || dp.getDiscordID() == null || dp.getDiscordID().equals("")) - return; - 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 onServerCommand(ServerCommandEvent e) { - DiscordPlugin.Restart = !e.getCommand().equalsIgnoreCase("stop"); // The variable is always true except if stopped - } -} +package buttondevteam.discordplugin.listeners; + +import buttondevteam.discordplugin.*; +import buttondevteam.discordplugin.commands.ConnectCommand; +import buttondevteam.lib.TBMCCoreAPI; +import buttondevteam.lib.TBMCSystemChatEvent; +import buttondevteam.lib.player.*; +import com.earth2me.essentials.CommandSource; +import lombok.val; +import net.ess3.api.events.AfkStatusChangeEvent; +import net.ess3.api.events.MuteStatusChangeEvent; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.*; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerKickEvent; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerLoginEvent.Result; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.server.BroadcastMessageEvent; +import org.bukkit.event.server.ServerCommandEvent; +import org.bukkit.plugin.AuthorNagException; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.RegisteredListener; +import sx.blah.discord.handle.obj.IRole; +import sx.blah.discord.handle.obj.IUser; +import sx.blah.discord.util.DiscordException; +import sx.blah.discord.util.MissingPermissionsException; + +import java.util.Arrays; +import java.util.logging.Level; + +public class MCListener implements Listener { + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerLogin(PlayerLoginEvent e) { + if (e.getResult() != Result.ALLOWED) + return; + MCChatListener.ConnectedSenders.values().stream() + .filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny() + .ifPresent(dcp -> callEventExcludingSome(new PlayerQuitEvent(dcp, ""))); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerJoin(TBMCPlayerJoinEvent 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 = e.GetPlayer().getAs(DiscordPlayer.class); + if (dp != null) { + val user = DiscordPlugin.dc.getUserByID(Long.parseLong(dp.getDiscordID())); + MCChatListener.OnlineSenders.put(dp.getDiscordID(), + new DiscordPlayerSender(user, user.getOrCreatePMChannel(), p)); + MCChatListener.OnlineSenders.put("P" + dp.getDiscordID(), + new DiscordPlayerSender(user, DiscordPlugin.chatchannel, p)); + } + if (ConnectCommand.WaitingToConnect.containsKey(e.GetPlayer().PlayerName().get())) { + IUser user = DiscordPlugin.dc + .getUserByID(Long.parseLong(ConnectCommand.WaitingToConnect.get(e.GetPlayer().PlayerName().get()))); + p.sendMessage("§bTo connect with the Discord account @" + user.getName() + "#" + user.getDiscriminator() + + " do /discord accept"); + p.sendMessage("§bIf it wasn't you, do /discord decline"); + } + if (!DiscordPlugin.hooked) + MCChatListener.sendSystemMessageToChat(e.GetPlayer().PlayerName().get() + " joined the game"); + MCChatListener.ListC = 0; + ChromaBot.getInstance().updatePlayerList(); + }); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerLeave(TBMCPlayerQuitEvent e) { + if (e.getPlayer() instanceof DiscordConnectedPlayer) + return; // Only care about real users + MCChatListener.OnlineSenders.entrySet() + .removeIf(entry -> entry.getValue().getUniqueId().equals(e.getPlayer().getUniqueId())); + Bukkit.getScheduler().runTask(DiscordPlugin.plugin, + () -> MCChatListener.ConnectedSenders.values().stream() + .filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny() + .ifPresent(dcp -> callEventExcludingSome(new PlayerJoinEvent(dcp, "")))); + Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, + ChromaBot.getInstance()::updatePlayerList, 5); + } + + @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.sendSystemMessageToChat(e.getPlayer().getName() + " left the game"); // message for this - Oh wait this doesn't even send normally because of the hook + } + + @EventHandler + public void onGetInfo(TBMCPlayerGetInfoEvent e) { + if (DiscordPlugin.SafeMode) + return; + DiscordPlayer dp = e.getPlayer().getAs(DiscordPlayer.class); + if (dp == null || dp.getDiscordID() == null || dp.getDiscordID().equals("")) + return; + IUser user = DiscordPlugin.dc.getUserByID(Long.parseLong(dp.getDiscordID())); + e.addInfo("Discord tag: " + user.getName() + "#" + user.getDiscriminator()); + e.addInfo(user.getPresence().getStatus().toString()); + if (user.getPresence().getActivity().isPresent() && user.getPresence().getText().isPresent()) + e.addInfo(user.getPresence().getActivity().get() + ": " + user.getPresence().getText().get()); + } + + @EventHandler(priority = EventPriority.LOW) + public void onPlayerDeath(PlayerDeathEvent e) { + if (!DiscordPlugin.hooked) + MCChatListener.sendSystemMessageToChat(e.getDeathMessage()); + } + + @EventHandler + public void onPlayerAFK(AfkStatusChangeEvent e) { + if (e.isCancelled() || !e.getAffected().getBase().isOnline()) + return; + MCChatListener.sendSystemMessageToChat(DPUtils.sanitizeString(e.getAffected().getBase().getDisplayName()) + + " is " + (e.getValue() ? "now" : "no longer") + " AFK."); + } + + @EventHandler + public void onServerCommand(ServerCommandEvent e) { + DiscordPlugin.Restart = !e.getCommand().equalsIgnoreCase("stop"); // The variable is always true except if stopped + } + + @EventHandler + public void onPlayerMute(MuteStatusChangeEvent e) { + try { + DPUtils.performNoWait(() -> { + final IRole role = DiscordPlugin.dc.getRoleByID(164090010461667328L); + final CommandSource source = e.getAffected().getSource(); + if (!source.isPlayer()) + return; + final IUser user = DiscordPlugin.dc.getUserByID( + Long.parseLong(TBMCPlayerBase.getPlayer(source.getPlayer().getUniqueId(), TBMCPlayer.class) + .getAs(DiscordPlayer.class).getDiscordID())); // TODO: Use long + if (e.getValue()) + user.addRole(role); + else + user.removeRole(role); + }); + } catch (DiscordException | MissingPermissionsException ex) { + TBMCCoreAPI.SendException("Failed to give/take Muted role to player " + e.getAffected().getName() + "!", + ex); + } + } + + @EventHandler + public void onChatSystemMessage(TBMCSystemChatEvent event) { + MCChatListener.sendSystemMessageToChat(event); + } + + @EventHandler + public void onBroadcastMessage(BroadcastMessageEvent event) { + MCChatListener.sendSystemMessageToChat(event.getMessage()); + } + + private static final String[] EXCLUDED_PLUGINS = {"ProtocolLib", "LibsDisguises"}; + + public static void callEventExcludingSome(Event event) { + callEventExcluding(event, EXCLUDED_PLUGINS); + } + + /** + * Calls an event with the given details. + *

+ * This method only synchronizes when the event is not asynchronous. + * + * @param event Event details + * @param plugins The plugins to exclude. Not case sensitive. + */ + private static void callEventExcluding(Event event, 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, plugins); + } else { + synchronized (Bukkit.getPluginManager()) { + fireEventExcluding(event, plugins); + } + } + } + + private static void fireEventExcluding(Event event, 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 -> 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); + } + } + } +} 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 e8307f3..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.java +++ /dev/null @@ -1,175 +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.GuildChannel; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.MessageChannel; -import discord4j.core.object.entity.User; -import discord4j.core.object.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; - } - 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 4319cf9..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.java +++ /dev/null @@ -1,51 +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.TBMCCoreAPI; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -import discord4j.core.object.entity.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; - } - try (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(); - } catch (Exception e) { - TBMCCoreAPI.SendException("Error while setting mcchat for user " + author.getUsername() + "#" + author.getDiscriminator(), e); - } - return true; - } // TODO: Pin channel switching to indicate the current channel - -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java deleted file mode 100644 index c9edaca..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java +++ /dev/null @@ -1,75 +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.core.object.entity.MessageChannel; -import discord4j.core.object.entity.User; -import discord4j.core.object.util.Snowflake; -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 ArrayList lastmsgCustom = new ArrayList<>(); - - public static void addCustomChat(MessageChannel channel, String groupid, Channel mcchannel, User user, DiscordConnectedPlayer dcp, int toggles, Set brtoggles) { - if (mcchannel instanceof ChatRoom) { - ((ChatRoom) mcchannel).joinRoom(dcp); - if (groupid == null) groupid = mcchannel.getGroupID(dcp); - } - 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) { - 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 Channel mcchannel; - 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 202fd93..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java +++ /dev/null @@ -1,432 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.ComponentManager; -import buttondevteam.core.component.channel.Channel; -import buttondevteam.core.component.channel.ChatRoom; -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.DiscordSender; -import buttondevteam.discordplugin.DiscordSenderBase; -import buttondevteam.discordplugin.listeners.CommandListener; -import buttondevteam.discordplugin.listeners.CommonListeners; -import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; -import buttondevteam.discordplugin.playerfaker.VanillaCommandListener14; -import buttondevteam.discordplugin.util.Timings; -import buttondevteam.lib.*; -import buttondevteam.lib.chat.ChatMessage; -import buttondevteam.lib.chat.TBMCChatAPI; -import buttondevteam.lib.player.TBMCPlayer; -import com.vdurmont.emoji.EmojiParser; -import discord4j.core.event.domain.message.MessageCreateEvent; -import discord4j.core.object.Embed; -import discord4j.core.object.entity.*; -import discord4j.core.object.util.Snowflake; -import discord4j.core.spec.EmbedCreateSpec; -import lombok.val; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.scheduler.BukkitTask; -import reactor.core.publisher.Mono; - -import java.awt.*; -import java.time.Instant; -import java.util.AbstractMap; -import java.util.Arrays; -import java.util.Optional; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeoutException; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -public class MCChatListener implements Listener { - private BukkitTask sendtask; - private LinkedBlockingQueue> sendevents = new LinkedBlockingQueue<>(); - private Runnable sendrunnable; - private static Thread sendthread; - private final MinecraftChatModule module; - - 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()) //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().substring(0, 1) + "]") // - + (DPUtils.sanitizeStringNoEscape(ChromaUtils.getDisplayName(e.getSender()))); - val color = e.getChannel().Color().get(); - final Consumer embed = ecs -> { - ecs.setDescription(e.getMessage()).setColor(new Color(color.getRed(), - color.getGreen(), color.getBlue())); - 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); - } - - val iterator = MCChatCustom.lastmsgCustom.iterator(); - while (iterator.hasNext()) { - val lmd = iterator.next(); - if ((e.isFromCommand() || isdifferentchannel.test(lmd.channel.getId())) //Test if msg is from Discord - && e.getChannel().ID.equals(lmd.mcchannel.ID) //If it's from a command, the command msg has been deleted, so we need to send it - && e.getGroupID().equals(lmd.groupID)) { //Check if this is the group we want to test - #58 - if (e.shouldSendTo(lmd.dcp)) //Check original user's permissions - doit.accept(lmd); - else { - iterator.remove(); //If the user no longer has permission, remove the connection - lmd.channel.createMessage("The user no longer has permission to view the channel, connection removed.").subscribe(); - } - } - } - } catch (InterruptedException ex) { //Stop if interrupted anywhere - sendtask.cancel(); - sendtask = null; - } catch (Exception ex) { - TBMCCoreAPI.SendException("Error while sending message to Discord!", ex); - } - } - - @EventHandler - public void onChatPreprocess(TBMCChatPreprocessEvent event) { - int start = -1; - while ((start = event.getMessage().indexOf('@', start + 1)) != -1) { - int mid = event.getMessage().indexOf('#', start + 1); - if (mid == -1) - return; - int end_ = event.getMessage().indexOf(' ', mid + 1); - if (end_ == -1) - end_ = event.getMessage().length(); - final int end = end_; - final int startF = start; - 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. Any calls to onMCChat will restart it as long as we're not in safe mode. - * - * @param wait Wait 5 seconds for the threads to stop - */ - public static void stop(boolean wait) { - if (sendthread != null) sendthread.interrupt(); - if (recthread != null) recthread.interrupt(); - try { - if (sendthread != null) { - sendthread.interrupt(); - if (wait) - sendthread.join(5000); - } - if (recthread != null) { - recthread.interrupt(); - if (wait) - recthread.join(5000); - } - MCChatUtils.lastmsgdata = null; - MCChatPrivate.lastmsgPerUser.clear(); - MCChatCustom.lastmsgCustom.clear(); - MCChatUtils.lastmsgfromd.clear(); - MCChatUtils.ConnectedSenders.clear(); - MCChatUtils.UnconnectedSenders.clear(); - recthread = sendthread = null; - } catch (InterruptedException e) { - e.printStackTrace(); //This thread shouldn't be interrupted - } - } - - private BukkitTask rectask; - private LinkedBlockingQueue recevents = new LinkedBlockingQueue<>(); - private Runnable recrun; - private static Thread recthread; - - // Discord - public Mono handleDiscord(MessageCreateEvent ev) { - val ret = Mono.just(true); - if (!ComponentManager.isEnabled(MinecraftChatModule.class)) - return ret; - Timings timings = CommonListeners.timings; - timings.printElapsed("Chat event"); - val author = ev.getMessage().getAuthor(); - final boolean hasCustomChat = MCChatCustom.hasCustomChat(ev.getMessage().getChannelId()); - return ev.getMessage().getChannel().filter(channel -> { - timings.printElapsed("Filter 1"); - return !(ev.getMessage().getChannelId().asLong() != module.chatChannel().get().asLong() - && !(channel instanceof PrivateChannel - && author.map(u -> MCChatPrivate.isMinecraftChatEnabled(u.getId().asString())).orElse(false)) - && !hasCustomChat); //Chat isn't enabled on this channel - }).filter(channel -> { - timings.printElapsed("Filter 2"); - return !(channel instanceof PrivateChannel //Only in private chat - && ev.getMessage().getContent().isPresent() - && ev.getMessage().getContent().get().length() < "/mcchat<>".length() - && ev.getMessage().getContent().get().replace("/", "") - .equalsIgnoreCase("mcchat")); //Either mcchat or /mcchat - //Allow disabling the chat if needed - }).filterWhen(channel -> CommandListener.runCommand(ev.getMessage(), channel, true)) - //Allow running commands in chat channels - .filter(channel -> { - MCChatUtils.resetLastMessage(channel); - recevents.add(ev); - timings.printElapsed("Message event added"); - if (rectask != null) - return true; - recrun = () -> { //Don't return in a while loop next time - recthread = Thread.currentThread(); - processDiscordToMC(); - if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down - rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Continue message processing - }; - rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Start message processing - return true; - }).map(b -> false).defaultIfEmpty(true); - } - - private void processDiscordToMC() { - MessageCreateEvent event; - try { - event = recevents.take(); - } catch (InterruptedException e1) { - rectask.cancel(); - return; - } - val sender = event.getMessage().getAuthor().orElse(null); - String dmessage = event.getMessage().getContent().orElse(""); - try { - final DiscordSenderBase dsender = MCChatUtils.getSender(event.getMessage().getChannelId(), sender); - val user = dsender.getChromaUser(); - - for (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 (!isPrivate) - event.getMessage().delete().subscribe(); - final String cmd = dmessage.substring(1); - final String cmdlowercased = cmd.toLowerCase(); - if (dsender instanceof DiscordSender && module.whitelistedCommands().get().stream() - .noneMatch(s -> cmdlowercased.equals(s) || cmdlowercased.startsWith(s + " "))) { - // Command not whitelisted - dsender.sendMessage("Sorry, you can only access these commands:\n" - + module.whitelistedCommands().get().stream().map(uc -> "/" + uc) - .collect(Collectors.joining(", ")) - + (user.getConnectedID(TBMCPlayer.class) == null - ? "\nTo access your commands, first please connect your accounts, using /connect in " - + DPUtils.botmention() - + "\nThen y" - : "\nY") - + "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!"); - return; - } - val ev = new TBMCCommandPreprocessEvent(dsender, dmessage); - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> - Bukkit.getPluginManager().callEvent(ev)); - if (ev.isCancelled()) - return; - int spi = cmdlowercased.indexOf(' '); - final String topcmd = spi == -1 ? cmdlowercased : cmdlowercased.substring(0, spi); - Optional ch = Channel.getChannels() - .filter(c -> c.ID.equalsIgnoreCase(topcmd) - || (c.IDs().get().length > 0 - && Arrays.stream(c.IDs().get()).anyMatch(id -> id.equalsIgnoreCase(topcmd)))).findAny(); - if (!ch.isPresent()) //TODO: What if talking in the public chat while we have it on a different one - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, //Commands need to be run sync - () -> { //TODO: Better handling... - val channel = user.channel(); - val chtmp = channel.get(); - if (clmd != null) { - channel.set(clmd.mcchannel); //Hack to send command in the channel - } //TODO: Permcheck isn't implemented for commands - try { - String mcpackage = Bukkit.getServer().getClass().getPackage().getName(); - if (mcpackage.contains("1_12")) - VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd); - else if (mcpackage.contains("1_14")) - VanillaCommandListener14.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); - } - Bukkit.getLogger().info(dsender.getName() + " issued command from Discord: /" + cmd); - if (clmd != null) - channel.set(chtmp); - }); - else { - Channel chc = ch.get(); - if (!chc.isGlobal() && !isPrivate) - dsender.sendMessage( - "You can only talk in a public chat here. DM `mcchat` to enable private chat to talk in the other channels."); - else { - if (spi == -1) // Switch channels - { - val channel = dsender.getChromaUser().channel(); - val oldch = channel.get(); - if (oldch instanceof ChatRoom) - ((ChatRoom) oldch).leaveRoom(dsender); - if (!oldch.ID.equals(chc.ID)) { - channel.set(chc); - if (chc instanceof ChatRoom) - ((ChatRoom) chc).joinRoom(dsender); - } else - channel.set(Channel.GlobalChat); - dsender.sendMessage("You're now talking in: " - + DPUtils.sanitizeString(channel.get().DisplayName().get())); - } else { // Send single message - final String msg = cmd.substring(spi + 1); - val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(msg)).fromCommand(true); - if (clmd == null) - TBMCChatAPI.SendChatMessage(cmb.build(), chc); - else - TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), chc); - react = true; - } - } - } - } else {// Not a command - if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0 - && !isPrivate && event.getMessage().getType() == Message.Type.CHANNEL_PINNED_MESSAGE) { - val rtr = clmd != null ? clmd.mcchannel.getRTR(clmd.dcp) - : dsender.getChromaUser().channel().get().getRTR(dsender); - TBMCChatAPI.SendSystemMessage(clmd != null ? clmd.mcchannel : dsender.getChromaUser().channel().get(), rtr, - (dsender instanceof Player ? ((Player) dsender).getDisplayName() - : dsender.getName()) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL); - } else { - val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(dmessage)).fromCommand(false); - if (clmd != null) - TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), clmd.mcchannel); - else - TBMCChatAPI.SendChatMessage(cmb.build()); - react = true; - } - } - if (react) { - try { - val lmfd = MCChatUtils.lastmsgfromd.get(event.getMessage().getChannelId().asLong()); - if (lmfd != null) { - lmfd.removeSelfReaction(DiscordPlugin.DELIVERED_REACTION).subscribe(); // Remove it no matter what, we know it's there 99.99% of the time - } - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while removing reactions from chat!", e); - } - MCChatUtils.lastmsgfromd.put(event.getMessage().getChannelId().asLong(), event.getMessage()); - event.getMessage().addReaction(DiscordPlugin.DELIVERED_REACTION).subscribe(); - } - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e); - } - } - - @FunctionalInterface - private interface InterruptibleConsumer { - void accept(T value) throws TimeoutException, InterruptedException; - } -} 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 06c3ce1..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.java +++ /dev/null @@ -1,67 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.ComponentManager; -import buttondevteam.discordplugin.DiscordConnectedPlayer; -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.lib.player.TBMCPlayer; -import discord4j.core.object.entity.MessageChannel; -import discord4j.core.object.entity.PrivateChannel; -import discord4j.core.object.entity.User; -import lombok.val; -import org.bukkit.Bukkit; - -import 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) { - 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); - 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; - if (p == null // Player is offline - If the player is online, that takes precedence - && sender.isLoggedIn()) //Don't call the quit event if login failed - MCChatUtils.callLogoutEvent(sender, true); - sender.setLoggedIn(false); - } - } // ---- PermissionsEx warning is normal on logout ---- - if (!start) - MCChatUtils.lastmsgfromd.remove(channel.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() { - 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(), false); //This is sync - MCChatUtils.ConnectedSenders.clear(); - } - -} 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 6b1c61d..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.java +++ /dev/null @@ -1,387 +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.core.object.entity.*; -import discord4j.core.object.util.Snowflake; -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 reactor.core.publisher.Mono; - -import javax.annotation.Nullable; -import java.net.InetAddress; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -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 HashMap> UnconnectedSenders = new HashMap<>(); - public static final HashMap> ConnectedSenders = new HashMap<>(); - /** - * May contain P<DiscordID> as key for public chat - */ - public static final HashMap> OnlineSenders = new HashMap<>(); - 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 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 getModule() == null; - } - - private static MinecraftChatModule getModule() { - if (module == null) module = ComponentManager.getIfEnabled(MinecraftChatModule.class); - else if (!module.isEnabled()) module = null; //Reset if disabled - 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.")); - 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 -> 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 - } - - private static boolean checkEssentials(Player p) { - var ess = MainPlugin.ess; - if (ess == null) return true; - return !ess.getUser(p).isHidden(); - } - - public static T addSender(HashMap> senders, - User user, T sender) { - return addSender(senders, user.getId().asString(), sender); - } - - public static T addSender(HashMap> senders, - String did, T sender) { - var map = senders.get(did); - if (map == null) - map = new HashMap<>(); - map.put(sender.getChannel().getId(), sender); - senders.put(did, map); - return sender; - } - - public static T getSender(HashMap> senders, - Snowflake channel, User user) { - var map = senders.get(user.getId().asString()); - if (map != null) - return map.get(channel); - return null; - } - - public static T removeSender(HashMap> senders, - Snowflake channel, User user) { - var map = senders.get(user.getId().asString()); - if (map != null) - return map.remove(channel); - return null; - } - - public static void forAllMCChat(Consumer> action) { - if (notEnabled()) return; - action.accept(module.chatChannelMono()); - for (LastMsgData data : MCChatPrivate.lastmsgPerUser) - action.accept(Mono.just(data.channel)); - // lastmsgCustom.forEach(cc -> action.accept(cc.channel)); - Only send relevant messages to custom chat - } - - /** - * For custom and all MC chat - * - * @param action The action to act - * @param toggle The toggle to check - * @param hookmsg Whether the message is also sent from the hook - */ - public static void forCustomAndAllMCChat(Consumer> action, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { - if (notEnabled()) return; - if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg) - forAllMCChat(action); - final Consumer customLMDConsumer = cc -> action.accept(Mono.just(cc.channel)); - if (toggle == null) - MCChatCustom.lastmsgCustom.forEach(customLMDConsumer); - else - MCChatCustom.lastmsgCustom.stream().filter(cc -> (cc.toggles & toggle.flag) != 0).forEach(customLMDConsumer); - } - - /** - * 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 void forAllowedCustomMCChat(Consumer> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle) { - if (notEnabled()) return; - MCChatCustom.lastmsgCustom.stream().filter(clmd -> { - //new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple - 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)); - }).forEach(cc -> action.accept(Mono.just(cc.channel))); //TODO: Send error messages on channel connect - } - - /** - * 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 void forAllowedCustomAndAllMCChat(Consumer> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { - if (notEnabled()) return; - if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg) - forAllMCChat(action); - forAllowedCustomMCChat(action, sender, toggle); - } - - public static Consumer> send(String message) { - return ch -> ch.flatMap(mc -> { - resetLastMessage(mc); - return mc.createMessage(DPUtils.sanitizeString(message)); - }).subscribe(); - } - - public static void forAllowedMCChat(Consumer> action, TBMCSystemChatEvent event) { - if (notEnabled()) return; - if (event.getChannel().isGlobal()) - action.accept(module.chatChannelMono()); - for (LastMsgData data : MCChatPrivate.lastmsgPerUser) - if (event.shouldSendTo(getSender(data.channel.getId(), data.user))) - action.accept(Mono.just(data.channel)); //TODO: Only store ID? - MCChatCustom.lastmsgCustom.stream().filter(clmd -> { - if (!clmd.brtoggles.contains(event.getTarget())) - return false; - return event.shouldSendTo(clmd.dcp); - }).map(clmd -> Mono.just(clmd.channel)).forEach(action); - } - - /** - * This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc. - */ - static DiscordSenderBase getSender(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. - */ - @SuppressWarnings("WeakerAccess") - 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); - DPUtils.getLogger().info(dcp.getName() + " (" + dcp.getUniqueId() + ") logged in from Discord"); - }); - } - - /** - * Only calls the events if the player is actually logged in - * - * @param dcp The player - * @param needsSync Whether we're in an async thread - */ - public static void callLogoutEvent(DiscordConnectedPlayer dcp, boolean needsSync) { - if (!dcp.isLoggedIn()) return; - val event = new PlayerQuitEvent(dcp, ""); - if (needsSync) callEventSync(event); - else callEventExcludingSome(event); - dcp.setLoggedIn(false); - DPUtils.getLogger().info(dcp.getName() + " (" + dcp.getUniqueId() + ") logged out from Discord"); - } - - static void callEventSync(Event event) { - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> callEventExcludingSome(event)); - } - - @RequiredArgsConstructor - public static class LastMsgData { - public 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 50843e6..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java +++ /dev/null @@ -1,185 +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.core.object.entity.Role; -import discord4j.core.object.util.Snowflake; -import lombok.RequiredArgsConstructor; -import lombok.val; -import net.ess3.api.events.AfkStatusChangeEvent; -import net.ess3.api.events.MuteStatusChangeEvent; -import net.ess3.api.events.NickChangeEvent; -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; - -@RequiredArgsConstructor -class MCListener implements Listener { - private final MinecraftChatModule module; - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerLogin(PlayerLoginEvent e) { - if (e.getResult() != Result.ALLOWED) - return; - if (e.getPlayer() instanceof DiscordConnectedPlayer) - return; - MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream()) //Only private mcchat should be in ConnectedSenders - .filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny() - .ifPresent(dcp -> MCChatUtils.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(), - new DiscordPlayerSender(user, chan, p)); - MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(), - new DiscordPlayerSender(user, cc, p)); //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); - 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, - () -> MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream()) - .filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny() - .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); - } - - @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); - } - - @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); - } - - private ConfigData> muteRole() { - return DPUtils.roleData(module.getConfig(), "muteRole", "Muted"); - } - - @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(); - DPUtils.getLogger().info(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); - } - - @EventHandler - public void onBroadcastMessage(BroadcastMessageEvent event) { - MCChatUtils.forCustomAndAllMCChat(MCChatUtils.send(event.getMessage()), ChannelconBroadcast.BROADCAST, false); - } - - @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()).subscribe(yeehaw -> - MCChatUtils.forAllMCChat(MCChatUtils.send(name + (yeehaw.map(guildEmoji -> " <:YEEHAW:" + guildEmoji.getId().asString() + ">s").orElse(" YEEHAWs"))))); - } - - @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 - //System.out.println("Last token: " + t); - if (!t.startsWith("@")) - return; - String token = t.substring(1); - //System.out.println("Token: " + token); - 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(); - //System.out.println("Finished - last: " + x); - } - - @EventHandler - public void onCommandSend(PlayerCommandSendEvent event) { - event.getCommands().add("g"); - } -} 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 1018ba8..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MinecraftChatModule.java +++ /dev/null @@ -1,170 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.MainPlugin; -import buttondevteam.core.component.channel.Channel; -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordConnectedPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.playerfaker.perm.LPInjector; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.TBMCSystemChatEvent; -import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.architecture.ConfigData; -import buttondevteam.lib.architecture.ReadOnlyConfigData; -import com.google.common.collect.Lists; -import discord4j.core.object.entity.MessageChannel; -import discord4j.core.object.util.Snowflake; -import lombok.Getter; -import lombok.val; -import org.bukkit.Bukkit; -import 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 { - private @Getter MCChatListener listener; - - /** - * 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() { - return 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() { - return 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() { - return getConfig().getData("excludedPlugins", new String[]{"ProtocolLib", "LibsDisguises", "JourneyMapServer"}); - } - - /** - * If this setting is on then players logged in through the 'mcchat' command will be able to teleport using plugin commands. - * They can then use commands like /tpahere to teleport others to that place.
- * If this is off, then teleporting will have no effect. - */ - public ConfigData allowFakePlayerTeleports() { - return getConfig().getData("allowFakePlayerTeleports", false); - } - - /** - * 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 between the first and 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() { - return 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() { - return 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() { - return 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() { - return getConfig().getData("profileURL", ""); - } - - @Override - protected void enable() { - if (DPUtils.disableIfConfigErrorRes(this, chatChannel(), chatChannelMono())) - return; - /*clientID = DiscordPlugin.dc.getApplicationInfo().blockOptional().map(info->info.getId().asString()) - .orElse("Unknown"); //Need to block because otherwise it may not be set in time*/ - 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(new MCChatCommand(this)); - getPlugin().getManager().registerCommand(new ChannelconCommand(this)); - - 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 { - new LPInjector(MainPlugin.Instance); - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to init LuckPerms injector", e); - } catch (NoClassDefFoundError e) { - getPlugin().getLogger().info("No LuckPerms, not injecting"); - } - } - - @Override - protected void disable() { - 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())); - } - MCChatListener.stop(true); - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/AcceptMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/AcceptMCCommand.java new file mode 100755 index 0000000..13b3095 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mccommands/AcceptMCCommand.java @@ -0,0 +1,43 @@ +package buttondevteam.discordplugin.mccommands; + +import buttondevteam.discordplugin.DiscordPlayer; +import buttondevteam.discordplugin.commands.ConnectCommand; +import buttondevteam.discordplugin.listeners.MCChatListener; +import buttondevteam.lib.chat.CommandClass; +import buttondevteam.lib.player.ChromaGamerBase; +import buttondevteam.lib.player.TBMCPlayer; +import buttondevteam.lib.player.TBMCPlayerBase; +import org.bukkit.entity.Player; + +@CommandClass(modOnly = false, path = "accept") +public class AcceptMCCommand extends DiscordMCCommandBase { + + @Override + public String[] GetHelpText(String alias) { + return new String[] { // + "§6---- Accept Discord connection ----", // + "Accept a pending connection between your Discord and Minecraft account.", // + "To start the connection process, do §b/connect §r in the #bot channel on Discord", // + "Usage: /" + alias + " accept" // + }; + } + + @Override + public boolean OnCommand(Player player, String alias, String[] args) { + String did = ConnectCommand.WaitingToConnect.get(player.getName()); + if (did == null) { + player.sendMessage("§cYou don't have a pending connection to Discord."); + return true; + } + DiscordPlayer dp = ChromaGamerBase.getUser(did, DiscordPlayer.class); + TBMCPlayer mcp = TBMCPlayerBase.getPlayer(player.getUniqueId(), TBMCPlayer.class); + dp.connectWith(mcp); + dp.save(); + mcp.save(); + ConnectCommand.WaitingToConnect.remove(player.getName()); + MCChatListener.UnconnectedSenders.remove(did); + player.sendMessage("§bAccounts connected."); + return true; + } + +} diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/DeclineMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/DeclineMCCommand.java new file mode 100755 index 0000000..831ad89 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mccommands/DeclineMCCommand.java @@ -0,0 +1,31 @@ +package buttondevteam.discordplugin.mccommands; + +import buttondevteam.discordplugin.commands.ConnectCommand; +import buttondevteam.lib.chat.CommandClass; +import org.bukkit.entity.Player; + +@CommandClass(modOnly = false, path = "decline") +public class DeclineMCCommand extends DiscordMCCommandBase { + + @Override + public String[] GetHelpText(String alias) { + return new String[] { // + "§6---- Decline Discord connection ----", // + "Decline a pending connection between your Discord and Minecraft account.", // + "To start the connection process, do §b/connect §r in the #bot channel on Discord", // + "Usage: /" + alias + " decline" // + }; + } + + @Override + public boolean OnCommand(Player player, String alias, String[] args) { + String did = ConnectCommand.WaitingToConnect.remove(player.getName()); + if (did == null) { + player.sendMessage("§cYou don't have a pending connection to Discord."); + return true; + } + player.sendMessage("§bPending connection declined."); + return true; + } + +} diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java deleted file mode 100644 index 61d9d6b..0000000 --- a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java +++ /dev/null @@ -1,146 +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.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); - dp.save(); - mcp.save(); - ConnectCommand.WaitingToConnect.remove(player.getName()); - MCChatUtils.UnconnectedSenders.remove(did); //Remove all unconnected, will be recreated where needed - player.sendMessage("§bAccounts connected."); - return true; - } - - @Command2.Subcommand - public boolean decline(Player player) { - 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 reset." - }) - public void reload(CommandSender sender) { - if (DiscordPlugin.plugin.tryReloadConfig()) - sender.sendMessage("§bConfig reloaded."); - else - sender.sendMessage("§cFailed to reload config."); - } - - public static boolean resetting = false; - - @Command2.Subcommand(permGroup = Command2.Subcommand.MOD_GROUP, helpText = { - "Reset ChromaBot", // - "This command disables and then enables the plugin." // - }) - public void reset(CommandSender sender) { - Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> { - if (!DiscordPlugin.plugin.tryReloadConfig()) { - sender.sendMessage("§cFailed to reload config so not resetting. Check the console."); - return; - } - resetting = true; //Turned off after sending enable message (ReadyEvent) - sender.sendMessage("§bDisabling DiscordPlugin..."); - Bukkit.getPluginManager().disablePlugin(DiscordPlugin.plugin); - if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors - sender.sendMessage("§bEnabling DiscordPlugin..."); - Bukkit.getPluginManager().enablePlugin(DiscordPlugin.plugin); - if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors - sender.sendMessage("§bReset finished!"); - }); - } - - @Command2.Subcommand(helpText = { - "Version command", - "Prints the plugin version" - }) - public void version(CommandSender sender) { - sender.sendMessage(VersionCommand.getVersion()); - } - - @Command2.Subcommand(helpText = { - "Invite", - "Shows an invite link to the server" - }) - public void invite(CommandSender sender) { - if (checkSafeMode(sender)) return; - String invi = DiscordPlugin.plugin.inviteLink().get(); - if (invi.length() > 0) { - sender.sendMessage("§bInvite link: " + invi); - return; - } - DiscordPlugin.mainServer.getInvites().limitRequest(1) - .switchIfEmpty(Mono.fromRunnable(() -> sender.sendMessage("§cNo invites found for the server."))) - .subscribe(inv -> { - sender.sendMessage("§bInvite link: https://discord.gg/" + inv.getCode()); - }, e -> sender.sendMessage("§cThe invite link is not set and the bot has no permission to get it.")); - } - - @Override - public String[] getHelpText(Method method, Command2.Subcommand ann) { - switch (method.getName()) { - case "accept": - return new String[]{ // - "Accept Discord connection", // - "Accept a pending connection between your Discord and Minecraft account.", // - "To start the connection process, do §b/connect §r in the " + DPUtils.botmention() + " channel on Discord", // - }; - case "decline": - return new String[]{ // - "Decline Discord connection", // - "Decline a pending connection between your Discord and Minecraft account.", // - "To start the connection process, do §b/connect §r in the " + DPUtils.botmention() + " channel on Discord", // - }; - default: - return super.getHelpText(method, ann); - } - } - - private boolean checkSafeMode(CommandSender sender) { - if (DiscordPlugin.SafeMode) { - sender.sendMessage("§cThe plugin isn't initialized. Check console for details."); - return true; - } - return false; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommandBase.java b/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommandBase.java new file mode 100755 index 0000000..5edbafe --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommandBase.java @@ -0,0 +1,9 @@ +package buttondevteam.discordplugin.mccommands; + +import buttondevteam.lib.chat.CommandClass; +import buttondevteam.lib.chat.PlayerCommandBase; + +@CommandClass(modOnly = false, path = "discord") +public abstract class DiscordMCCommandBase extends PlayerCommandBase { + +} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordEntity.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordEntity.java new file mode 100755 index 0000000..6ce85f8 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordEntity.java @@ -0,0 +1,300 @@ +package buttondevteam.discordplugin.playerfaker; + +import buttondevteam.discordplugin.DiscordSenderBase; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.*; +import org.bukkit.block.PistonMoveReaction; +import org.bukkit.entity.Entity; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; +import org.bukkit.metadata.MetadataValue; +import org.bukkit.plugin.Plugin; +import org.bukkit.util.Vector; +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IUser; + +import java.util.*; + +@Getter +@Setter +@SuppressWarnings("deprecated") +public abstract class DiscordEntity extends DiscordSenderBase implements Entity { + protected DiscordEntity(IUser user, IChannel channel, int entityId, UUID uuid) { + super(user, channel); + this.entityId = entityId; + uniqueId = uuid; + } + + private HashMap metadata = new HashMap(); + + private Location location = new Location(Bukkit.getWorlds().get(0), 0, 0, 0); + private Vector velocity; + private final int entityId; + private EntityDamageEvent lastDamageCause; + private final Set scoreboardTags = new HashSet(); + private final UUID uniqueId; + + @Override + public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { + metadata.put(metadataKey, newMetadataValue); + } + + @Override + public List getMetadata(String metadataKey) { + return Arrays.asList(metadata.get(metadataKey)); // Who needs multiple data anyways + } + + @Override + public boolean hasMetadata(String metadataKey) { + return metadata.containsKey(metadataKey); + } + + @Override + public void removeMetadata(String metadataKey, Plugin owningPlugin) { + metadata.remove(metadataKey); + } + + @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 double getHeight() { + return 0; + } + + @Override + public double getWidth() { + return 0; + } + + @Override + public boolean isOnGround() { + return false; + } + + @Override + public World getWorld() { + return location.getWorld(); + } + + @Override + public boolean teleport(Location location) { + this.location = location; + return true; + } + + @Override + public boolean teleport(Location location, TeleportCause cause) { + this.location = location; + return true; + } + + @Override + public boolean teleport(Entity destination) { + this.location = destination.getLocation(); + return true; + } + + @Override + public boolean teleport(Entity destination, TeleportCause cause) { + this.location = destination.getLocation(); + return true; + } + + @Override + public List getNearbyEntities(double x, double y, double z) { + return Arrays.asList(); + } + + @Override + public int getFireTicks() { + return 0; + } + + @Override + public int getMaxFireTicks() { + return 0; + } + + @Override + public void setFireTicks(int ticks) { + } + + @Override + public void remove() { + } + + @Override + public boolean isDead() { // Impossible to kill + return false; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public Server getServer() { + return Bukkit.getServer(); + } + + @Override + public Entity getPassenger() { + return null; + } + + @Override + public boolean setPassenger(Entity passenger) { + return false; + } + + @Override + public List getPassengers() { + return Arrays.asList(); + } + + @Override + public boolean addPassenger(Entity passenger) { + return false; + } + + @Override + public boolean removePassenger(Entity passenger) { // Don't support passengers + return false; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean eject() { + return false; + } + + @Override + public float getFallDistance() { + return 0; + } + + @Override + public void setFallDistance(float distance) { + } + + @Override + public int getTicksLived() { + return 1; + } + + @Override + public void setTicksLived(int value) { + } + + @Override + public void playEffect(EntityEffect type) { + } + + @Override + public boolean isInsideVehicle() { + return false; + } + + @Override + public boolean leaveVehicle() { + return false; + } + + @Override + public Entity getVehicle() { // Don't support vehicles + return null; + } + + @Override + public void setCustomNameVisible(boolean flag) { + } + + @Override + public boolean isCustomNameVisible() { + return true; + } + + @Override + public void setGlowing(boolean flag) { + } + + @Override + public boolean isGlowing() { + return false; + } + + @Override + public void setInvulnerable(boolean flag) { + } + + @Override + public boolean isInvulnerable() { + return true; + } + + @Override + public boolean isSilent() { + return true; + } + + @Override + public void setSilent(boolean flag) { + } + + @Override + public boolean hasGravity() { + return false; + } + + @Override + public void setGravity(boolean gravity) { + } + + @Override + public int getPortalCooldown() { + return 0; + } + + @Override + public void setPortalCooldown(int cooldown) { + } + + @Override + public boolean addScoreboardTag(String tag) { + return scoreboardTags.add(tag); + } + + @Override + public boolean removeScoreboardTag(String tag) { + return scoreboardTags.remove(tag); + } + + @Override + public PistonMoveReaction getPistonMoveReaction() { + return PistonMoveReaction.IGNORE; + } + + @Override + public Entity.Spigot spigot() { + return new Entity.Spigot(); + } + +} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordFakePlayer.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordFakePlayer.java new file mode 100755 index 0000000..c5b2c5c --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordFakePlayer.java @@ -0,0 +1,716 @@ +package buttondevteam.discordplugin.playerfaker; + +import buttondevteam.discordplugin.DiscordPlugin; +import lombok.Getter; +import lombok.experimental.Delegate; +import org.bukkit.*; +import org.bukkit.advancement.Advancement; +import org.bukkit.advancement.AdvancementProgress; +import org.bukkit.conversations.Conversation; +import org.bukkit.conversations.ConversationAbandonedEvent; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.map.MapView; +import org.bukkit.permissions.PermissibleBase; +import org.bukkit.plugin.Plugin; +import org.bukkit.scoreboard.Scoreboard; +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IUser; + +import java.net.InetSocketAddress; +import java.util.*; + +public class DiscordFakePlayer extends DiscordHumanEntity implements Player { + protected DiscordFakePlayer(IUser user, IChannel channel, int entityId, UUID uuid, String mcname) { + super(user, channel, entityId, uuid); + perm = new PermissibleBase(Bukkit.getOfflinePlayer(uuid)); + name = mcname; + } + + @Delegate + private PermissibleBase perm; + + private @Getter String name; + + @Override + public EntityType getType() { + return EntityType.PLAYER; + } + + @Override + public String getCustomName() { + return user.getName(); + } + + @Override + public void setCustomName(String name) { + } + + @Override + public boolean isConversing() { + + return false; + } + + @Override + public void acceptConversationInput(String input) { + } + + @Override + public boolean beginConversation(Conversation conversation) { + return false; + } + + @Override + public void abandonConversation(Conversation conversation) { + } + + @Override + public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) { + } + + @Override + public boolean isOnline() { + return true;// Let's pretend + } + + @Override + public boolean isBanned() { + return false; + } + + @Override + public boolean isWhitelisted() { + return true; + } + + @Override + public void setWhitelisted(boolean value) { + } + + @Override + public Player getPlayer() { + return this; + } + + @Override + public long getFirstPlayed() { + return 0; + } + + @Override + public long getLastPlayed() { + return 0; + } + + @Override + public boolean hasPlayedBefore() { + return false; + } + + @Override + public Map serialize() { + return new HashMap<>(); + } + + @Override + public void sendPluginMessage(Plugin source, String channel, byte[] message) { + } + + @Override + public Set getListeningPluginChannels() { + return Collections.emptySet(); + } + + @Override + public String getDisplayName() { + return user.getDisplayName(DiscordPlugin.mainServer); + } + + @Override + public void setDisplayName(String name) { + } + + @Override + public String getPlayerListName() { + return getName(); + } + + @Override + public void setPlayerListName(String name) { + } + + @Override + public void setCompassTarget(Location loc) { + } + + @Override + public Location getCompassTarget() { + return new Location(Bukkit.getWorlds().get(0), 0, 0, 0); + } + + @Override + public InetSocketAddress getAddress() { + return null; + } + + @Override + public void sendRawMessage(String message) { + sendMessage(message); + } + + @Override + public void kickPlayer(String message) { + } + + @Override + public void chat(String msg) { + Bukkit.getPluginManager() + .callEvent(new AsyncPlayerChatEvent(true, this, msg, new HashSet<>(Bukkit.getOnlinePlayers()))); + } + + @Override + public boolean performCommand(String command) { + return Bukkit.getServer().dispatchCommand(this, command); + } + + @Override + public boolean isSneaking() { + return false; + } + + @Override + public void setSneaking(boolean sneak) { + } + + @Override + public boolean isSprinting() { + return false; + } + + @Override + public void setSprinting(boolean sprinting) { + } + + @Override + public void saveData() { + } + + @Override + public void loadData() { + } + + @Override + public void setSleepingIgnored(boolean isSleeping) { + } + + @Override + public boolean isSleepingIgnored() { + return false; + } + + @Override + public void playNote(Location loc, byte instrument, byte note) { + } + + @Override + public void playNote(Location loc, Instrument instrument, Note note) { + } + + @Override + public void playSound(Location location, Sound sound, float volume, float pitch) { + } + + @Override + public void playSound(Location location, String sound, float volume, float pitch) { + } + + @Override + public void playSound(Location location, Sound sound, SoundCategory category, float volume, float pitch) { + } + + @Override + public void playSound(Location location, String sound, SoundCategory category, float volume, float pitch) { + } + + @Override + public void stopSound(Sound sound) { + } + + @Override + public void stopSound(String sound) { + } + + @Override + public void stopSound(Sound sound, SoundCategory category) { + } + + @Override + public void stopSound(String sound, SoundCategory category) { + } + + @Override + public void playEffect(Location loc, Effect effect, int data) { + } + + @Override + public void playEffect(Location loc, Effect effect, T data) { + } + + @Override + public void sendBlockChange(Location loc, Material material, byte data) { + } + + @Override + public boolean sendChunkChange(Location loc, int sx, int sy, int sz, byte[] data) { + return false; + } + + @Override + public void sendBlockChange(Location loc, int material, byte data) { + } + + @Override + public void sendSignChange(Location loc, String[] lines) throws IllegalArgumentException { + } + + @Override + public void sendMap(MapView map) { + } + + @Override + public void updateInventory() { + } + + @Override + public void awardAchievement(@SuppressWarnings("deprecation") Achievement achievement) { + } + + @Override + public void removeAchievement(@SuppressWarnings("deprecation") Achievement achievement) { + } + + @Override + public boolean hasAchievement(@SuppressWarnings("deprecation") Achievement achievement) { + return false; + } + + @Override + public void incrementStatistic(Statistic statistic) throws IllegalArgumentException { + } + + @Override + public void decrementStatistic(Statistic statistic) throws IllegalArgumentException { + } + + @Override + public void incrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException { + + } + + @Override + public void decrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException { + + } + + @Override + public void setStatistic(Statistic statistic, int newValue) throws IllegalArgumentException { + + } + + @Override + public int getStatistic(Statistic statistic) throws IllegalArgumentException { + + return 0; + } + + @Override + public void incrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + + } + + @Override + public void decrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + + } + + @Override + public int getStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + + return 0; + } + + @Override + public void incrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException { + + } + + @Override + public void decrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException { + + } + + @Override + public void setStatistic(Statistic statistic, Material material, int newValue) throws IllegalArgumentException { + + } + + @Override + public void incrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + + } + + @Override + public void decrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + + } + + @Override + public int getStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + + return 0; + } + + @Override + public void incrementStatistic(Statistic statistic, EntityType entityType, int amount) + throws IllegalArgumentException { + + } + + @Override + public void decrementStatistic(Statistic statistic, EntityType entityType, int amount) { + + } + + @Override + public void setStatistic(Statistic statistic, EntityType entityType, int newValue) { + + } + + @Override + public void setPlayerTime(long time, boolean relative) { + + } + + @Override + public long getPlayerTime() { + + return 0; + } + + @Override + public long getPlayerTimeOffset() { + + return 0; + } + + @Override + public boolean isPlayerTimeRelative() { + + return false; + } + + @Override + public void resetPlayerTime() { + + } + + @Override + public void setPlayerWeather(WeatherType type) { + + } + + @Override + public WeatherType getPlayerWeather() { + + return null; + } + + @Override + public void resetPlayerWeather() { + + } + + @Override + public void giveExp(int amount) { + + } + + @Override + public void giveExpLevels(int amount) { + + } + + @Override + public float getExp() { + + return 0; + } + + @Override + public void setExp(float exp) { + + } + + @Override + public int getLevel() { + + return 0; + } + + @Override + public void setLevel(int level) { + + } + + @Override + public int getTotalExperience() { + + return 0; + } + + @Override + public void setTotalExperience(int exp) { + + } + + @Override + public float getExhaustion() { + + return 0; + } + + @Override + public void setExhaustion(float value) { + + } + + @Override + public float getSaturation() { + + return 0; + } + + @Override + public void setSaturation(float value) { + + } + + @Override + public int getFoodLevel() { + + return 0; + } + + @Override + public void setFoodLevel(int value) { + + } + + @Override + public Location getBedSpawnLocation() { + return null; + } + + @Override + public void setBedSpawnLocation(Location location) { + } + + @Override + public void setBedSpawnLocation(Location location, boolean force) { + } + + @Override + public boolean getAllowFlight() { + return false; + } + + @Override + public void setAllowFlight(boolean flight) { + } + + @Override + public void hidePlayer(Player player) { + } + + @Override + public void showPlayer(Player player) { + } + + @Override + public boolean canSee(Player player) { // Nobody can see them + return false; + } + + @Override + public boolean isFlying() { + return false; + } + + @Override + public void setFlying(boolean value) { + } + + @Override + public void setFlySpeed(float value) throws IllegalArgumentException { + } + + @Override + public void setWalkSpeed(float value) throws IllegalArgumentException { + } + + @Override + public float getFlySpeed() { + return 0; + } + + @Override + public float getWalkSpeed() { + return 0; + } + + @Override + public void setTexturePack(String url) { + } + + @Override + public void setResourcePack(String url) { + } + + @Override + public void setResourcePack(String url, byte[] hash) { + } + + @Override + public Scoreboard getScoreboard() { + return null; + } + + @Override + public void setScoreboard(Scoreboard scoreboard) throws IllegalArgumentException, IllegalStateException { + } + + @Override + public boolean isHealthScaled() { + return false; + } + + @Override + public void setHealthScaled(boolean scale) { + } + + @Override + public void setHealthScale(double scale) throws IllegalArgumentException { + } + + @Override + public double getHealthScale() { + return 1; + } + + @Override + public Entity getSpectatorTarget() { + return null; + } + + @Override + public void setSpectatorTarget(Entity entity) { + } + + @Override + public void sendTitle(String title, String subtitle) { + } + + @Override + public void sendTitle(String title, String subtitle, int fadeIn, int stay, int fadeOut) { + } + + @Override + public void resetTitle() { + } + + @Override + public void spawnParticle(Particle particle, Location location, int count) { + } + + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count) { + + } + + @Override + public void spawnParticle(Particle particle, Location location, int count, T data) { + + } + + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, T data) { + + } + + @Override + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, + double offsetZ) { + + } + + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, + double offsetY, double offsetZ) { + + } + + @Override + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, + double offsetZ, T data) { + + } + + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, + double offsetY, double offsetZ, T data) { + + } + + @Override + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, + double offsetZ, double extra) { + + } + + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, + double offsetY, double offsetZ, double extra) { + + } + + @Override + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, + double offsetZ, double extra, T data) { + + } + + @Override + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, + double offsetY, double offsetZ, double extra, T data) { + + } + + @Override + public AdvancementProgress getAdvancementProgress(Advancement advancement) { // TODO: Test + return null; + } + + @Override + public String getLocale() { + + return null; + } + + @Override + public Player.Spigot spigot() { + return new Player.Spigot(); + } +} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordHumanEntity.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordHumanEntity.java new file mode 100755 index 0000000..c1522f1 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordHumanEntity.java @@ -0,0 +1,167 @@ +package buttondevteam.discordplugin.playerfaker; + +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Entity; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Villager; +import org.bukkit.inventory.*; +import org.bukkit.inventory.InventoryView.Property; +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IUser; + +import java.util.UUID; + +public abstract class DiscordHumanEntity extends DiscordLivingEntity implements HumanEntity { + protected DiscordHumanEntity(IUser user, IChannel channel, int entityId, UUID uuid) { + super(user, channel, entityId, uuid); + } + + private PlayerInventory inv = new DiscordPlayerInventory(this); + + @Override + public PlayerInventory getInventory() { + return inv; + } + + private Inventory enderchest = new DiscordInventory(this); + + @Override + public Inventory getEnderChest() { + return enderchest; + } + + @Override + public MainHand getMainHand() { + return MainHand.RIGHT; + } + + @Override + public boolean setWindowProperty(Property prop, int value) { + return false; + } + + @Override + public InventoryView getOpenInventory() { // TODO: Test + return null; + } + + @Override + public InventoryView openInventory(Inventory inventory) { + return null; + } + + @Override + public InventoryView openWorkbench(Location location, boolean force) { + return null; + } + + @Override + public InventoryView openEnchanting(Location location, boolean force) { + return null; + } + + @Override + public void openInventory(InventoryView inventory) { + } + + @Override + public InventoryView openMerchant(Villager trader, boolean force) { + return null; + } + + @Override + public InventoryView openMerchant(Merchant merchant, boolean force) { + return null; + } + + @Override + public void closeInventory() { + } + + @Override + public ItemStack getItemInHand() { // TODO: Test all ItemStack methods + return null; + } + + @Override + public void setItemInHand(ItemStack item) { + } + + @Override + public ItemStack getItemOnCursor() { + return null; + } + + @Override + public void setItemOnCursor(ItemStack item) { + } + + @Override + public boolean hasCooldown(Material material) { + return false; + } + + @Override + public int getCooldown(Material material) { + return 0; + } + + @Override + public void setCooldown(Material material, int ticks) { + } + + @Override + public boolean isSleeping() { + return false; + } + + @Override + public int getSleepTicks() { + return 0; + } + + @Override + public GameMode getGameMode() { + return GameMode.SPECTATOR; + } + + @Override + public void setGameMode(GameMode mode) { + } + + @Override + public boolean isBlocking() { + return false; + } + + @Override + public boolean isHandRaised() { + return false; + } + + @Override + public int getExpToLevel() { + return 0; + } + + @Override + public Entity getShoulderEntityLeft() { + return null; + } + + @Override + public void setShoulderEntityLeft(Entity entity) { + } + + @Override + public Entity getShoulderEntityRight() { + return null; + } + + @Override + public void setShoulderEntityRight(Entity entity) { + } + +} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordInventory.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordInventory.java old mode 100644 new mode 100755 index ab22f52..a97f715 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordInventory.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordInventory.java @@ -1,110 +1,110 @@ package buttondevteam.discordplugin.playerfaker; -import lombok.Getter; -import lombok.Setter; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.entity.HumanEntity; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.stream.Collectors; 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); + public DiscordInventory(DiscordHumanEntity holder) { + this.holder = holder; + } @Override public int getSize() { - return items.length; + return 0; + } + + @Override + public int getMaxStackSize() { + return 0; + } + + @Override + public void setMaxStackSize(int size) { } @Override public String getName() { - return "Discord inventory"; + return "Player inventory"; } @Override public ItemStack getItem(int index) { - if (index >= items.length) - return emptyStack; - else - return items[index]; + return null; } @Override - public void setItem(int index, ItemStack item) { - if (index < items.length) - items[index] = item; - } - - @Override - public HashMap addItem(ItemStack... items) throws IllegalArgumentException { - return IntStream.range(0, items.length).collect(HashMap::new, (map, i) -> map.put(i, items[i]), HashMap::putAll); //Pretend that we can't add anything + public HashMap addItem(ItemStack... items) throws IllegalArgumentException { // Can't add anything + return new HashMap<>( + IntStream.range(0, items.length).mapToObj(i -> i).collect(Collectors.toMap(i -> i, i -> items[i]))); } @Override public HashMap removeItem(ItemStack... items) throws IllegalArgumentException { - return IntStream.range(0, items.length).collect(HashMap::new, (map, i) -> map.put(i, items[i]), HashMap::putAll); //Pretend that we can't add anything + return new HashMap<>( + IntStream.range(0, items.length).mapToObj(i -> i).collect(Collectors.toMap(i -> i, i -> items[i]))); } @Override public ItemStack[] getContents() { - return items; + return new ItemStack[0]; } @Override public void setContents(ItemStack[] items) throws IllegalArgumentException { - this.items = items; + if (items.length > 0) + throw new IllegalArgumentException("This inventory does not support items"); } @Override public ItemStack[] getStorageContents() { - return items; + return new ItemStack[0]; } @Override public void setStorageContents(ItemStack[] items) throws IllegalArgumentException { - this.items = items; + if (items.length > 0) + throw new IllegalArgumentException("This inventory does not support items"); } - @SuppressWarnings("deprecation") @Override public boolean contains(int materialId) { - return itemStacks.stream().anyMatch(is -> is.getType().getId() == materialId); + return false; } @Override public boolean contains(Material material) throws IllegalArgumentException { - return itemStacks.stream().anyMatch(is -> is.getType() == material); + return false; } @Override public boolean contains(ItemStack item) { - return itemStacks.stream().anyMatch(is -> is.getType() == item.getType() && is.getAmount() == item.getAmount()); + return false; } - @SuppressWarnings("deprecation") @Override public boolean contains(int materialId, int amount) { - return itemStacks.stream().anyMatch(is -> is.getType().getId() == materialId && is.getAmount() == amount); + return false; } @Override public boolean contains(Material material, int amount) throws IllegalArgumentException { - return itemStacks.stream().anyMatch(is -> is.getType() == material && is.getAmount() == amount); + return false; } @Override - public boolean contains(ItemStack item, int amount) { //Not correct implementation but whatever - return itemStacks.stream().anyMatch(is -> is.getType() == item.getType() && is.getAmount() == amount); + public boolean contains(ItemStack item, int amount) { + return false; } @Override @@ -161,48 +161,52 @@ public class DiscordInventory implements Inventory { @Override public void clear(int index) { - if (index < items.length) - items[index] = null; } @Override public void clear() { - Arrays.fill(items, null); } @Override public List getViewers() { - return Collections.emptyList(); + return new ArrayList<>(0); } @Override public String getTitle() { - return "Discord inventory"; + return "Player inventory"; } @Override public InventoryType getType() { - return InventoryType.CHEST; + return InventoryType.PLAYER; } - @Override - public InventoryHolder getHolder() { - return null; - } + private ListIterator iterator = new ArrayList(0).listIterator(); - @SuppressWarnings("NullableProblems") @Override public ListIterator iterator() { - return itemStacks.listIterator(); + return iterator; } @Override public ListIterator iterator(int index) { - return itemStacks.listIterator(index); + return iterator; } @Override public Location getLocation() { - return null; + return holder.getLocation(); + } + + @Override + public void setItem(int index, ItemStack item) { + } + + private HumanEntity holder; + + @Override + public HumanEntity getHolder() { + return holder; } } diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordLivingEntity.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordLivingEntity.java new file mode 100755 index 0000000..f261de4 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordLivingEntity.java @@ -0,0 +1,297 @@ +package buttondevteam.discordplugin.playerfaker; + +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.util.Vector; +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IUser; + +import java.util.*; + +public abstract class DiscordLivingEntity extends DiscordEntity implements LivingEntity { + + protected DiscordLivingEntity(IUser user, IChannel channel, int entityId, UUID uuid) { + super(user, channel, entityId, uuid); + } + + private @Getter EntityEquipment equipment = new DiscordEntityEquipment(this); + + @Getter + @Setter + private static class DiscordEntityEquipment implements EntityEquipment { + + private float leggingsDropChance; + private ItemStack leggings; + private float itemInOffHandDropChance; + private ItemStack itemInOffHand; + private float itemInMainHandDropChance; + private ItemStack itemInMainHand; + private float itemInHandDropChance; + private ItemStack itemInHand; + private float helmetDropChance; + private ItemStack helmet; + private float chestplateDropChance; + private ItemStack chestplate; + private float bootsDropChance; + private ItemStack boots; + private ItemStack[] armorContents = new ItemStack[0]; // TODO + private final Entity holder; + + public DiscordEntityEquipment(Entity holder) { + this.holder = holder; + } + + @Override + public void clear() { + armorContents = new ItemStack[0]; + } + } + + @Override + public AttributeInstance getAttribute(Attribute attribute) { // We don't support any attributes + return null; + } + + @Override + public void damage(double amount) { + } + + @Override + public void damage(double amount, Entity source) { + } + + @Override + public double getHealth() { + return getMaxHealth(); + } + + @Override + public void setHealth(double health) { + } + + @Override + public double getMaxHealth() { + return 100; + } + + @Override + public void setMaxHealth(double health) { + } + + @Override + public void resetMaxHealth() { + } + + @Override + public T launchProjectile(Class projectile) { + return null; + } + + @Override + public T launchProjectile(Class projectile, Vector velocity) { + return null; + } + + @Override + public double getEyeHeight() { + return 0; + } + + @Override + public double getEyeHeight(boolean ignoreSneaking) { + return 0; + } + + @Override + public Location getEyeLocation() { + return getLocation(); + } + + @Override + public List getLineOfSight(Set transparent, int maxDistance) { + return Arrays.asList(); + } + + @Override + public Block getTargetBlock(HashSet transparent, int maxDistance) { + return null; + } + + @Override + public Block getTargetBlock(Set transparent, int maxDistance) { + return null; + } + + @Override + public List getLastTwoTargetBlocks(HashSet transparent, int maxDistance) { + return Arrays.asList(); + } + + @Override + public List getLastTwoTargetBlocks(Set transparent, int maxDistance) { + return Arrays.asList(); + } + + @Override + public int getRemainingAir() { + return 100; + } + + @Override + public void setRemainingAir(int ticks) { + } + + @Override + public int getMaximumAir() { + return 100; + } + + @Override + public void setMaximumAir(int ticks) { + } + + @Override + public int getMaximumNoDamageTicks() { + return 100; + } + + @Override + public void setMaximumNoDamageTicks(int ticks) { + } + + @Override + public double getLastDamage() { + return 0; + } + + @Override + public void setLastDamage(double damage) { + } + + @Override + public int getNoDamageTicks() { + return 100; + } + + @Override + public void setNoDamageTicks(int ticks) { + } + + @Override + public Player getKiller() { + return null; + } + + @Override + public boolean addPotionEffect(PotionEffect effect) { + return false; + } + + @Override + public boolean addPotionEffect(PotionEffect effect, boolean force) { + return false; + } + + @Override + public boolean addPotionEffects(Collection effects) { + return false; + } + + @Override + public boolean hasPotionEffect(PotionEffectType type) { + return false; + } + + @Override + public PotionEffect getPotionEffect(PotionEffectType type) { + return null; + } + + @Override + public void removePotionEffect(PotionEffectType type) { + } + + @Override + public Collection getActivePotionEffects() { + return Arrays.asList(); + } + + @Override + public boolean hasLineOfSight(Entity other) { + return false; + } + + @Override + public boolean getRemoveWhenFarAway() { + return false; + } + + @Override + public void setRemoveWhenFarAway(boolean remove) { + } + + @Override + public void setCanPickupItems(boolean pickup) { + } + + @Override + public boolean getCanPickupItems() { + return false; + } + + @Override + public boolean isLeashed() { + return false; + } + + @Override + public Entity getLeashHolder() throws IllegalStateException { + throw new IllegalStateException(); + } + + @Override + public boolean setLeashHolder(Entity holder) { + return false; + } + + @Override + public boolean isGliding() { + return false; + } + + @Override + public void setGliding(boolean gliding) { + } + + @Override + public void setAI(boolean ai) { + } + + @Override + public boolean hasAI() { + return false; + } + + @Override + public void setCollidable(boolean collidable) { + } + + @Override + public boolean isCollidable() { + return false; + } + +} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordPlayerInventory.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordPlayerInventory.java new file mode 100755 index 0000000..447cbcd --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordPlayerInventory.java @@ -0,0 +1,105 @@ +package buttondevteam.discordplugin.playerfaker; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +public class DiscordPlayerInventory extends DiscordInventory implements PlayerInventory { + public DiscordPlayerInventory(DiscordHumanEntity holder) { + super(holder); + } + + @Override + public ItemStack[] getArmorContents() { + return new ItemStack[0]; + } + + @Override + public ItemStack[] getExtraContents() { + return new ItemStack[0]; + } + + @Override + public ItemStack getHelmet() { + return null; + } + + @Override + public ItemStack getChestplate() { + return null; + } + + @Override + public ItemStack getLeggings() { + return null; + } + + @Override + public ItemStack getBoots() { + return null; + } + + @Override + public void setArmorContents(ItemStack[] items) { + } + + @Override + public void setExtraContents(ItemStack[] items) { + } + + @Override + public void setHelmet(ItemStack helmet) { + } + + @Override + public void setChestplate(ItemStack chestplate) { + } + + @Override + public void setLeggings(ItemStack leggings) { + } + + @Override + public void setBoots(ItemStack boots) { + } + + @Override + public ItemStack getItemInMainHand() { + return null; + } + + @Override + public void setItemInMainHand(ItemStack item) { + } + + @Override + public ItemStack getItemInOffHand() { + return null; + } + + @Override + public void setItemInOffHand(ItemStack item) { + } + + @Override + public ItemStack getItemInHand() { + return null; + } + + @Override + public void setItemInHand(ItemStack stack) { + } + + @Override + public int getHeldItemSlot() { + return 0; + } + + @Override + public void setHeldItemSlot(int slot) { + } + + @Override + public int clear(int id, int data) { + return 0; + } +} 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 4bcf5eb..0000000 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java +++ /dev/null @@ -1,39 +0,0 @@ -package buttondevteam.discordplugin.playerfaker; - -import buttondevteam.discordplugin.DiscordSenderBase; -import buttondevteam.discordplugin.IMCPlayer; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -@RequiredArgsConstructor -public class VCMDWrapper { - @Getter //Needed to mock the player - 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) { - return createListener(player, 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 static > Object createListener(T player, Player bukkitplayer) { - String mcpackage = Bukkit.getServer().getClass().getPackage().getName(); - if (mcpackage.contains("1_12")) - return bukkitplayer == null ? new VanillaCommandListener<>(player) : new VanillaCommandListener<>(player, bukkitplayer); - else if (mcpackage.contains("1_14")) - return bukkitplayer == null ? new VanillaCommandListener14<>(player) : new VanillaCommandListener14<>(player, bukkitplayer); - else - return null; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener.java b/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener.java index c12a308..29f3a13 100755 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener.java @@ -87,7 +87,7 @@ public class VanillaCommandListener> if (!vcmd.testPermission(sender)) return true; - ICommandListener icommandlistener = (ICommandListener) sender.getVanillaCmdListener().getListener(); + ICommandListener icommandlistener = sender.getVanillaCmdListener(); String[] args = cmdstr.split(" "); args = Arrays.copyOfRange(args, 1, args.length); try { 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 fbaf958..0000000 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/VanillaCommandListener14.java +++ /dev/null @@ -1,106 +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 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(); - 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/perm/LPInjector.java b/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java deleted file mode 100644 index 802accf..0000000 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java +++ /dev/null @@ -1,240 +0,0 @@ -package buttondevteam.discordplugin.playerfaker.perm; - -import buttondevteam.core.MainPlugin; -import buttondevteam.discordplugin.DiscordConnectedPlayer; -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.dummy.DummyPermissibleBase; -import me.lucko.luckperms.bukkit.inject.permissible.LPPermissible; -import me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener; -import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.locale.message.Message; -import me.lucko.luckperms.common.model.User; -import org.bukkit.Bukkit; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerLoginEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.permissions.PermissibleBase; -import org.bukkit.permissions.PermissionAttachment; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; - -public final class LPInjector implements Listener { //Disable login event for LuckPerms - private LPBukkitPlugin plugin; - private BukkitConnectionListener connectionListener; - private Set deniedLogin; - private Field detectedCraftBukkitOfflineMode; - private Method printCraftBukkitOfflineModeError; - private Field PERMISSIBLE_BASE_ATTACHMENTS_FIELD; - private Method convertAndAddAttachments; - private Method getActive; - private Method setOldPermissible; - private Method getOldPermissible; - - public LPInjector(MainPlugin mp) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException { - LPBukkitBootstrap bs = (LPBukkitBootstrap) Bukkit.getPluginManager().getPlugin("LuckPerms"); - Field field = LPBukkitBootstrap.class.getDeclaredField("plugin"); - field.setAccessible(true); - plugin = (LPBukkitPlugin) field.get(bs); - MCChatUtils.addStaticExcludedPlugin(PlayerLoginEvent.class, "LuckPerms"); - MCChatUtils.addStaticExcludedPlugin(PlayerQuitEvent.class, "LuckPerms"); - - field = LPBukkitPlugin.class.getDeclaredField("connectionListener"); - field.setAccessible(true); - connectionListener = (BukkitConnectionListener) field.get(plugin); - field = connectionListener.getClass().getDeclaredField("deniedLogin"); - field.setAccessible(true); - //noinspection unchecked - deniedLogin = (Set) field.get(connectionListener); - field = connectionListener.getClass().getDeclaredField("detectedCraftBukkitOfflineMode"); - field.setAccessible(true); - detectedCraftBukkitOfflineMode = field; - printCraftBukkitOfflineModeError = connectionListener.getClass().getDeclaredMethod("printCraftBukkitOfflineModeError"); - printCraftBukkitOfflineModeError.setAccessible(true); - - //PERMISSIBLE_FIELD = DiscordFakePlayer.class.getDeclaredField("perm"); - //PERMISSIBLE_FIELD.setAccessible(true); //Hacking my own plugin, while we're at it - PERMISSIBLE_BASE_ATTACHMENTS_FIELD = PermissibleBase.class.getDeclaredField("attachments"); - PERMISSIBLE_BASE_ATTACHMENTS_FIELD.setAccessible(true); - - convertAndAddAttachments = LPPermissible.class.getDeclaredMethod("convertAndAddAttachments", Collection.class); - convertAndAddAttachments.setAccessible(true); - getActive = LPPermissible.class.getDeclaredMethod("getActive"); - getActive.setAccessible(true); - setOldPermissible = LPPermissible.class.getDeclaredMethod("setOldPermissible", PermissibleBase.class); - setOldPermissible.setAccessible(true); - getOldPermissible = LPPermissible.class.getDeclaredMethod("getOldPermissible"); - getOldPermissible.setAccessible(true); - - TBMCCoreAPI.RegisterEventsForExceptions(this, mp); - } - - - //Code copied from LuckPerms - me.lucko.luckperms.bukkit.listeners.BukkitConnectionListener - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerLogin(PlayerLoginEvent e) { - /* Called when the player starts logging into the server. - At this point, the users data should be present and loaded. */ - - if (!(e.getPlayer() instanceof DiscordConnectedPlayer)) - return; //Normal players must be handled by the plugin - - final DiscordConnectedPlayer player = (DiscordConnectedPlayer) e.getPlayer(); - - if (plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { - plugin.getLogger().info("Processing login for " + player.getUniqueId() + " - " + player.getName()); - } - - final User user = plugin.getUserManager().getIfLoaded(player.getUniqueId()); - - /* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */ - if (user == null) { - deniedLogin.add(player.getUniqueId()); - - if (!connectionListener.getUniqueConnections().contains(player.getUniqueId())) { - - plugin.getLogger().warn("User " + player.getUniqueId() + " - " + player.getName() + - " doesn't have data pre-loaded, they have never been processed during pre-login in this session." + - " - denying login."); - - try { - if ((Boolean) detectedCraftBukkitOfflineMode.get(connectionListener)) { - printCraftBukkitOfflineModeError.invoke(connectionListener); - e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_STATE_ERROR_CB_OFFLINE_MODE.asString(plugin.getLocaleManager())); - return; - } - } catch (IllegalAccessException | InvocationTargetException ex) { - ex.printStackTrace(); - } - - } else { - plugin.getLogger().warn("User " + player.getUniqueId() + " - " + player.getName() + - " doesn't currently have data pre-loaded, but they have been processed before in this session." + - " - denying login."); - } - - e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_STATE_ERROR.asString(plugin.getLocaleManager())); - return; - } - - // User instance is there, now we can inject our custom Permissible into the player. - // Care should be taken at this stage to ensure that async tasks which manipulate bukkit data check that the player is still online. - try { - // get the existing PermissibleBase held by the player - PermissibleBase oldPermissible = player.getPerm(); - - // Make a new permissible for the user - LPPermissible lpPermissible = new LPPermissible(player, user, plugin); - - // Inject into the player - inject(player, lpPermissible, oldPermissible); - - } catch (Throwable t) { - plugin.getLogger().warn("Exception thrown when setting up permissions for " + - player.getUniqueId() + " - " + player.getName() + " - denying login."); - t.printStackTrace(); - - e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_SETUP_ERROR.asString(plugin.getLocaleManager())); - return; - } - - plugin.refreshAutoOp(player, true); - } - - // Wait until the last priority to unload, so plugins can still perform permission checks on this event - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerQuit(PlayerQuitEvent e) { - if (!(e.getPlayer() instanceof DiscordConnectedPlayer)) - return; - - final DiscordConnectedPlayer player = (DiscordConnectedPlayer) e.getPlayer(); - - connectionListener.handleDisconnect(player.getUniqueId()); - - // perform unhooking from bukkit objects 1 tick later. - // this allows plugins listening after us on MONITOR to still have intact permissions data - this.plugin.getBootstrap().getServer().getScheduler().runTaskLaterAsynchronously(this.plugin.getBootstrap(), () -> { - // Remove the custom permissible - try { - uninject(player, true); - } catch (Exception ex) { - ex.printStackTrace(); - } - - // Handle auto op - if (this.plugin.getConfiguration().get(ConfigKeys.AUTO_OP)) { - player.setOp(false); - } - - // remove their contexts cache - this.plugin.getContextManager().onPlayerQuit(player); - }, 1L); - } - - //me.lucko.luckperms.bukkit.inject.permissible.PermissibleInjector - private void inject(DiscordConnectedPlayer player, LPPermissible newPermissible, PermissibleBase oldPermissible) throws IllegalAccessException, InvocationTargetException { - - // seems we have already injected into this player. - if (oldPermissible instanceof LPPermissible) { - throw new IllegalStateException("LPPermissible already injected into player " + player.toString()); - } - - // Move attachments over from the old permissible - - //noinspection unchecked - List attachments = (List) PERMISSIBLE_BASE_ATTACHMENTS_FIELD.get(oldPermissible); - - convertAndAddAttachments.invoke(newPermissible, attachments); - attachments.clear(); - oldPermissible.clearPermissions(); - - // Setup the new permissible - ((AtomicBoolean) getActive.invoke(newPermissible)).set(true); - setOldPermissible.invoke(newPermissible, oldPermissible); - - // inject the new instance - player.setPerm(newPermissible); - } - - private void uninject(DiscordConnectedPlayer player, boolean dummy) throws Exception { - - // gets the players current permissible. - PermissibleBase permissible = player.getPerm(); - - // only uninject if the permissible was a luckperms one. - if (permissible instanceof LPPermissible) { - LPPermissible lpPermissible = ((LPPermissible) permissible); - - // clear all permissions - lpPermissible.clearPermissions(); - - // set to inactive - ((AtomicBoolean) getActive.invoke(lpPermissible)).set(false); - - // handle the replacement permissible. - if (dummy) { - // just inject a dummy class. this is used when we know the player is about to quit the server. - player.setPerm(DummyPermissibleBase.INSTANCE); - - } else { - PermissibleBase newPb = (PermissibleBase) getOldPermissible.invoke(lpPermissible); - if (newPb == null) { - newPb = new PermissibleBase(player); - } - - player.setPerm(newPb); - } - } - } -} diff --git a/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.java b/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.java deleted file mode 100644 index d666a13..0000000 --- a/src/main/java/buttondevteam/discordplugin/role/GameRoleModule.java +++ /dev/null @@ -1,120 +0,0 @@ -package buttondevteam.discordplugin.role; - -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.ReadOnlyConfigData; -import discord4j.core.event.domain.role.RoleCreateEvent; -import discord4j.core.event.domain.role.RoleDeleteEvent; -import discord4j.core.event.domain.role.RoleEvent; -import discord4j.core.event.domain.role.RoleUpdateEvent; -import discord4j.core.object.entity.MessageChannel; -import discord4j.core.object.entity.Role; -import lombok.val; -import org.bukkit.Bukkit; -import reactor.core.publisher.Mono; - -import java.awt.*; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Automatically collects roles with a certain color (the second to last in the upper row - #95a5a6). - * Users can add these roles to themselves using the /role Discord command. - */ -public class GameRoleModule extends Component { - public List GameRoles; - - @Override - protected void enable() { - getPlugin().getManager().registerCommand(new RoleCommand(this)); - GameRoles = DiscordPlugin.mainServer.getRoles().filterWhen(r -> isGameRole(r, false)).map(Role::getName).collect(Collectors.toList()).block(); - } - - @Override - protected void disable() { - - } - - /** - * The channel where the bot logs when it detects a role change that results in a new game role or one being removed. - */ - private ReadOnlyConfigData> logChannel() { - return DPUtils.channelData(getConfig(), "logChannel"); - } - - 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(); - if (roleEvent instanceof RoleCreateEvent) { - Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> { - Role role=((RoleCreateEvent) roleEvent).getRole(); - grm.isGameRole(role, false).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 (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()) { - DPUtils.getLogger().warning("Old role not stored, cannot update game role!"); - return; - } - Role or=event.getOld().get(); - grm.isGameRole(event.getCurrent(), true).flatMap(b -> { - if (!b) { - if (GameRoles.remove(or.getName()) && logChannel != null) - return logChannel.flatMap(ch -> ch.createMessage("Removed " + or.getName() + " as a game role because it's color changed.")); - } else { - if (GameRoles.contains(or.getName()) && or.getName().equals(event.getCurrent().getName())) - return Mono.empty(); - boolean removed = GameRoles.remove(or.getName()); //Regardless of whether it was a game role - GameRoles.add(event.getCurrent().getName()); //Add it because it has no color - if (logChannel != null) { - if (removed) - return logChannel.flatMap(ch -> ch.createMessage("Changed game role from " + or.getName() + " to " + event.getCurrent().getName() + ".")); - else - return logChannel.flatMap(ch -> ch.createMessage("Added " + event.getCurrent().getName() + " as game role because it has the color of one.")); - } - } - return Mono.empty(); - }).subscribe(); - } - } - - private Mono isGameRole(Role r, boolean debugMC) { - boolean debug = debugMC && r.getName().equalsIgnoreCase("Minecraft"); - if (debug) TBMCCoreAPI.sendDebugMessage("Checking if Minecraft is a game role..."); - if (r.getGuildId().asLong() != DiscordPlugin.mainServer.getId().asLong()) { - if (debug) TBMCCoreAPI.sendDebugMessage("Not in the main server: " + r.getGuildId().asString()); - return Mono.just(false); //Only allow on the main server - } - val rc = new Color(149, 165, 166, 0); - if (debug) TBMCCoreAPI.sendDebugMessage("Game role color: " + rc + " - MC color: " + r.getColor()); - return Mono.just(r.getColor().equals(rc)) - .doAfterSuccessOrError((b, e) -> { - if (debug) TBMCCoreAPI.sendDebugMessage("1. b: " + b + " - e: " + e); - }).filter(b -> b).flatMap(b -> - DiscordPlugin.dc.getSelf().flatMap(u -> u.asMember(DiscordPlugin.mainServer.getId())) - .doAfterSuccessOrError((m, e) -> { - if (debug) TBMCCoreAPI.sendDebugMessage("2. m: " + m.getDisplayName() + " e: " + e); - }).flatMap(m -> m.hasHigherRoles(Collections.singleton(r)))) //Below one of our roles - .doAfterSuccessOrError((b, e) -> { - if (debug) TBMCCoreAPI.sendDebugMessage("3. b: " + b + " - e: " + e); - }).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 d484ef2..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); - 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); - 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.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/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/resources/plugin.yml b/src/main/resources/plugin.yml index f7ffcf1..85bbe30 100755 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,10 +1,7 @@ -name: Chroma-Discord -main: buttondevteam.discordplugin.DiscordPlugin -version: '1.0' -author: NorbiPeti -depend: [ChromaCore] -softdepend: - - Essentials -commands: - discord: -website: 'https://github.com/TBMCPlugins/DiscordPlugin' +name: DiscordPlugin +main: buttondevteam.discordplugin.DiscordPlugin +version: 1.0 +author: TBMCPlugins +depend: [] +commands: + discord: diff --git a/src/test/java/buttondevteam/DiscordPlugin/AppTest.java b/src/test/java/buttondevteam/DiscordPlugin/AppTest.java index 4b8bbb9..46ad699 100755 --- a/src/test/java/buttondevteam/DiscordPlugin/AppTest.java +++ b/src/test/java/buttondevteam/DiscordPlugin/AppTest.java @@ -1,40 +1,34 @@ -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); - } -} +package buttondevteam.DiscordPlugin; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * 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() { + assertTrue(true); + } +}