Converted mcchat classes to Scala

This commit is contained in:
Norbi Peti 2021-02-26 02:27:59 +01:00
parent 428361c46c
commit 9f47509dcb
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
24 changed files with 1578 additions and 1655 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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 <mcname>.", //
"Call this command from the channel you want to use.", //
"Usage: @Bot channelcon <mcchannel>", //
"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: <Unknown>" //
})
@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<String> 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 <MCname>").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 <mcname>.", //
"Call this command from the channel you want to use.", //
"Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf().block()).getMention() + " channelcon <mcchannel>", //
"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: <https://discordapp.com/oauth2/authorize?client_id=" + DiscordPlugin.dc.getApplicationInfo().map(info -> info.getId().asString()).blockOptional().orElse("Unknown") + "&scope=bot&permissions=268509264>"
};
}
}

View file

@ -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 <mcname>.",
"Call this command from the channel you want to use.", "Usage: @Bot channelcon <mcchannel>",
"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: <Unknown>" //
)))
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 <MCname>").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 <mcname>.",
"Call this command from the channel you want to use.", "Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf.block).getMention + " channelcon <mcchannel>",
"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: <https://discordapp.com/oauth2/authorize?client_id="
+ DiscordPlugin.dc.getApplicationInfo.map((info) => info.getId.asString).blockOptional.orElse("Unknown")
+ "&scope=bot&permissions=268509264>")
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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<CustomLMD> lastmsgCustom = new ArrayList<>();
public static void addCustomChat(MessageChannel channel, String groupid, Channel mcchannel, User user, DiscordConnectedPlayer dcp, int toggles, Set<TBMCSystemChatEvent.BroadcastTarget> 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<CustomLMD> 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<TBMCSystemChatEvent.BroadcastTarget> brtoggles;
private CustomLMD(@NonNull MessageChannel channel, @NonNull User user,
@NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp, int toggles, Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles) {
super(channel, user);
groupID = groupid;
this.mcchannel = mcchannel;
this.dcp = dcp;
this.toggles = toggles;
this.brtoggles = brtoggles;
}
}
}

View file

@ -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) {
}
}

View file

@ -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<AbstractMap.SimpleEntry<TBMCChatEvent, Instant>> 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<EmbedCreateSpec> 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<MCChatUtils.LastMsgData> 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<Snowflake> 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<MessageCreateEvent> recevents = new LinkedBlockingQueue<>();
private Runnable recrun;
private Thread recthread;
// Discord
public Mono<Boolean> 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("<a?:(\\S+):(\\d+)>", ":$1:"); //We don't need info about the custom emojis, just display their text
Function<String, String> 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<String, String> 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<T> {
void accept(T value) throws TimeoutException, InterruptedException;
}
}

View file

@ -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("<a?:(\\S+):(\\d+)>", ":$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
}
}

View file

@ -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<MCChatUtils.LastMsgData> 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());
}
}
}

View file

@ -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)
}
}
}
}
}

View file

@ -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&lt;DiscordID&gt; as key for public chat
*/
public static final ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, DiscordSender>> UnconnectedSenders = new ConcurrentHashMap<>();
public static final ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, DiscordConnectedPlayer>> ConnectedSenders = new ConcurrentHashMap<>();
/**
* May contain P&lt;DiscordID&gt; as key for public chat
*/
public static final ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, DiscordPlayerSender>> OnlineSenders = new ConcurrentHashMap<>();
public static final ConcurrentHashMap<UUID, DiscordConnectedPlayer> LoggedInPlayers = new ConcurrentHashMap<>();
static @Nullable LastMsgData lastmsgdata;
static LongObjectHashMap<Message> lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks
private static MinecraftChatModule module;
private static final HashMap<Class<? extends Event>, HashSet<String>> 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 extends DiscordSenderBase> T addSender(ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, T>> senders,
User user, T sender) {
return addSender(senders, user.getId().asString(), sender);
}
public static <T extends DiscordSenderBase> T addSender(ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, T>> 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 extends DiscordSenderBase> T getSender(ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, T>> senders,
Snowflake channel, User user) {
var map = senders.get(user.getId().asString());
if (map != null)
return map.get(channel);
return null;
}
public static <T extends DiscordSenderBase> T removeSender(ConcurrentHashMap<String, ConcurrentHashMap<Snowflake, T>> 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<MessageChannel>, Mono<?>> action) {
if (notEnabled()) return Mono.empty();
var list = new ArrayList<Mono<?>>();
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<MessageChannel>, Mono<?>> action, @Nullable ChannelconBroadcast toggle, boolean hookmsg) {
if (notEnabled()) return Mono.empty();
var list = new ArrayList<Publisher<?>>();
if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg)
list.add(forPublicPrivateChat(action));
final Function<MCChatCustom.CustomLMD, Publisher<?>> 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<MessageChannel>, Mono<?>> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle) {
if (notEnabled()) return Mono.empty();
Stream<Publisher<?>> 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<MessageChannel>, 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<MessageChannel>, Mono<?>> send(String message) {
return ch -> ch.flatMap(mc -> {
resetLastMessage(mc);
return mc.createMessage(DPUtils.sanitizeString(message));
});
}
public static Mono<?> forAllowedMCChat(Function<Mono<MessageChannel>, Mono<?>> action, TBMCSystemChatEvent event) {
if (notEnabled()) return Mono.empty();
var list = new ArrayList<Mono<?>>();
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.<Supplier<Optional<DiscordSenderBase>>>of( // https://stackoverflow.com/a/28833677/2703239
() -> Optional.ofNullable(getSender(OnlineSenders, channel, author)), // Find first non-null
() -> Optional.ofNullable(getSender(ConnectedSenders, channel, author)), // This doesn't support the public chat, but it'll always return null for it
() -> Optional.ofNullable(getSender(UnconnectedSenders, channel, author)), //
() -> Optional.of(addSender(UnconnectedSenders, author,
new DiscordSender(author, (MessageChannel) DiscordPlugin.dc.getChannelById(channel).block())))).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get();
}
/**
* Resets the last message, so it will start a new one instead of appending to it.
* This is used when someone (even the bot) sends a message to the channel.
*
* @param channel The channel to reset in - the process is slightly different for the public, private and custom chats
*/
public static void resetLastMessage(Channel channel) {
if (notEnabled()) return;
if (channel.getId().asLong() == module.chatChannel.get().asLong()) {
(lastmsgdata == null ? lastmsgdata = new LastMsgData(module.chatChannelMono().block(), null)
: lastmsgdata).message = null;
return;
} // Don't set the whole object to null, the player and channel information should be preserved
for (LastMsgData data : channel instanceof PrivateChannel ? MCChatPrivate.lastmsgPerUser : MCChatCustom.lastmsgCustom) {
if (data.channel.getId().asLong() == channel.getId().asLong()) {
data.message = null;
return;
}
}
//If it gets here, it's sending a message to a non-chat channel
}
public static void addStaticExcludedPlugin(Class<? extends Event> event, String plugin) {
staticExcludedPlugins.compute(event, (e, hs) -> hs == null
? Sets.newHashSet(plugin)
: (hs.add(plugin) ? hs : hs));
}
public static void callEventExcludingSome(Event event) {
if (notEnabled()) return;
val second = staticExcludedPlugins.get(event.getClass());
String[] first = module.excludedPlugins.get();
String[] both = second == null ? first
: Arrays.copyOf(first, first.length + second.size());
int i = first.length;
if (second != null)
for (String plugin : second)
both[i++] = plugin;
callEventExcluding(event, false, both);
}
/**
* Calls an event with the given details.
* <p>
* This method only synchronizes when the event is not asynchronous.
*
* @param event Event details
* @param only Flips the operation and <b>includes</b> 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<Supplier<String>> 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;
}
}

View file

@ -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&lt;DiscordID&gt; 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.
* <p>
* This method only synchronizes when the event is not asynchronous.
*
* @param event Event details
* @param only Flips the operation and <b>includes</b> 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
}
}

View file

@ -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<Mono<Role>> 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> 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);
}
}

View file

@ -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())
}
}

View file

@ -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<DiscordPlugin> {
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<ArrayList<String>> 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<Snowflake> chatChannel = DPUtils.snowflakeData(getConfig(), "chatChannel", 0L);
public Mono<MessageChannel> 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<Mono<MessageChannel>> 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<String[]> 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.<br />
* If this is off, then teleporting will have no effect.
*/
public ConfigData<Boolean> 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 <b>everything</b> 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<Boolean> showPlayerListOnDC = getConfig().getData("showPlayerListOnDC", true);
/**
* This setting controls whether custom chat connections can be <i>created</i> (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<Boolean> 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<Boolean> 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<String> profileURL = getConfig().getData("profileURL", "");
/**
* Enables support for running vanilla commands through Discord, if you ever need it.
*/
public ConfigData<Boolean> 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<Boolean> addFakePlayersToBukkit = getConfig().getData("addFakePlayersToBukkit", false);
/**
* Set by the component to report crashes.
*/
private final ConfigData<Boolean> 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();
}
}

View file

@ -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.<br />
* 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 <b>everything</b> 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 <i>created</i> (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
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;