diff --git a/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.java b/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.java index cc52f5e..c51471b 100644 --- a/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.java +++ b/src/main/java/buttondevteam/discordplugin/BukkitLogWatcher.java @@ -1,6 +1,5 @@ package buttondevteam.discordplugin; -import buttondevteam.discordplugin.mcchat.MinecraftChatModule; import buttondevteam.discordplugin.util.DPState; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.Filter; diff --git a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java index fab1c02..714d347 100644 --- a/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordConnectedPlayer.java @@ -1,6 +1,5 @@ package buttondevteam.discordplugin; -import buttondevteam.discordplugin.mcchat.MinecraftChatModule; import buttondevteam.discordplugin.playerfaker.DiscordInventory; import buttondevteam.discordplugin.playerfaker.VCMDWrapper; import discord4j.core.object.entity.User; diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java b/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java index b6b8d99..40ce273 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlayer.java @@ -1,6 +1,5 @@ package buttondevteam.discordplugin; -import buttondevteam.discordplugin.mcchat.MCChatPrivate; import buttondevteam.lib.player.ChromaGamerBase; import buttondevteam.lib.player.UserClass; import discord4j.core.object.entity.User; diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java index ac3ae1c..b9e7f86 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlayerSender.java @@ -1,6 +1,5 @@ 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; diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.java b/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.java deleted file mode 100644 index b7cc116..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.java +++ /dev/null @@ -1,174 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.component.channel.Channel; -import buttondevteam.core.component.channel.ChatRoom; -import buttondevteam.discordplugin.*; -import buttondevteam.lib.TBMCSystemChatEvent; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -import buttondevteam.lib.player.TBMCPlayer; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.GuildChannel; -import discord4j.core.object.entity.channel.MessageChannel; -import discord4j.rest.util.Permission; -import lombok.RequiredArgsConstructor; -import lombok.val; -import org.bukkit.Bukkit; -import reactor.core.publisher.Mono; - -import javax.annotation.Nullable; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -@SuppressWarnings("SimplifyOptionalCallChains") //Java 11 -@CommandClass(helpText = {"Channel connect", // - "This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", // - "You need to have access to the MC channel and have manage permissions on the Discord channel.", // - "You also need to have your Minecraft account connected. In #bot use /connect .", // - "Call this command from the channel you want to use.", // - "Usage: @Bot channelcon ", // - "Use the ID (command) of the channel, for example `g` for the global chat.", // - "To remove a connection use @ChromaBot channelcon remove in the channel.", // - "Mentioning the bot is needed in this case because the / prefix only works in #bot.", // - "Invite link: " // -}) -@RequiredArgsConstructor -public class ChannelconCommand extends ICommand2DC { - private final MinecraftChatModule module; - - @Command2.Subcommand - public boolean remove(Command2DCSender sender) { - val message = sender.getMessage(); - if (checkPerms(message, null)) return true; - if (MCChatCustom.removeCustomChat(message.getChannelId())) - DPUtils.reply(message, Mono.empty(), "channel connection removed.").subscribe(); - else - DPUtils.reply(message, Mono.empty(), "this channel isn't connected.").subscribe(); - return true; - } - - @Command2.Subcommand - public boolean toggle(Command2DCSender sender, @Command2.OptionalArg String toggle) { - val message = sender.getMessage(); - if (checkPerms(message, null)) return true; - val cc = MCChatCustom.getCustomChat(message.getChannelId()); - if (cc == null) - return respond(sender, "this channel isn't connected."); - Supplier togglesString = () -> Arrays.stream(ChannelconBroadcast.values()).map(t -> t.toString().toLowerCase() + ": " + ((cc.toggles & t.flag) == 0 ? "disabled" : "enabled")).collect(Collectors.joining("\n")) - + "\n\n" + TBMCSystemChatEvent.BroadcastTarget.stream().map(target -> target.getName() + ": " + (cc.brtoggles.contains(target) ? "enabled" : "disabled")).collect(Collectors.joining("\n")); - if (toggle == null) { - DPUtils.reply(message, Mono.empty(), "toggles:\n" + togglesString.get()).subscribe(); - return true; - } - String arg = toggle.toUpperCase(); - val b = Arrays.stream(ChannelconBroadcast.values()).filter(t -> t.toString().equals(arg)).findAny(); - if (!b.isPresent()) { - val bt = TBMCSystemChatEvent.BroadcastTarget.get(arg); - if (bt == null) { - DPUtils.reply(message, Mono.empty(), "cannot find toggle. Toggles:\n" + togglesString.get()).subscribe(); - return true; - } - final boolean add; - if (add = !cc.brtoggles.contains(bt)) - cc.brtoggles.add(bt); - else - cc.brtoggles.remove(bt); - return respond(sender, "'" + bt.getName() + "' " + (add ? "en" : "dis") + "abled"); - } - //A B | F - //------- A: original - B: mask - F: new - //0 0 | 0 - //0 1 | 1 - //1 0 | 1 - //1 1 | 0 - // XOR - cc.toggles ^= b.get().flag; - DPUtils.reply(message, Mono.empty(), "'" + b.get().toString().toLowerCase() + "' " + ((cc.toggles & b.get().flag) == 0 ? "disabled" : "enabled")).subscribe(); - return true; - } - - @Command2.Subcommand - public boolean def(Command2DCSender sender, String channelID) { - val message = sender.getMessage(); - if (!module.allowCustomChat.get()) { - sender.sendMessage("channel connection is not allowed on this Minecraft server."); - return true; - } - val channel = message.getChannel().block(); - if (checkPerms(message, channel)) return true; - if (MCChatCustom.hasCustomChat(message.getChannelId())) - return respond(sender, "this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it."); - val chan = Channel.getChannels().filter(ch -> ch.ID.equalsIgnoreCase(channelID) || (Arrays.stream(ch.IDs.get()).anyMatch(cid -> cid.equalsIgnoreCase(channelID)))).findAny(); - if (!chan.isPresent()) { //TODO: Red embed that disappears over time (kinda like the highlight messages in OW) - DPUtils.reply(message, channel, "MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /.").subscribe(); - return true; - } - if (!message.getAuthor().isPresent()) return true; - val author = message.getAuthor().get(); - val dp = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class); - val chp = dp.getAs(TBMCPlayer.class); - if (chp == null) { - DPUtils.reply(message, channel, "you need to connect your Minecraft account. On the main server in " + DPUtils.botmention() + " do " + DiscordPlugin.getPrefix() + "connect ").subscribe(); - return true; - } - DiscordConnectedPlayer dcp = DiscordConnectedPlayer.create(message.getAuthor().get(), channel, chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName(), module); - //Using a fake player with no login/logout, should be fine for this event - String groupid = chan.get().getGroupID(dcp); - if (groupid == null && !(chan.get() instanceof ChatRoom)) { //ChatRooms don't allow it unless the user joins, which happens later - DPUtils.reply(message, channel, "sorry, you cannot use that Minecraft channel.").subscribe(); - return true; - } - if (chan.get() instanceof ChatRoom) { //ChatRooms don't work well - DPUtils.reply(message, channel, "chat rooms are not supported yet.").subscribe(); - return true; - } - /*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) { - DPUtils.reply(message, null, "sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm."); - return true; - }*/ //TODO: "Channel admins" that can connect channels? - MCChatCustom.addCustomChat(channel, groupid, chan.get(), author, dcp, 0, new HashSet<>()); - if (chan.get() instanceof ChatRoom) - DPUtils.reply(message, channel, "alright, connection made to the room!").subscribe(); - else - DPUtils.reply(message, channel, "alright, connection made to group `" + groupid + "`!").subscribe(); - return true; - } - - @SuppressWarnings("ConstantConditions") - private boolean checkPerms(Message message, @Nullable MessageChannel channel) { - if (channel == null) - channel = message.getChannel().block(); - if (!(channel instanceof GuildChannel)) { - DPUtils.reply(message, channel, "you can only use this command in a server!").subscribe(); - return true; - } - //noinspection OptionalGetWithoutIsPresent - var perms = ((GuildChannel) channel).getEffectivePermissions(message.getAuthor().map(User::getId).get()).block(); - if (!perms.contains(Permission.ADMINISTRATOR) && !perms.contains(Permission.MANAGE_CHANNELS)) { - DPUtils.reply(message, channel, "you need to have manage permissions for this channel!").subscribe(); - return true; - } - return false; - } - - @Override - public String[] getHelpText(Method method, Command2.Subcommand ann) { - return new String[]{ // - "Channel connect", // - "This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", // - "You need to have access to the MC channel and have manage permissions on the Discord channel.", // - "You also need to have your Minecraft account connected. In " + DPUtils.botmention() + " use " + DiscordPlugin.getPrefix() + "connect .", // - "Call this command from the channel you want to use.", // - "Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf().block()).getMention() + " channelcon ", // - "Use the ID (command) of the channel, for example `g` for the global chat.", // - "To remove a connection use @ChromaBot channelcon remove in the channel.", // - "Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix() + " prefix only works in " + DPUtils.botmention() + ".", // - "Invite link: " - }; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.scala b/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.scala new file mode 100644 index 0000000..cb1df83 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mcchat/ChannelconCommand.scala @@ -0,0 +1,183 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.core.component.channel.Channel +import buttondevteam.core.component.channel.ChatRoom +import buttondevteam.discordplugin._ +import buttondevteam.discordplugin.commands.{Command2DCSender, ICommand2DC} +import buttondevteam.lib.TBMCSystemChatEvent +import buttondevteam.lib.chat.Command2 +import buttondevteam.lib.chat.CommandClass +import buttondevteam.lib.player.TBMCPlayer +import discord4j.core.`object`.entity.Message +import discord4j.core.`object`.entity.channel.{GuildChannel, MessageChannel} +import discord4j.rest.util.{Permission, PermissionSet} +import lombok.RequiredArgsConstructor +import org.bukkit.Bukkit +import org.bukkit.command.CommandSender +import reactor.core.publisher.Mono + +import javax.annotation.Nullable +import java.lang.reflect.Method +import java.util +import java.util.{Objects, Optional} +import java.util.function.Supplier +import java.util.stream.Collectors + +@SuppressWarnings(Array("SimplifyOptionalCallChains")) //Java 11 +@CommandClass(helpText = Array(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)) true + else if (MCChatCustom.removeCustomChat(message.getChannelId)) + DPUtils.reply(message, Mono.empty, "channel connection removed.").subscribe + else + DPUtils.reply(message, Mono.empty, "this channel isn't connected.").subscribe + 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] = () => util.Arrays.stream(ChannelconBroadcast.values) + .map((t: ChannelconBroadcast) => + t.toString.toLowerCase + ": " + (if ((cc.toggles & t.flag) == 0) "disabled" else "enabled")) + .collect(Collectors.joining("\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, Mono.empty, "toggles:\n" + togglesString.get).subscribe + return true + } + val arg: String = toggle.toUpperCase + val b: Optional[ChannelconBroadcast] = util.Arrays.stream(ChannelconBroadcast.values).filter((t: ChannelconBroadcast) => t.toString == arg).findAny + if (!b.isPresent) { + val bt: TBMCSystemChatEvent.BroadcastTarget = TBMCSystemChatEvent.BroadcastTarget.get(arg) + if (bt == null) { + DPUtils.reply(message, Mono.empty, "cannot find toggle. Toggles:\n" + togglesString.get).subscribe + return true + } + val add: Boolean = !(cc.brtoggles.contains(bt)) + if (add) { + cc.brtoggles.add(bt) + } + else { + cc.brtoggles.remove(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 ^= b.get.flag + DPUtils.reply(message, Mono.empty, "'" + b.get.toString.toLowerCase + "' " + + (if ((cc.toggles & b.get.flag) == 0) "disabled" else "enabled")).subscribe + return 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, new util.HashSet[TBMCSystemChatEvent.BroadcastTarget]) + 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 + } + return 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 + } + + def getHelpText(method: Method, ann: Command2.Subcommand): Array[String] = + Array[String]( + "Channel connect", + "This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", + "You need to have access to the MC channel and have manage permissions on the Discord channel.", + "You also need to have your Minecraft account connected. In " + DPUtils.botmention + " use " + DiscordPlugin.getPrefix + "connect .", + "Call this command from the channel you want to use.", "Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf.block).getMention + " channelcon ", + "Use the ID (command) of the channel, for example `g` for the global chat.", + "To remove a connection use @ChromaBot channelcon remove in the channel.", + "Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix + " prefix only works in " + DPUtils.botmention + ".", + "Invite link: ") +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.java deleted file mode 100755 index 0f60db5..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.java +++ /dev/null @@ -1,45 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.discordplugin.DPUtils; -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.lib.chat.Command2; -import buttondevteam.lib.chat.CommandClass; -import discord4j.core.object.entity.channel.PrivateChannel; -import lombok.RequiredArgsConstructor; -import lombok.val; - -@CommandClass(helpText = { - "MC Chat", - "This command enables or disables the Minecraft chat in private messages.", // - "It can be useful if you don't want your messages to be visible, for example when talking in a private channel.", // - "You can also run all of the ingame commands you have access to using this command, if you have your accounts connected." // -}) -@RequiredArgsConstructor -public class MCChatCommand extends ICommand2DC { - - private final MinecraftChatModule module; - - @Command2.Subcommand - public boolean def(Command2DCSender sender) { - if (!module.allowPrivateChat.get()) { - sender.sendMessage("using the private chat is not allowed on this Minecraft server."); - return true; - } - val message = sender.getMessage(); - val channel = message.getChannel().block(); - @SuppressWarnings("OptionalGetWithoutIsPresent") val author = message.getAuthor().get(); - if (!(channel instanceof PrivateChannel)) { - DPUtils.reply(message, channel, "this command can only be issued in a direct message with the bot.").subscribe(); - return true; - } - final DiscordPlayer user = DiscordPlayer.getUser(author.getId().asString(), DiscordPlayer.class); - boolean mcchat = !user.isMinecraftChatEnabled(); - MCChatPrivate.privateMCChat(channel, mcchat, author, user); - DPUtils.reply(message, channel, "Minecraft chat " + (mcchat // - ? "enabled. Use '" + DiscordPlugin.getPrefix() + "mcchat' again to turn it off." // - : "disabled.")).subscribe(); - return true; - } // TODO: Pin channel switching to indicate the current channel - -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.scala b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.scala new file mode 100644 index 0000000..7b76572 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCommand.scala @@ -0,0 +1,37 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.discordplugin.{DPUtils, DiscordPlayer, DiscordPlugin} +import buttondevteam.discordplugin.commands.{Command2DCSender, ICommand2DC} +import buttondevteam.lib.chat.{Command2, CommandClass} +import buttondevteam.lib.player.ChromaGamerBase +import discord4j.core.`object`.entity.channel.PrivateChannel + +@CommandClass(helpText = Array(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/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java deleted file mode 100644 index 611d0e8..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.java +++ /dev/null @@ -1,78 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.component.channel.Channel; -import buttondevteam.core.component.channel.ChatRoom; -import buttondevteam.discordplugin.DiscordConnectedPlayer; -import buttondevteam.lib.TBMCSystemChatEvent; -import discord4j.common.util.Snowflake; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.MessageChannel; -import lombok.NonNull; -import lombok.val; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -public class MCChatCustom { - /** - * Used for town or nation chats or anything else - */ - static final ArrayList lastmsgCustom = new ArrayList<>(); - - public static void addCustomChat(MessageChannel channel, String groupid, Channel mcchannel, User user, DiscordConnectedPlayer dcp, int toggles, Set brtoggles) { - synchronized (lastmsgCustom) { - if (mcchannel instanceof ChatRoom) { - ((ChatRoom) mcchannel).joinRoom(dcp); - if (groupid == null) groupid = mcchannel.getGroupID(dcp); - } - val lmd = new CustomLMD(channel, user, groupid, mcchannel, dcp, toggles, brtoggles); - lastmsgCustom.add(lmd); - } - } - - public static boolean hasCustomChat(Snowflake channel) { - return lastmsgCustom.stream().anyMatch(lmd -> lmd.channel.getId().asLong() == channel.asLong()); - } - - @Nullable - public static CustomLMD getCustomChat(Snowflake channel) { - return lastmsgCustom.stream().filter(lmd -> lmd.channel.getId().asLong() == channel.asLong()).findAny().orElse(null); - } - - public static boolean removeCustomChat(Snowflake channel) { - synchronized (lastmsgCustom) { - MCChatUtils.lastmsgfromd.remove(channel.asLong()); - return lastmsgCustom.removeIf(lmd -> { - if (lmd.channel.getId().asLong() != channel.asLong()) - return false; - if (lmd.mcchannel instanceof ChatRoom) - ((ChatRoom) lmd.mcchannel).leaveRoom(lmd.dcp); - return true; - }); - } - } - - public static List getCustomChats() { - return Collections.unmodifiableList(lastmsgCustom); - } - - public static class CustomLMD extends MCChatUtils.LastMsgData { - public final String groupID; - public final DiscordConnectedPlayer dcp; - public int toggles; - public Set brtoggles; - - private CustomLMD(@NonNull MessageChannel channel, @NonNull User user, - @NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp, int toggles, Set brtoggles) { - super(channel, user); - groupID = groupid; - this.mcchannel = mcchannel; - this.dcp = dcp; - this.toggles = toggles; - this.brtoggles = brtoggles; - } - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.scala b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatCustom.scala new file mode 100644 index 0000000..c6fbcec --- /dev/null +++ b/src/main/java/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 lombok.NonNull + +import java.util +import java.util.Collections +import javax.annotation.Nullable + +object MCChatCustom { + /** + * Used for town or nation chats or anything else + */ + private[mcchat] val lastmsgCustom = new util.ArrayList[MCChatCustom.CustomLMD] + + def addCustomChat(channel: MessageChannel, groupid: String, mcchannel: Channel, user: User, dcp: DiscordConnectedPlayer, toggles: Int, brtoggles: util.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.add(lmd) + } + true + } + + def hasCustomChat(channel: Snowflake): Boolean = + lastmsgCustom.stream.anyMatch((lmd: MCChatCustom.CustomLMD) => lmd.channel.getId.asLong == channel.asLong) + + @Nullable def getCustomChat(channel: Snowflake): CustomLMD = + lastmsgCustom.stream.filter((lmd: MCChatCustom.CustomLMD) => lmd.channel.getId.asLong == channel.asLong).findAny.orElse(null) + + def removeCustomChat(channel: Snowflake): Boolean = { + lastmsgCustom synchronized MCChatUtils.lastmsgfromd.remove(channel.asLong) + lastmsgCustom.removeIf((lmd: MCChatCustom.CustomLMD) => { + def foo(lmd: MCChatCustom.CustomLMD): Boolean = { + if (lmd.channel.getId.asLong != channel.asLong) return false + lmd.mcchannel match { + case room: ChatRoom => room.leaveRoom(lmd.dcp) + case _ => + } + true + } + + foo(lmd) + }) + } + + def getCustomChats: util.List[CustomLMD] = Collections.unmodifiableList(lastmsgCustom) + + class CustomLMD private(@NonNull channel: MessageChannel, @NonNull user: User, val groupID: String, + @NonNull val mcchannel: Channel, val dcp: DiscordConnectedPlayer, var toggles: Int, + var brtoggles: Set[TBMCSystemChatEvent.BroadcastTarget]) extends MCChatUtils.LastMsgData(channel, user) { + } + +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java deleted file mode 100755 index 3c257ad..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.java +++ /dev/null @@ -1,416 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.ComponentManager; -import buttondevteam.discordplugin.*; -import buttondevteam.discordplugin.listeners.CommandListener; -import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; -import buttondevteam.discordplugin.playerfaker.VanillaCommandListener14; -import buttondevteam.discordplugin.playerfaker.VanillaCommandListener15; -import buttondevteam.discordplugin.util.Timings; -import buttondevteam.lib.*; -import buttondevteam.lib.chat.ChatMessage; -import buttondevteam.lib.chat.TBMCChatAPI; -import buttondevteam.lib.player.TBMCPlayer; -import com.vdurmont.emoji.EmojiParser; -import discord4j.common.util.Snowflake; -import discord4j.core.event.domain.message.MessageCreateEvent; -import discord4j.core.object.Embed; -import discord4j.core.object.entity.Attachment; -import discord4j.core.object.entity.Guild; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.GuildChannel; -import discord4j.core.object.entity.channel.PrivateChannel; -import discord4j.core.spec.EmbedCreateSpec; -import discord4j.rest.util.Color; -import lombok.val; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.scheduler.BukkitTask; -import reactor.core.publisher.Mono; - -import java.time.Instant; -import java.util.AbstractMap; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeoutException; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -public class MCChatListener implements Listener { - private BukkitTask sendtask; - private final LinkedBlockingQueue> sendevents = new LinkedBlockingQueue<>(); - private Runnable sendrunnable; - private Thread sendthread; - private final MinecraftChatModule module; - private boolean stop = false; //A new instance will be created on enable - - public MCChatListener(MinecraftChatModule minecraftChatModule) { - module = minecraftChatModule; - } - - @EventHandler // Minecraft - public void onMCChat(TBMCChatEvent ev) { - if (!ComponentManager.isEnabled(MinecraftChatModule.class) || ev.isCancelled()) //SafeMode: Needed so it doesn't restart after server shutdown - return; - sendevents.add(new AbstractMap.SimpleEntry<>(ev, Instant.now())); - if (sendtask != null) - return; - sendrunnable = () -> { - sendthread = Thread.currentThread(); - processMCToDiscord(); - if (DiscordPlugin.plugin.isEnabled() && !stop) //Don't run again if shutting down - sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); - }; - sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable); - } - - private void processMCToDiscord() { - try { - TBMCChatEvent e; - Instant time; - val se = sendevents.take(); // Wait until an element is available - e = se.getKey(); - time = se.getValue(); - - final String authorPlayer = "[" + DPUtils.sanitizeStringNoEscape(e.getChannel().DisplayName.get()) + "] " // - + ("Minecraft".equals(e.getOrigin()) ? "" : "[" + e.getOrigin().charAt(0) + "]") // - + (DPUtils.sanitizeStringNoEscape(ChromaUtils.getDisplayName(e.getSender()))); - val color = e.getChannel().Color.get(); - final Consumer embed = ecs -> { - ecs.setDescription(e.getMessage()).setColor(Color.of(color.getRed(), - color.getGreen(), color.getBlue())); - String url = module.profileURL.get(); - if (e.getSender() instanceof Player) - DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(), - url.length() > 0 ? url + "?type=minecraft&id=" - + ((Player) e.getSender()).getUniqueId() : null); - else if (e.getSender() instanceof DiscordSenderBase) - ecs.setAuthor(authorPlayer, url.length() > 0 ? url + "?type=discord&id=" - + ((DiscordSenderBase) e.getSender()).getUser().getId().asString() : null, - ((DiscordSenderBase) e.getSender()).getUser().getAvatarUrl()); - else - DPUtils.embedWithHead(ecs, authorPlayer, e.getSender().getName(), null); - ecs.setTimestamp(time); - }; - final long nanoTime = System.nanoTime(); - InterruptibleConsumer doit = lastmsgdata -> { - if (lastmsgdata.message == null - || !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().map(Embed.Author::getName).orElse(null)) - || lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120 - || !lastmsgdata.mcchannel.ID.equals(e.getChannel().ID) - || lastmsgdata.content.length() + e.getMessage().length() + 1 > 2048) { - lastmsgdata.message = lastmsgdata.channel.createEmbed(embed).block(); - lastmsgdata.time = nanoTime; - lastmsgdata.mcchannel = e.getChannel(); - lastmsgdata.content = e.getMessage(); - } else { - lastmsgdata.content = lastmsgdata.content + "\n" - + e.getMessage(); // The message object doesn't get updated - lastmsgdata.message.edit(mes -> mes.setEmbed(embed.andThen(ecs -> - ecs.setDescription(lastmsgdata.content)))).block(); - } - }; - // Checks if the given channel is different than where the message was sent from - // Or if it was from MC - Predicate isdifferentchannel = id -> !(e.getSender() instanceof DiscordSenderBase) - || ((DiscordSenderBase) e.getSender()).getChannel().getId().asLong() != id.asLong(); - - if (e.getChannel().isGlobal() - && (e.isFromCommand() || isdifferentchannel.test(module.chatChannel.get()))) - doit.accept(MCChatUtils.lastmsgdata == null - ? MCChatUtils.lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannelMono().block(), null) - : MCChatUtils.lastmsgdata); - - for (MCChatUtils.LastMsgData data : MCChatPrivate.lastmsgPerUser) { - if ((e.isFromCommand() || isdifferentchannel.test(data.channel.getId())) - && e.shouldSendTo(MCChatUtils.getSender(data.channel.getId(), data.user))) - doit.accept(data); - } - - synchronized (MCChatCustom.lastmsgCustom) { - val iterator = MCChatCustom.lastmsgCustom.iterator(); - while (iterator.hasNext()) { - val lmd = iterator.next(); - if ((e.isFromCommand() || isdifferentchannel.test(lmd.channel.getId())) //Test if msg is from Discord - && e.getChannel().ID.equals(lmd.mcchannel.ID) //If it's from a command, the command msg has been deleted, so we need to send it - && e.getGroupID().equals(lmd.groupID)) { //Check if this is the group we want to test - #58 - if (e.shouldSendTo(lmd.dcp)) //Check original user's permissions - doit.accept(lmd); - else { - iterator.remove(); //If the user no longer has permission, remove the connection - lmd.channel.createMessage("The user no longer has permission to view the channel, connection removed.").subscribe(); - } - } - } - } - } catch (InterruptedException ex) { //Stop if interrupted anywhere - sendtask.cancel(); - sendtask = null; - } catch (Exception ex) { - TBMCCoreAPI.SendException("Error while sending message to Discord!", ex, module); - } - } - - @EventHandler - public void onChatPreprocess(TBMCChatPreprocessEvent event) { - int start = -1; - while ((start = event.getMessage().indexOf('@', start + 1)) != -1) { - int mid = event.getMessage().indexOf('#', start + 1); - if (mid == -1) - return; - int end_ = event.getMessage().indexOf(' ', mid + 1); - if (end_ == -1) - end_ = event.getMessage().length(); - final int end = end_; - final int startF = start; - val user = DiscordPlugin.dc.getUsers().filter(u -> u.getUsername().equals(event.getMessage().substring(startF + 1, mid))) - .filter(u -> u.getDiscriminator().equals(event.getMessage().substring(mid + 1, end))).blockFirst(); - if (user != null) //TODO: Nicknames - event.setMessage(event.getMessage().substring(0, startF) + "@" + user.getUsername() - + (event.getMessage().length() > end ? event.getMessage().substring(end) : "")); // TODO: Add formatting - start = end; // Skip any @s inside the mention - } - } - - // ......................DiscordSender....DiscordConnectedPlayer.DiscordPlayerSender - // Offline public chat......x............................................ - // Online public chat.......x...........................................x - // Offline private chat.....x.......................x.................... - // Online private chat......x.......................x...................x - // If online and enabling private chat, don't login - // If leaving the server and private chat is enabled (has ConnectedPlayer), call login in a task on lowest priority - // If private chat is enabled and joining the server, logout the fake player on highest priority - // If online and disabling private chat, don't logout - // The maps may not contain the senders for UnconnectedSenders - - /** - * Stop the listener permanently. Enabling the module will create a new instance. - * - * @param wait Wait 5 seconds for the threads to stop - */ - public void stop(boolean wait) { - stop = true; - MCChatPrivate.logoutAll(); - MCChatUtils.LoggedInPlayers.clear(); - if (sendthread != null) sendthread.interrupt(); - if (recthread != null) recthread.interrupt(); - try { - if (sendthread != null) { - sendthread.interrupt(); - if (wait) - sendthread.join(5000); - } - if (recthread != null) { - recthread.interrupt(); - if (wait) - recthread.join(5000); - } - MCChatUtils.lastmsgdata = null; - MCChatPrivate.lastmsgPerUser.clear(); - MCChatCustom.lastmsgCustom.clear(); - MCChatUtils.lastmsgfromd.clear(); - MCChatUtils.UnconnectedSenders.clear(); - recthread = sendthread = null; - } catch (InterruptedException e) { - e.printStackTrace(); //This thread shouldn't be interrupted - } - } - - private BukkitTask rectask; - private final LinkedBlockingQueue recevents = new LinkedBlockingQueue<>(); - private Runnable recrun; - private Thread recthread; - - // Discord - public Mono handleDiscord(MessageCreateEvent ev) { - Timings timings = CommonListeners.timings; - timings.printElapsed("Chat event"); - val author = ev.getMessage().getAuthor(); - final boolean hasCustomChat = MCChatCustom.hasCustomChat(ev.getMessage().getChannelId()); - var prefix = DiscordPlugin.getPrefix(); - return ev.getMessage().getChannel().filter(channel -> { - timings.printElapsed("Filter 1"); - return !(ev.getMessage().getChannelId().asLong() != module.chatChannel.get().asLong() - && !(channel instanceof PrivateChannel - && author.map(u -> MCChatPrivate.isMinecraftChatEnabled(u.getId().asString())).orElse(false)) - && !hasCustomChat); //Chat isn't enabled on this channel - }).filter(channel -> { - timings.printElapsed("Filter 2"); - return !(channel instanceof PrivateChannel //Only in private chat - && ev.getMessage().getContent().length() < "/mcchat<>".length() - && ev.getMessage().getContent().replace(prefix + "", "") - .equalsIgnoreCase("mcchat")); //Either mcchat or /mcchat - //Allow disabling the chat if needed - }).filterWhen(channel -> CommandListener.runCommand(ev.getMessage(), DiscordPlugin.plugin.commandChannel.get(), true)) - //Allow running commands in chat channels - .filter(channel -> { - MCChatUtils.resetLastMessage(channel); - recevents.add(ev); - timings.printElapsed("Message event added"); - if (rectask != null) - return true; - recrun = () -> { //Don't return in a while loop next time - recthread = Thread.currentThread(); - processDiscordToMC(); - if (DiscordPlugin.plugin.isEnabled() && !stop) //Don't run again if shutting down - rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Continue message processing - }; - rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Start message processing - return true; - }).map(b -> false).defaultIfEmpty(true); - } - - private void processDiscordToMC() { - MessageCreateEvent event; - try { - event = recevents.take(); - } catch (InterruptedException e1) { - rectask.cancel(); - return; - } - val sender = event.getMessage().getAuthor().orElse(null); - String dmessage = event.getMessage().getContent(); - try { - final DiscordSenderBase dsender = MCChatUtils.getSender(event.getMessage().getChannelId(), sender); - val user = dsender.getChromaUser(); - - for (User u : event.getMessage().getUserMentions().toIterable()) { //TODO: Role mentions - dmessage = dmessage.replace(u.getMention(), "@" + u.getUsername()); // TODO: IG Formatting - val m = u.asMember(DiscordPlugin.mainServer.getId()).onErrorResume(t -> Mono.empty()).blockOptional(); - if (m.isPresent()) { - val mm = m.get(); - final String nick = mm.getDisplayName(); - dmessage = dmessage.replace(mm.getNicknameMention(), "@" + nick); - } - } - for (GuildChannel ch : event.getGuild().flux().flatMap(Guild::getChannels).toIterable()) { - dmessage = dmessage.replace(ch.getMention(), "#" + ch.getName()); // TODO: IG Formatting - } - - dmessage = EmojiParser.parseToAliases(dmessage, EmojiParser.FitzpatrickAction.PARSE); //Converts emoji to text- TODO: Add option to disable (resource pack?) - dmessage = dmessage.replaceAll(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:"); //Convert to Discord's format so it still shows up - - dmessage = dmessage.replaceAll("", ":$1:"); //We don't need info about the custom emojis, just display their text - - Function getChatMessage = msg -> // - msg + (event.getMessage().getAttachments().size() > 0 ? "\n" + event.getMessage() - .getAttachments().stream().map(Attachment::getUrl).collect(Collectors.joining("\n")) - : ""); - - MCChatCustom.CustomLMD clmd = MCChatCustom.getCustomChat(event.getMessage().getChannelId()); - - boolean react = false; - - val sendChannel = event.getMessage().getChannel().block(); - boolean isPrivate = sendChannel instanceof PrivateChannel; - if (dmessage.startsWith("/")) { // Ingame command - if (handleIngameCommand(event, dmessage, dsender, user, clmd, isPrivate)) return; - } else {// Not a command - react = handleIngameMessage(event, dmessage, dsender, user, getChatMessage, clmd, isPrivate); - } - if (react) { - try { - val lmfd = MCChatUtils.lastmsgfromd.get(event.getMessage().getChannelId().asLong()); - if (lmfd != null) { - lmfd.removeSelfReaction(DiscordPlugin.DELIVERED_REACTION).subscribe(); // Remove it no matter what, we know it's there 99.99% of the time - } - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while removing reactions from chat!", e, module); - } - MCChatUtils.lastmsgfromd.put(event.getMessage().getChannelId().asLong(), event.getMessage()); - event.getMessage().addReaction(DiscordPlugin.DELIVERED_REACTION).subscribe(); - } - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e, module); - } - } - - private boolean handleIngameMessage(MessageCreateEvent event, String dmessage, DiscordSenderBase dsender, DiscordPlayer user, Function getChatMessage, MCChatCustom.CustomLMD clmd, boolean isPrivate) { - boolean react = false; - if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0 - && !isPrivate && event.getMessage().getType() == Message.Type.CHANNEL_PINNED_MESSAGE) { - val rtr = clmd != null ? clmd.mcchannel.getRTR(clmd.dcp) - : dsender.getChromaUser().channel.get().getRTR(dsender); - TBMCChatAPI.SendSystemMessage(clmd != null ? clmd.mcchannel : dsender.getChromaUser().channel.get(), rtr, - (dsender instanceof Player ? ((Player) dsender).getDisplayName() - : dsender.getName()) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL); - } else { - val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(dmessage)).fromCommand(false); - if (clmd != null) - TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), clmd.mcchannel); - else - TBMCChatAPI.SendChatMessage(cmb.build()); - react = true; - } - return react; - } - - private boolean handleIngameCommand(MessageCreateEvent event, String dmessage, DiscordSenderBase dsender, DiscordPlayer user, MCChatCustom.CustomLMD clmd, boolean isPrivate) { - if (!isPrivate) - event.getMessage().delete().subscribe(); - final String cmd = dmessage.substring(1); - final String cmdlowercased = cmd.toLowerCase(); - if (dsender instanceof DiscordSender && module.whitelistedCommands().get().stream() - .noneMatch(s -> cmdlowercased.equals(s) || cmdlowercased.startsWith(s + " "))) { - // Command not whitelisted - dsender.sendMessage("Sorry, you can only access these commands from here:\n" - + module.whitelistedCommands().get().stream().map(uc -> "/" + uc) - .collect(Collectors.joining(", ")) - + (user.getConnectedID(TBMCPlayer.class) == null - ? "\nTo access your commands, first please connect your accounts, using /connect in " - + DPUtils.botmention() - + "\nThen y" - : "\nY") - + "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!"); - return true; - } - module.log(dsender.getName() + " ran from DC: /" + cmd); - if (dsender instanceof DiscordSender && runCustomCommand(dsender, cmdlowercased)) return true; - val channel = clmd == null ? user.channel.get() : clmd.mcchannel; - val ev = new TBMCCommandPreprocessEvent(dsender, channel, dmessage, clmd == null ? dsender : clmd.dcp); - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, //Commands need to be run sync - () -> { - Bukkit.getPluginManager().callEvent(ev); - if (ev.isCancelled()) - return; - try { - String mcpackage = Bukkit.getServer().getClass().getPackage().getName(); - if (!module.enableVanillaCommands.get()) - Bukkit.dispatchCommand(dsender, cmd); - else if (mcpackage.contains("1_12")) - VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd); - else if (mcpackage.contains("1_14")) - VanillaCommandListener14.runBukkitOrVanillaCommand(dsender, cmd); - else if (mcpackage.contains("1_15") || mcpackage.contains("1_16")) - VanillaCommandListener15.runBukkitOrVanillaCommand(dsender, cmd); - else - Bukkit.dispatchCommand(dsender, cmd); - } catch (NoClassDefFoundError e) { - TBMCCoreAPI.SendException("A class is not found when trying to run command " + cmd + "!", e, module); - } catch (Exception e) { - TBMCCoreAPI.SendException("An error occurred when trying to run command " + cmd + "! Vanilla commands are only supported in some MC versions.", e, module); - } - }); - return true; - } - - private boolean runCustomCommand(DiscordSenderBase dsender, String cmdlowercased) { - if (cmdlowercased.startsWith("list")) { - var players = Bukkit.getOnlinePlayers(); - dsender.sendMessage("There are " + players.stream().filter(MCChatUtils::checkEssentials).count() + " out of " + Bukkit.getMaxPlayers() + " players online."); - dsender.sendMessage("Players: " + players.stream().filter(MCChatUtils::checkEssentials) - .map(Player::getDisplayName).collect(Collectors.joining(", "))); - return true; - } - return false; - } - - @FunctionalInterface - private interface InterruptibleConsumer { - void accept(T value) throws TimeoutException, InterruptedException; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.scala b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.scala new file mode 100644 index 0000000..62eeef6 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatListener.scala @@ -0,0 +1,477 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.core.ComponentManager +import buttondevteam.core.component.channel.Channel +import buttondevteam.discordplugin._ +import buttondevteam.discordplugin.listeners.{CommandListener, CommonListeners} +import buttondevteam.discordplugin.playerfaker.{VanillaCommandListener, VanillaCommandListener14, VanillaCommandListener15} +import buttondevteam.discordplugin.util.Timings +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.{EmbedCreateSpec, MessageEditSpec} +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 java.time.Instant +import java.util +import java.util.Optional +import java.util.concurrent.{LinkedBlockingQueue, TimeoutException} +import java.util.function.{Consumer, Function, Predicate} +import java.util.stream.Collectors + +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) + } + +} + +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[EmbedCreateSpec] = (ecs: EmbedCreateSpec) => { + def foo(ecs: EmbedCreateSpec) = { + 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: MCChatListener.InterruptibleConsumer[MCChatUtils.LastMsgData] = (lastmsgdata: MCChatUtils.LastMsgData) => { + def foo(lastmsgdata: MCChatUtils.LastMsgData): Unit = { + if (lastmsgdata.message == null || !(authorPlayer == lastmsgdata.message.getEmbeds.get(0).getAuthor.map(_.getName).orElse(null)) || 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: MessageEditSpec) => mes.setEmbed(embed.andThen((ecs: EmbedCreateSpec) => ecs.setDescription(lastmsgdata.content)))).block + } + } + + foo(lastmsgdata) + } + // 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.accept(MCChatUtils.lastmsgdata) + } + + for (data <- MCChatPrivate.lastmsgPerUser) { + if ((e.isFromCommand || isdifferentchannel.test(data.channel.getId)) && e.shouldSendTo(MCChatUtils.getSender(data.channel.getId, data.user))) { + doit.accept(data) + } + } + MCChatCustom.lastmsgCustom synchronized + val iterator = MCChatCustom.lastmsgCustom.iterator + while ( { + iterator.hasNext + }) { + val lmd = iterator.next + if ((e.isFromCommand || isdifferentchannel.test(lmd.channel.getId)) //Test if msg is from Discord + && e.getChannel.ID == 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.accept(lmd) + } + else { + iterator.remove() //If the user no longer has permission, remove the connection + lmd.channel.createMessage("The user no longer has permission to view the channel, connection removed.").subscribe + } + } + } + } catch { + 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): Mono[Boolean] = { + val timings: Timings = CommonListeners.timings + timings.printElapsed("Chat event") + val author: Optional[User] = ev.getMessage.getAuthor + val hasCustomChat: Boolean = MCChatCustom.hasCustomChat(ev.getMessage.getChannelId) + val prefix: Char = DiscordPlugin.getPrefix + return ev.getMessage.getChannel.filter((channel: MessageChannel) => { + def foo(channel: MessageChannel) = { + timings.printElapsed("Filter 1") + return !((ev.getMessage.getChannelId.asLong != module.chatChannel.get.asLong && !((channel.isInstanceOf[PrivateChannel] && author.map((u: User) => MCChatPrivate.isMinecraftChatEnabled(u.getId.asString)).orElse(false))) && !(hasCustomChat))) //Chat isn't enabled on this channel + } + + foo(channel) + }).filter((channel: MessageChannel) => { + def foo(channel: MessageChannel) = { + timings.printElapsed("Filter 2") + return !((channel.isInstanceOf[PrivateChannel] //Only in private chat && ev.getMessage.getContent.length < "/mcchat<>".length && ev.getMessage.getContent.replace(prefix + "", "").equalsIgnoreCase("mcchat")))//Either mcchat or /mcchat + //Allow disabling the chat if needed + } + + foo(channel) + }).filterWhen((channel: MessageChannel) => CommandListener.runCommand(ev.getMessage, DiscordPlugin.plugin.commandChannel.get, true)).filter //Allow running commands in chat channels + ((channel: MessageChannel) => { + def foo(channel: MessageChannel) = { + MCChatUtils.resetLastMessage(channel) + recevents.add(ev) + timings.printElapsed("Message event added") + if (rectask != null) { + return true + } + recrun = () => { + def foo() = { //Don't return in a while loop next time + recthread = Thread.currentThread + processDiscordToMC() + if (DiscordPlugin.plugin.isEnabled && !(stop)) { + rectask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, recrun) //Continue message processing + } + } + + foo() + } + rectask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, recrun) //Start message processing + return true + } + + foo(channel) + }).map((b: MessageChannel) => false).defaultIfEmpty(true) + } + + private def processDiscordToMC(): Unit = { + var event: MessageCreateEvent = null + try event = recevents.take + catch { + case e1: 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 + + for (u <- event.getMessage.getUserMentions.toIterable) { //TODO: Role mentions + dmessage = dmessage.replace(u.getMention, "@" + u.getUsername) // TODO: IG Formatting + val m: Optional[Member] = u.asMember(DiscordPlugin.mainServer.getId).onErrorResume((t: Throwable) => Mono.empty).blockOptional + if (m.isPresent) { + val mm: Member = m.get + val nick: String = mm.getDisplayName + dmessage = dmessage.replace(mm.getNicknameMention, "@" + nick) + } + } + + for (ch <- event.getGuild.flux.flatMap(_.getChannels).toIterable) { + dmessage = dmessage.replace(ch.getMention, "#" + ch.getName) + } + dmessage = EmojiParser.parseToAliases(dmessage, EmojiParser.FitzpatrickAction.PARSE) //Converts emoji to text- TODO: Add option to disable (resource pack?) + dmessage = dmessage.replaceAll(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:") //Convert to Discord's format so it still shows up + dmessage = dmessage.replaceAll("", ":$1:") //We don't need info about the custom emojis, just display their text + val getChatMessage: Function[String, String] = (msg: String) => // + msg + (if (event.getMessage.getAttachments.size > 0) { + "\n" + event.getMessage.getAttachments.stream.map(_.getUrl).collect(Collectors.joining("\n")) + } + else { + "" + }) + val clmd: MCChatCustom.CustomLMD = MCChatCustom.getCustomChat(event.getMessage.getChannelId) + var react: Boolean = false + val sendChannel: MessageChannel = event.getMessage.getChannel.block + val isPrivate: Boolean = sendChannel.isInstanceOf[PrivateChannel] + if (dmessage.startsWith("/")) { // Ingame command + if (handleIngameCommand(event, dmessage, dsender, user, clmd, isPrivate)) { + return + } + } + else { // Not a command + react = handleIngameMessage(event, dmessage, dsender, user, getChatMessage, clmd, isPrivate) + } + if (react) { + try { + val lmfd: Message = 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 + } + } catch { + case e: Exception => + TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e, module) + } + } + + private def handleIngameMessage(event: MessageCreateEvent, dmessage: String, dsender: DiscordSenderBase, user: DiscordPlayer, getChatMessage: Function[String, String], clmd: MCChatCustom.CustomLMD, isPrivate: Boolean): Boolean = { + var react: Boolean = false + if (dmessage.isEmpty && event.getMessage.getAttachments.size == 0 && !(isPrivate) && (event.getMessage.getType eq Message.Type.CHANNEL_PINNED_MESSAGE)) { + val rtr: Channel.RecipientTestResult = if (clmd != null) { + clmd.mcchannel.getRTR(clmd.dcp) + } + else { + dsender.getChromaUser.channel.get.getRTR(dsender) + } + TBMCChatAPI.SendSystemMessage(if (clmd != null) clmd.mcchannel else dsender.getChromaUser.channel.get, rtr, + (dsender match { + case player: Player => + player.getDisplayName + case _ => + dsender.getName + }) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL) + } + else { + val cmb: ChatMessage.ChatMessageBuilder = ChatMessage.builder(dsender, user, getChatMessage.apply(dmessage)).fromCommand(false) + if (clmd != null) { + TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build, clmd.mcchannel) + } + else { + TBMCChatAPI.SendChatMessage(cmb.build) + } + react = true + } + return react + } + + private def handleIngameCommand(event: MessageCreateEvent, dmessage: String, dsender: DiscordSenderBase, user: DiscordPlayer, clmd: MCChatCustom.CustomLMD, isPrivate: Boolean): Boolean = { + if (!(isPrivate)) { + event.getMessage.delete.subscribe + } + val cmd: String = dmessage.substring(1) + val cmdlowercased: String = cmd.toLowerCase + if (dsender.isInstanceOf[DiscordSender] && module.whitelistedCommands.get.stream.noneMatch((s: String) => cmdlowercased == s || cmdlowercased.startsWith(s + " "))) { // Command not whitelisted + dsender.sendMessage("Sorry, you can only access these commands from here:\n" + module.whitelistedCommands.get.stream.map((uc: String) => "/" + uc).collect(Collectors.joining(", ")) + (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 true + } + module.log(dsender.getName + " ran from DC: /" + cmd) + if (dsender.isInstanceOf[DiscordSender] && runCustomCommand(dsender, cmdlowercased)) { + return true + } + val channel: Channel = if (clmd == null) { + user.channel.get + } + else { + clmd.mcchannel + } + val ev: TBMCCommandPreprocessEvent = new TBMCCommandPreprocessEvent(dsender, channel, dmessage, if (clmd == null) { + dsender + } + else { + clmd.dcp + }) + Bukkit.getScheduler.runTask(DiscordPlugin.plugin, //Commands need to be run sync + () => { + def foo(): Unit = { + Bukkit.getPluginManager.callEvent(ev) + if (ev.isCancelled) { + return + } + try { + val mcpackage: String = 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) + } + } + + foo() + }) + return true + } + + private def runCustomCommand(dsender: DiscordSenderBase, cmdlowercased: String): Boolean = { + if (cmdlowercased.startsWith("list")) { + val players: util.Collection[_ <: Player] = Bukkit.getOnlinePlayers + dsender.sendMessage("There are " + players.stream.filter(MCChatUtils.checkEssentials).count + " out of " + Bukkit.getMaxPlayers + " players online.") + dsender.sendMessage("Players: " + players.stream.filter(MCChatUtils.checkEssentials).map(Player.getDisplayName).collect(Collectors.joining(", "))) + return true + } + return false + } +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.java deleted file mode 100644 index 647aa0d..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.java +++ /dev/null @@ -1,76 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.ComponentManager; -import buttondevteam.discordplugin.DiscordConnectedPlayer; -import buttondevteam.discordplugin.DiscordPlayer; -import buttondevteam.discordplugin.DiscordPlugin; -import buttondevteam.discordplugin.DiscordSenderBase; -import buttondevteam.lib.player.TBMCPlayer; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.MessageChannel; -import discord4j.core.object.entity.channel.PrivateChannel; -import lombok.val; -import org.bukkit.Bukkit; - -import java.util.ArrayList; - -public class MCChatPrivate { - - /** - * Used for messages in PMs (mcchat). - */ - static ArrayList lastmsgPerUser = new ArrayList<>(); - - public static boolean privateMCChat(MessageChannel channel, boolean start, User user, DiscordPlayer dp) { - synchronized (MCChatUtils.ConnectedSenders) { - TBMCPlayer mcp = dp.getAs(TBMCPlayer.class); - if (mcp != null) { // If the accounts aren't connected, can't make a connected sender - val p = Bukkit.getPlayer(mcp.getUUID()); - val op = Bukkit.getOfflinePlayer(mcp.getUUID()); - val mcm = ComponentManager.getIfEnabled(MinecraftChatModule.class); - if (start) { - val sender = DiscordConnectedPlayer.create(user, channel, mcp.getUUID(), op.getName(), mcm); - MCChatUtils.addSender(MCChatUtils.ConnectedSenders, user, sender); - MCChatUtils.LoggedInPlayers.put(mcp.getUUID(), sender); - if (p == null) // Player is offline - If the player is online, that takes precedence - MCChatUtils.callLoginEvents(sender); - } else { - val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel.getId(), user); - assert sender != null; - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> { - if ((p == null || p instanceof DiscordSenderBase) // Player is offline - If the player is online, that takes precedence - && sender.isLoggedIn()) //Don't call the quit event if login failed - MCChatUtils.callLogoutEvent(sender, false); //The next line has to run *after* this one, so can't use the needsSync parameter - MCChatUtils.LoggedInPlayers.remove(sender.getUniqueId()); - sender.setLoggedIn(false); - }); - } - } // ---- PermissionsEx warning is normal on logout ---- - if (!start) - MCChatUtils.lastmsgfromd.remove(channel.getId().asLong()); - return start // - ? lastmsgPerUser.add(new MCChatUtils.LastMsgData(channel, user)) // Doesn't support group DMs - : lastmsgPerUser.removeIf(lmd -> lmd.channel.getId().asLong() == channel.getId().asLong()); - } - } - - public static boolean isMinecraftChatEnabled(DiscordPlayer dp) { - return isMinecraftChatEnabled(dp.getDiscordID()); - } - - public static boolean isMinecraftChatEnabled(String did) { // Don't load the player data just for this - return lastmsgPerUser.stream() - .anyMatch(lmd -> ((PrivateChannel) lmd.channel) - .getRecipientIds().stream().anyMatch(u -> u.asString().equals(did))); - } - - public static void logoutAll() { - synchronized (MCChatUtils.ConnectedSenders) { - for (val entry : MCChatUtils.ConnectedSenders.entrySet()) - for (val valueEntry : entry.getValue().entrySet()) - if (MCChatUtils.getSender(MCChatUtils.OnlineSenders, valueEntry.getKey(), valueEntry.getValue().getUser()) == null) //If the player is online then the fake player was already logged out - MCChatUtils.callLogoutEvent(valueEntry.getValue(), !Bukkit.isPrimaryThread()); - } - } - -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.scala b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.scala new file mode 100644 index 0000000..e2e023e --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatPrivate.scala @@ -0,0 +1,76 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.core.ComponentManager +import buttondevteam.discordplugin.{DiscordConnectedPlayer, DiscordPlayer, DiscordPlugin, DiscordSenderBase} +import buttondevteam.lib.player.TBMCPlayer +import discord4j.common.util.Snowflake +import discord4j.core.`object`.entity.User +import discord4j.core.`object`.entity.channel.{MessageChannel, PrivateChannel} +import org.bukkit.Bukkit + +import java.util + +object MCChatPrivate { + /** + * Used for messages in PMs (mcchat). + */ + private[mcchat] val lastmsgPerUser = new util.ArrayList[MCChatUtils.LastMsgData] + + 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, 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.add(new MCChatUtils.LastMsgData(channel, user)) // Doesn't support group DMs + else lastmsgPerUser.removeIf((lmd: MCChatUtils.LastMsgData) => lmd.channel.getId.asLong == channel.getId.asLong) + } + + def isMinecraftChatEnabled(dp: DiscordPlayer): Boolean = isMinecraftChatEnabled(dp.getDiscordID) + + def isMinecraftChatEnabled(did: String): Boolean = { // Don't load the player data just for this + lastmsgPerUser.stream.anyMatch((lmd: MCChatUtils.LastMsgData) => + lmd.channel.asInstanceOf[PrivateChannel].getRecipientIds.stream.anyMatch((u: Snowflake) => u.asString == did)) + } + + def logoutAll(): Unit = { + MCChatUtils.ConnectedSenders synchronized + for (entry <- MCChatUtils.ConnectedSenders.entrySet) { + for (valueEntry <- entry.getValue.entrySet) { + if (MCChatUtils.getSender(MCChatUtils.OnlineSenders, valueEntry.getKey, valueEntry.getValue.getUser) == null) { //If the player is online then the fake player was already logged out + MCChatUtils.callLogoutEvent(valueEntry.getValue, !Bukkit.isPrimaryThread) + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.java b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.java deleted file mode 100644 index 4319d3a..0000000 --- a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.java +++ /dev/null @@ -1,409 +0,0 @@ -package buttondevteam.discordplugin.mcchat; - -import buttondevteam.core.ComponentManager; -import buttondevteam.core.MainPlugin; -import buttondevteam.discordplugin.*; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.TBMCSystemChatEvent; -import com.google.common.collect.Sets; -import discord4j.common.util.Snowflake; -import discord4j.core.object.entity.Message; -import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.Channel; -import discord4j.core.object.entity.channel.MessageChannel; -import discord4j.core.object.entity.channel.PrivateChannel; -import discord4j.core.object.entity.channel.TextChannel; -import io.netty.util.collection.LongObjectHashMap; -import lombok.RequiredArgsConstructor; -import lombok.val; -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.bukkit.event.player.AsyncPlayerPreLoginEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerLoginEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.plugin.AuthorNagException; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.RegisteredListener; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; - -import javax.annotation.Nullable; -import java.net.InetAddress; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class MCChatUtils { - /** - * May contain P<DiscordID> as key for public chat - */ - public static final ConcurrentHashMap> UnconnectedSenders = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap> ConnectedSenders = new ConcurrentHashMap<>(); - /** - * May contain P<DiscordID> as key for public chat - */ - public static final ConcurrentHashMap> OnlineSenders = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap LoggedInPlayers = new ConcurrentHashMap<>(); - static @Nullable LastMsgData lastmsgdata; - static LongObjectHashMap lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks - private static MinecraftChatModule module; - private static final HashMap, HashSet> staticExcludedPlugins = new HashMap<>(); - - public static void updatePlayerList() { - val mod = getModule(); - if (mod == null || !mod.showPlayerListOnDC.get()) return; - if (lastmsgdata != null) - updatePL(lastmsgdata); - MCChatCustom.lastmsgCustom.forEach(MCChatUtils::updatePL); - } - - private static boolean notEnabled() { - return (module == null || !module.disabling) && getModule() == null; //Allow using things while disabling the module - } - - private static MinecraftChatModule getModule() { - if (module == null || !module.isEnabled()) module = ComponentManager.getIfEnabled(MinecraftChatModule.class); - //If disabled, it will try to get it again because another instance may be enabled - useful for /discord restart - return module; - } - - private static void updatePL(LastMsgData lmd) { - if (!(lmd.channel instanceof TextChannel)) { - TBMCCoreAPI.SendException("Failed to update player list for channel " + lmd.channel.getId(), - new Exception("The channel isn't a (guild) text channel."), getModule()); - return; - } - String topic = ((TextChannel) lmd.channel).getTopic().orElse(""); - if (topic.length() == 0) - topic = ".\n----\nMinecraft chat\n----\n."; - String[] s = topic.split("\\n----\\n"); - if (s.length < 3) - return; - String gid; - if (lmd instanceof MCChatCustom.CustomLMD) - gid = ((MCChatCustom.CustomLMD) lmd).groupID; - else //If we're not using a custom chat then it's either can ("everyone") or can't (null) see at most - gid = buttondevteam.core.component.channel.Channel.GROUP_EVERYONE; // (Though it's a public chat then rn) - AtomicInteger C = new AtomicInteger(); - s[s.length - 1] = "Players: " + Bukkit.getOnlinePlayers().stream() - .filter(p -> (lmd.mcchannel == null - ? gid.equals(buttondevteam.core.component.channel.Channel.GROUP_EVERYONE) //If null, allow if public (custom chats will have their channel stored anyway) - : gid.equals(lmd.mcchannel.getGroupID(p)))) //If they can see it - .filter(MCChatUtils::checkEssentials) - .filter(p -> C.incrementAndGet() > 0) //Always true - .map(p -> DPUtils.sanitizeString(p.getDisplayName())).collect(Collectors.joining(", ")); - s[0] = C + " player" + (C.get() != 1 ? "s" : "") + " online"; - ((TextChannel) lmd.channel).edit(tce -> tce.setTopic(String.join("\n----\n", s)).setReason("Player list update")).subscribe(); //Don't wait - } - - static boolean checkEssentials(Player p) { - var ess = MainPlugin.ess; - if (ess == null) return true; - return !ess.getUser(p).isHidden(); - } - - public static T addSender(ConcurrentHashMap> senders, - User user, T sender) { - return addSender(senders, user.getId().asString(), sender); - } - - public static T addSender(ConcurrentHashMap> senders, - String did, T sender) { - var map = senders.get(did); - if (map == null) - map = new ConcurrentHashMap<>(); - map.put(sender.getChannel().getId(), sender); - senders.put(did, map); - return sender; - } - - public static T getSender(ConcurrentHashMap> senders, - Snowflake channel, User user) { - var map = senders.get(user.getId().asString()); - if (map != null) - return map.get(channel); - return null; - } - - public static T removeSender(ConcurrentHashMap> senders, - Snowflake channel, User user) { - var map = senders.get(user.getId().asString()); - if (map != null) - return map.remove(channel); - return null; - } - - public static Mono forPublicPrivateChat(Function, Mono> action) { - if (notEnabled()) return Mono.empty(); - var list = new ArrayList>(); - list.add(action.apply(module.chatChannelMono())); - for (LastMsgData data : MCChatPrivate.lastmsgPerUser) - list.add(action.apply(Mono.just(data.channel))); - // lastmsgCustom.forEach(cc -> action.accept(cc.channel)); - Only send relevant messages to custom chat - return Mono.whenDelayError(list); - } - - /** - * For custom and all MC chat - * - * @param action The action to act (cannot complete empty) - * @param toggle The toggle to check - * @param hookmsg Whether the message is also sent from the hook - */ - public static Mono forCustomAndAllMCChat(Function, Mono> action, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { - if (notEnabled()) return Mono.empty(); - var list = new ArrayList>(); - if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg) - list.add(forPublicPrivateChat(action)); - final Function> customLMDFunction = cc -> action.apply(Mono.just(cc.channel)); - if (toggle == null) - MCChatCustom.lastmsgCustom.stream().map(customLMDFunction).forEach(list::add); - else - MCChatCustom.lastmsgCustom.stream().filter(cc -> (cc.toggles & toggle.flag) != 0).map(customLMDFunction).forEach(list::add); - return Mono.whenDelayError(list); - } - - /** - * Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled. - * - * @param action The action to do - * @param sender The sender to check perms of or null to send to all that has it toggled - * @param toggle The toggle to check or null to send to all allowed - */ - public static Mono forAllowedCustomMCChat(Function, Mono> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle) { - if (notEnabled()) return Mono.empty(); - Stream> st = MCChatCustom.lastmsgCustom.stream().filter(clmd -> { - //new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple - if (toggle != null && (clmd.toggles & toggle.flag) == 0) - return false; //If null then allow - if (sender == null) - return true; - return clmd.groupID.equals(clmd.mcchannel.getGroupID(sender)); - }).map(cc -> action.apply(Mono.just(cc.channel))); //TODO: Send error messages on channel connect - return Mono.whenDelayError(st::iterator); //Can't convert as an iterator or inside the stream, but I can convert it as a stream - } - - /** - * Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled. - * - * @param action The action to do - * @param sender The sender to check perms of or null to send to all that has it toggled - * @param toggle The toggle to check or null to send to all allowed - * @param hookmsg Whether the message is also sent from the hook - */ - public static Mono forAllowedCustomAndAllMCChat(Function, Mono> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { - if (notEnabled()) return Mono.empty(); - var cc = forAllowedCustomMCChat(action, sender, toggle); - if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg) - return Mono.whenDelayError(forPublicPrivateChat(action), cc); - return Mono.whenDelayError(cc); - } - - public static Function, Mono> send(String message) { - return ch -> ch.flatMap(mc -> { - resetLastMessage(mc); - return mc.createMessage(DPUtils.sanitizeString(message)); - }); - } - - public static Mono forAllowedMCChat(Function, Mono> action, TBMCSystemChatEvent event) { - if (notEnabled()) return Mono.empty(); - var list = new ArrayList>(); - if (event.getChannel().isGlobal()) - list.add(action.apply(module.chatChannelMono())); - for (LastMsgData data : MCChatPrivate.lastmsgPerUser) - if (event.shouldSendTo(getSender(data.channel.getId(), data.user))) - list.add(action.apply(Mono.just(data.channel))); //TODO: Only store ID? - MCChatCustom.lastmsgCustom.stream().filter(clmd -> { - if (!clmd.brtoggles.contains(event.getTarget())) - return false; - return event.shouldSendTo(clmd.dcp); - }).map(clmd -> action.apply(Mono.just(clmd.channel))).forEach(list::add); - return Mono.whenDelayError(list); - } - - /** - * This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc. - */ - static DiscordSenderBase getSender(Snowflake channel, final User author) { - //noinspection OptionalGetWithoutIsPresent - return Stream.>>of( // https://stackoverflow.com/a/28833677/2703239 - () -> Optional.ofNullable(getSender(OnlineSenders, channel, author)), // Find first non-null - () -> Optional.ofNullable(getSender(ConnectedSenders, channel, author)), // This doesn't support the public chat, but it'll always return null for it - () -> Optional.ofNullable(getSender(UnconnectedSenders, channel, author)), // - () -> Optional.of(addSender(UnconnectedSenders, author, - new DiscordSender(author, (MessageChannel) DiscordPlugin.dc.getChannelById(channel).block())))).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get(); - } - - /** - * Resets the last message, so it will start a new one instead of appending to it. - * This is used when someone (even the bot) sends a message to the channel. - * - * @param channel The channel to reset in - the process is slightly different for the public, private and custom chats - */ - public static void resetLastMessage(Channel channel) { - if (notEnabled()) return; - if (channel.getId().asLong() == module.chatChannel.get().asLong()) { - (lastmsgdata == null ? lastmsgdata = new LastMsgData(module.chatChannelMono().block(), null) - : lastmsgdata).message = null; - return; - } // Don't set the whole object to null, the player and channel information should be preserved - for (LastMsgData data : channel instanceof PrivateChannel ? MCChatPrivate.lastmsgPerUser : MCChatCustom.lastmsgCustom) { - if (data.channel.getId().asLong() == channel.getId().asLong()) { - data.message = null; - return; - } - } - //If it gets here, it's sending a message to a non-chat channel - } - - public static void addStaticExcludedPlugin(Class event, String plugin) { - staticExcludedPlugins.compute(event, (e, hs) -> hs == null - ? Sets.newHashSet(plugin) - : (hs.add(plugin) ? hs : hs)); - } - - public static void callEventExcludingSome(Event event) { - if (notEnabled()) return; - val second = staticExcludedPlugins.get(event.getClass()); - String[] first = module.excludedPlugins.get(); - String[] both = second == null ? first - : Arrays.copyOf(first, first.length + second.size()); - int i = first.length; - if (second != null) - for (String plugin : second) - both[i++] = plugin; - callEventExcluding(event, false, both); - } - - /** - * Calls an event with the given details. - *

- * This method only synchronizes when the event is not asynchronous. - * - * @param event Event details - * @param only Flips the operation and includes the listed plugins - * @param plugins The plugins to exclude. Not case sensitive. - */ - public static void callEventExcluding(Event event, boolean only, String... plugins) { // Copied from Spigot-API and modified a bit - if (event.isAsynchronous()) { - if (Thread.holdsLock(Bukkit.getPluginManager())) { - throw new IllegalStateException( - event.getEventName() + " cannot be triggered asynchronously from inside synchronized code."); - } - if (Bukkit.getServer().isPrimaryThread()) { - throw new IllegalStateException( - event.getEventName() + " cannot be triggered asynchronously from primary server thread."); - } - fireEventExcluding(event, only, plugins); - } else { - synchronized (Bukkit.getPluginManager()) { - fireEventExcluding(event, only, plugins); - } - } - } - - private static void fireEventExcluding(Event event, boolean only, String... plugins) { - HandlerList handlers = event.getHandlers(); // Code taken from SimplePluginManager in Spigot-API - RegisteredListener[] listeners = handlers.getRegisteredListeners(); - val server = Bukkit.getServer(); - - for (RegisteredListener registration : listeners) { - if (!registration.getPlugin().isEnabled() - || Arrays.stream(plugins).anyMatch(p -> only ^ p.equalsIgnoreCase(registration.getPlugin().getName()))) - continue; // Modified to exclude plugins - - try { - registration.callEvent(event); - } catch (AuthorNagException ex) { - Plugin plugin = registration.getPlugin(); - - if (plugin.isNaggable()) { - plugin.setNaggable(false); - - server.getLogger().log(Level.SEVERE, - String.format("Nag author(s): '%s' of '%s' about the following: %s", - plugin.getDescription().getAuthors(), plugin.getDescription().getFullName(), - ex.getMessage())); - } - } catch (Throwable ex) { - server.getLogger().log(Level.SEVERE, "Could not pass event " + event.getEventName() + " to " - + registration.getPlugin().getDescription().getFullName(), ex); - } - } - } - - /** - * Call it from an async thread. - */ - public static void callLoginEvents(DiscordConnectedPlayer dcp) { - Consumer> loginFail = kickMsg -> { - dcp.sendMessage("Minecraft chat disabled, as the login failed: " + kickMsg.get()); - MCChatPrivate.privateMCChat(dcp.getChannel(), false, dcp.getUser(), dcp.getChromaUser()); - }; //Probably also happens if the user is banned or so - val event = new AsyncPlayerPreLoginEvent(dcp.getName(), InetAddress.getLoopbackAddress(), dcp.getUniqueId()); - callEventExcludingSome(event); - if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { - loginFail.accept(event::getKickMessage); - return; - } - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> { - val ev = new PlayerLoginEvent(dcp, "localhost", InetAddress.getLoopbackAddress()); - callEventExcludingSome(ev); - if (ev.getResult() != PlayerLoginEvent.Result.ALLOWED) { - loginFail.accept(ev::getKickMessage); - return; - } - callEventExcludingSome(new PlayerJoinEvent(dcp, "")); - dcp.setLoggedIn(true); - if (module != null) { - if (module.serverWatcher != null) - module.serverWatcher.fakePlayers.add(dcp); - module.log(dcp.getName() + " (" + dcp.getUniqueId() + ") logged in from Discord"); - } - }); - } - - /** - * Only calls the events if the player is actually logged in - * - * @param dcp The player - * @param needsSync Whether we're in an async thread - */ - public static void callLogoutEvent(DiscordConnectedPlayer dcp, boolean needsSync) { - if (!dcp.isLoggedIn()) return; - val event = new PlayerQuitEvent(dcp, ""); - if (needsSync) callEventSync(event); - else callEventExcludingSome(event); - dcp.setLoggedIn(false); - if (module != null) { - module.log(dcp.getName() + " (" + dcp.getUniqueId() + ") logged out from Discord"); - if (module.serverWatcher != null) - module.serverWatcher.fakePlayers.remove(dcp); - } - } - - static void callEventSync(Event event) { - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> callEventExcludingSome(event)); - } - - @RequiredArgsConstructor - public static class LastMsgData { - public Message message; - public long time; - public String content; - public final MessageChannel channel; - public buttondevteam.core.component.channel.Channel mcchannel; - public final User user; - } -} diff --git a/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.scala b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.scala new file mode 100644 index 0000000..e06a34d --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCChatUtils.scala @@ -0,0 +1,369 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.core.{ComponentManager, MainPlugin, component} +import buttondevteam.discordplugin._ +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.TextChannelEditSpec +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 org.reactivestreams.Publisher +import reactor.core.publisher.Mono + +import java.net.InetAddress +import java.util +import java.util._ +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger +import java.util.function.Supplier +import java.util.logging.Level +import java.util.stream.{Collectors, Stream} +import javax.annotation.Nullable + +object MCChatUtils { + /** + * May contain P<DiscordID> as key for public chat + */ + val UnconnectedSenders = new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordSender]] + val ConnectedSenders = new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordConnectedPlayer]] + val OnlineSenders = new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordPlayerSender]] + val LoggedInPlayers = 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 = new util.HashMap[Class[_ <: Event], util.HashSet[String]] + + 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) = C + " player" + (if (C.get != 1) "s" else "") + " online" + lmd.channel.asInstanceOf[TextChannel].edit((tce: TextChannelEditSpec) => 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: ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, T]], user: User, sender: T): T = + addSender(senders, user.getId.asString, sender) + + def addSender[T <: DiscordSenderBase](senders: ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, T]], did: String, sender: T): T = { + var map = senders.get(did) + if (map == null) map = new ConcurrentHashMap[Snowflake, T] + map.put(sender.getChannel.getId, sender) + senders.put(did, map) + sender + } + + def getSender[T <: DiscordSenderBase](senders: ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, T]], channel: Snowflake, user: User): T = { + val map = senders.get(user.getId.asString) + if (map != null) return map.get(channel) + null + } + + def removeSender[T <: DiscordSenderBase](senders: ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, T]], channel: Snowflake, user: User): T = { + val map = senders.get(user.getId.asString) + if (map != null) return map.remove(channel) + null + } + + def forPublicPrivateChat(action: Mono[MessageChannel] => Mono[_]): Mono[_] = { + if (notEnabled) return Mono.empty + val list = new util.ArrayList[Mono[_]] + list.add(action.apply(module.chatChannelMono)) + for (data <- MCChatPrivate.lastmsgPerUser) { + list.add(action.apply(Mono.just(data.channel))) + } + // lastmsgCustom.forEach(cc -> action.accept(cc.channel)); - Only send relevant messages to custom chat + Mono.whenDelayError(list) + } + + /** + * For custom and all MC chat + * + * @param action The action to act (cannot complete empty) + * @param toggle The toggle to check + * @param hookmsg Whether the message is also sent from the hook + */ + def forCustomAndAllMCChat(action: Mono[MessageChannel] => Mono[_], @Nullable toggle: ChannelconBroadcast, hookmsg: Boolean): Mono[_] = { + if (notEnabled) return Mono.empty + val list = new util.ArrayList[Publisher[_]] + if (!GeneralEventBroadcasterModule.isHooked || !hookmsg) list.add(forPublicPrivateChat(action)) + val customLMDFunction = (cc: MCChatCustom.CustomLMD) => action.apply(Mono.just(cc.channel)) + if (toggle == null) MCChatCustom.lastmsgCustom.stream.map(customLMDFunction).forEach(list.add(_)) + else MCChatCustom.lastmsgCustom.stream.filter((cc) => (cc.toggles & toggle.flag) ne 0).map(customLMDFunction).forEach(list.add(_)) + Mono.whenDelayError(list) + } + + /** + * Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled. + * + * @param action The action to do + * @param sender The sender to check perms of or null to send to all that has it toggled + * @param toggle The toggle to check or null to send to all allowed + */ + def forAllowedCustomMCChat(action: Mono[MessageChannel] => Mono[_], @Nullable sender: CommandSender, @Nullable toggle: ChannelconBroadcast): Mono[_] = { + if (notEnabled) return Mono.empty + val st = MCChatCustom.lastmsgCustom.stream.filter((clmd) => { + def foo(clmd: CustomLMD): Boolean = { //new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple + if (toggle != null && ((clmd.toggles & toggle.flag) eq 0)) return false //If null then allow + if (sender == null) return true + clmd.groupID.equals(clmd.mcchannel.getGroupID(sender)) + } + + foo(clmd) + }).map((cc) => action.apply(Mono.just(cc.channel))) //TODO: Send error messages on channel connect + Mono.whenDelayError(st.iterator) //Can't convert as an iterator or inside the stream, but I can convert it as a stream + } + + /** + * Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled. + * + * @param action The action to do + * @param sender The sender to check perms of or null to send to all that has it toggled + * @param toggle The toggle to check or null to send to all allowed + * @param hookmsg Whether the message is also sent from the hook + */ + def forAllowedCustomAndAllMCChat(action: Mono[MessageChannel] => Mono[_], @Nullable sender: CommandSender, @Nullable toggle: ChannelconBroadcast, hookmsg: Boolean): Mono[_] = { + if (notEnabled) return Mono.empty + val cc = forAllowedCustomMCChat(action, sender, toggle) + if (!GeneralEventBroadcasterModule.isHooked || !hookmsg) return Mono.whenDelayError(forPublicPrivateChat(action), cc) + Mono.whenDelayError(cc) + } + + def send(message: String): Mono[MessageChannel] => Mono[_] = (ch: Mono[MessageChannel]) => ch.flatMap((mc: MessageChannel) => { + def foo(mc: MessageChannel) = { + resetLastMessage(mc) + mc.createMessage(DPUtils.sanitizeString(message)) + } + + foo(mc) + }) + + def forAllowedMCChat(action: Mono[MessageChannel] => Mono[_], event: TBMCSystemChatEvent): Mono[_] = { + if (notEnabled) return Mono.empty + val list = new util.ArrayList[Mono[_]] + if (event.getChannel.isGlobal) list.add(action.apply(module.chatChannelMono)) + for (data <- MCChatPrivate.lastmsgPerUser) + if (event.shouldSendTo(getSender(data.channel.getId, data.user))) list.add(action.apply(Mono.just(data.channel))) //TODO: Only store ID?} + MCChatCustom.lastmsgCustom.stream.filter((clmd) => { + def foo(clmd: CustomLMD): Boolean = { + clmd.brtoggles.contains(event.getTarget) && event.shouldSendTo(clmd.dcp) + } + + foo(clmd) + }).map((clmd) => action.apply(Mono.just(clmd.channel))).forEach(list.add(_)) + Mono.whenDelayError(list) + } + + /** + * This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc. + */ + private[mcchat] def getSender(channel: Snowflake, author: User) = { //noinspection OptionalGetWithoutIsPresent + Stream.of[Supplier[Optional[DiscordSenderBase]]]( // 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, DiscordPlugin.dc.getChannelById(channel).block.asInstanceOf[MessageChannel])))).map(_.get).filter(_.isPresent).map(_.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 + */ + 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, (e: Class[_ <: 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 == null) first + else util.Arrays.copyOf(first, first.length + second.size) + var i = first.length + if (second != null) { + for (plugin <- second) { + both({ + i += 1; + i - 1 + }) = 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. + */ + 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) { + if (!registration.getPlugin.isEnabled || util.Arrays.stream(plugins).anyMatch((p: String) => only ^ p.equalsIgnoreCase(registration.getPlugin.getName))) { + continue //todo: continue is not supported + // Modified to exclude plugins + } + 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) => { + def foo(kickMsg: String): Unit = { + dcp.sendMessage("Minecraft chat disabled, as the login failed: " + kickMsg) + MCChatPrivate.privateMCChat(dcp.getChannel, start = false, dcp.getUser, dcp.getChromaUser) + } + + foo(kickMsg) + } //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: String = null + var time = 0L + var content: String = null + var mcchannel: component.channel.Channel = null + } + +} \ No newline at end of file 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 f76c5a9..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.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::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/MCListener.scala b/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.scala new file mode 100644 index 0000000..57b1e14 --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MCListener.scala @@ -0,0 +1,143 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.discordplugin._ +import buttondevteam.lib.TBMCSystemChatEvent +import buttondevteam.lib.player.{TBMCPlayer, TBMCPlayerBase, TBMCYEEHAWEvent} +import discord4j.common.util.Snowflake +import discord4j.core.`object`.entity.channel.MessageChannel +import discord4j.core.`object`.entity.{Member, Role, User} +import net.ess3.api.events.{AfkStatusChangeEvent, MuteStatusChangeEvent, NickChangeEvent, VanishStatusChangeEvent} +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.event.{EventHandler, EventPriority, Listener} +import org.bukkit.event.entity.PlayerDeathEvent +import org.bukkit.event.player.PlayerLoginEvent.Result +import org.bukkit.event.player._ +import org.bukkit.event.server.{BroadcastMessageEvent, TabCompleteEvent} +import reactor.core.publisher.{Flux, Mono} + +import java.util.Optional + +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 != null) MCChatUtils.callLogoutEvent(dcp, 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: MessageChannel) => { + def foo(cc: MessageChannel) = { + 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 + Mono.empty + } + + foo(cc) + }))).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.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.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(DiscordPlugin.dc.getUserById(Snowflake.of(p.getDiscordID)).flatMap((user: User) => user.asMember(DiscordPlugin.mainServer.getId)).flatMap((user: Member) => role.flatMap((r: Role) => { + def foo(r: Role): Mono[_] = { + 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) => ch.createMessage(msg)) + Mono.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(Optional.of(_)).defaultIfEmpty(Optional.empty) + .flatMap((yeehaw) => MCChatUtils.forPublicPrivateChat(MCChatUtils.send(name + + yeehaw.map((guildEmoji) => " <:YEEHAW:" + guildEmoji.getId.asString + ">s").orElse(" 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) => Flux.just(m.getUsername, m.getNickname.orElse(""))) + .filter((s) => s.startsWith(token)).map((s) => "@" + s).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/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/mcchat/MinecraftChatModule.scala b/src/main/java/buttondevteam/discordplugin/mcchat/MinecraftChatModule.scala new file mode 100644 index 0000000..5b8aa3f --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/mcchat/MinecraftChatModule.scala @@ -0,0 +1,227 @@ +package buttondevteam.discordplugin.mcchat + +import buttondevteam.core.component.channel.Channel +import buttondevteam.discordplugin.{ChannelconBroadcast, DPUtils, DiscordConnectedPlayer, DiscordPlugin} +import buttondevteam.discordplugin.playerfaker.ServerWatcher +import buttondevteam.discordplugin.playerfaker.perm.LPInjector +import buttondevteam.discordplugin.util.DPState +import buttondevteam.lib.{TBMCCoreAPI, TBMCSystemChatEvent} +import buttondevteam.lib.architecture.{Component, ConfigData, 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 org.bukkit.Bukkit +import reactor.core.publisher.Mono + +import java.util +import java.util.{Objects, 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. + */ +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: Mono[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[Mono[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) { + 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) continue //todo: continue is not supported + Bukkit.getScheduler.runTask(getPlugin, () => { + def foo() = { //<-- 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.stream.map(TBMCSystemChatEvent.BroadcastTarget.get).filter(Objects.nonNull).collect(Collectors.toSet)) + } + + foo() + }) + } + } + try if (lpInjector == null) 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.stream.map(_.getName).collect(Collectors.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))), + 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((_: Throwable) => Mono.empty)), ChannelconBroadcast.RESTART, hookmsg = false).block +} \ No newline at end of file diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java index 91eb7ee..e4cd17d 100644 --- a/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java +++ b/src/main/java/buttondevteam/discordplugin/mccommands/DiscordMCCommand.java @@ -4,8 +4,6 @@ import buttondevteam.discordplugin.DPUtils; import buttondevteam.discordplugin.DiscordPlayer; import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.DiscordSenderBase; -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; diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java b/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java index 903cea3..bf3051e 100644 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/ServerWatcher.java @@ -1,6 +1,5 @@ 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; diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java b/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java index 6ff856b..7727673 100644 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/VCMDWrapper.java @@ -2,7 +2,6 @@ package buttondevteam.discordplugin.playerfaker; import buttondevteam.discordplugin.DiscordSenderBase; import buttondevteam.discordplugin.IMCPlayer; -import buttondevteam.discordplugin.mcchat.MinecraftChatModule; import buttondevteam.lib.TBMCCoreAPI; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java b/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java index 055c5e8..c13b7dd 100644 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/perm/LPInjector.java @@ -2,7 +2,6 @@ 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;