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