diff --git a/Essentials/src/main/java/net/ess3/api/events/LocalChatSpyEvent.java b/Essentials/src/main/java/net/ess3/api/events/LocalChatSpyEvent.java index 9829a1291..ffcc99fcf 100644 --- a/Essentials/src/main/java/net/ess3/api/events/LocalChatSpyEvent.java +++ b/Essentials/src/main/java/net/ess3/api/events/LocalChatSpyEvent.java @@ -52,8 +52,8 @@ public class LocalChatSpyEvent extends Event implements Cancellable { } /** - * Gets the format to use to display this chat message. When this event finishes execution, the first format - * parameter is the {@link Player#getDisplayName()} and the second parameter is {@link #getMessage()} + * Gets the format to use to display this chat message to spy recipients. When this event finishes execution, the + * first format parameter is the {@link Player#getDisplayName()} and the second parameter is {@link #getMessage()} * * @return {@link String#format(String, Object...)} compatible format string */ @@ -62,8 +62,8 @@ public class LocalChatSpyEvent extends Event implements Cancellable { } /** - * Sets the format to use to display this chat message. When this event finishes execution, the first format - * parameter is the {@link Player#getDisplayName()} and the second parameter is {@link #getMessage()} + * Sets the format to use to display this chat message to spy recipients. When this event finishes execution, the + * first format parameter is the {@link Player#getDisplayName()} and the second parameter is {@link #getMessage()} * * @param format {@link String#format(String, Object...)} compatible format string * @throws IllegalFormatException if the underlying API throws the exception diff --git a/Essentials/src/main/resources/messages.properties b/Essentials/src/main/resources/messages.properties index 01ffe27fc..f2dfa4544 100644 --- a/Essentials/src/main/resources/messages.properties +++ b/Essentials/src/main/resources/messages.properties @@ -314,6 +314,7 @@ enderchestCommandUsage2=/ enderchestCommandUsage2Description=Opens the ender chest of the target player errorCallingCommand=Error calling the command /{0} errorWithMessage=\u00a7cError\:\u00a74 {0} +essChatNoSecureMsg=EssentialsX Chat version {0} does not support secure chat on this server software. Update EssentialsX, and if this issue persists, inform the developers. essentialsCommandDescription=Reloads essentials. essentialsCommandUsage=/ essentialsCommandUsage1=/ reload diff --git a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/ChatStore.java b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/ChatStore.java deleted file mode 100644 index 915aebf0e..000000000 --- a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/ChatStore.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.earth2me.essentials.chat; - -import com.earth2me.essentials.Trade; -import com.earth2me.essentials.User; -import net.ess3.api.IEssentials; - -class ChatStore { - private final User user; - private final String type; - private final Trade charge; - private long radius; - - ChatStore(final IEssentials ess, final User user, final String type) { - this.user = user; - this.type = type; - this.charge = new Trade(getLongType(), ess); - } - - User getUser() { - return user; - } - - Trade getCharge() { - return charge; - } - - String getType() { - return type; - } - - final String getLongType() { - return type.length() == 0 ? "chat" : "chat-" + type; - } - - long getRadius() { - return radius; - } - - void setRadius(final long radius) { - this.radius = radius; - } -} diff --git a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChat.java b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChat.java index e9c2c6d0d..969714aa6 100644 --- a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChat.java +++ b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChat.java @@ -1,17 +1,16 @@ package com.earth2me.essentials.chat; +import com.earth2me.essentials.Essentials; import com.earth2me.essentials.EssentialsLogger; +import com.earth2me.essentials.chat.processing.LegacyChatHandler; +import com.earth2me.essentials.chat.processing.SignedChatHandler; import com.earth2me.essentials.metrics.MetricsWrapper; import net.ess3.api.IEssentials; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; -import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import java.util.logging.Level; import static com.earth2me.essentials.I18n.tl; @@ -33,14 +32,13 @@ public class EssentialsChat extends JavaPlugin { return; } - final Map chatStore = Collections.synchronizedMap(new HashMap<>()); - - final EssentialsChatPlayerListenerLowest playerListenerLowest = new EssentialsChatPlayerListenerLowest(getServer(), ess, this, chatStore); - final EssentialsChatPlayerListenerNormal playerListenerNormal = new EssentialsChatPlayerListenerNormal(getServer(), ess, this, chatStore); - final EssentialsChatPlayerListenerHighest playerListenerHighest = new EssentialsChatPlayerListenerHighest(getServer(), ess, this, chatStore); - pluginManager.registerEvents(playerListenerLowest, this); - pluginManager.registerEvents(playerListenerNormal, this); - pluginManager.registerEvents(playerListenerHighest, this); + final SignedChatHandler signedHandler = new SignedChatHandler((Essentials) ess, this); + if (signedHandler.tryRegisterListeners()) { + getLogger().info("Secure signed chat and previews are enabled."); + } else { + final LegacyChatHandler legacyHandler = new LegacyChatHandler((Essentials) ess, this); + legacyHandler.registerListeners(); + } if (metrics == null) { metrics = new MetricsWrapper(this, 3814, false); diff --git a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayer.java b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayer.java deleted file mode 100644 index c3d991ac2..000000000 --- a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayer.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.earth2me.essentials.chat; - -import com.earth2me.essentials.ChargeException; -import com.earth2me.essentials.Trade; -import com.earth2me.essentials.User; -import net.ess3.api.IEssentials; -import org.bukkit.Server; -import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; - -import java.util.Map; - -public abstract class EssentialsChatPlayer implements Listener { - final transient IEssentials ess; - final transient EssentialsChat essChat; - final transient Server server; - private final transient Map chatStorage; - - EssentialsChatPlayer(final Server server, final IEssentials ess, final EssentialsChat essChat, final Map chatStorage) { - this.ess = ess; - this.essChat = essChat; - this.server = server; - this.chatStorage = chatStorage; - } - - public abstract void onPlayerChat(final AsyncPlayerChatEvent event); - - boolean isAborted(final AsyncPlayerChatEvent event) { - return event.isCancelled(); - } - - String getChatType(final User user, final String message) { - if (message.length() == 0) { - //Ignore empty chat events generated by plugins - return ""; - } - - final char prefix = message.charAt(0); - if (prefix == ess.getSettings().getChatShout()) { - if (user.isToggleShout()) { - return ""; - } - return message.length() > 1 ? "shout" : ""; - } else if (ess.getSettings().isChatQuestionEnabled() && prefix == ess.getSettings().getChatQuestion()) { - return message.length() > 1 ? "question" : ""; - } else if (user.isToggleShout()) { - return message.length() > 1 ? "shout" : ""; - } else { - return ""; - } - } - - ChatStore getChatStore(final AsyncPlayerChatEvent event) { - return chatStorage.get(event); - } - - void setChatStore(final AsyncPlayerChatEvent event, final ChatStore chatStore) { - chatStorage.put(event, chatStore); - } - - ChatStore delChatStore(final AsyncPlayerChatEvent event) { - return chatStorage.remove(event); - } - - private void charge(final User user, final Trade charge) throws ChargeException { - charge.charge(user); - } - - boolean charge(final AsyncPlayerChatEvent event, final ChatStore chatStore) { - try { - charge(chatStore.getUser(), chatStore.getCharge()); - } catch (final ChargeException e) { - ess.showError(chatStore.getUser().getSource(), e, "\\ chat " + chatStore.getLongType()); - event.setCancelled(true); - return false; - } - return true; - } -} diff --git a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayerListenerHighest.java b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayerListenerHighest.java deleted file mode 100644 index 863c2ed65..000000000 --- a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayerListenerHighest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.earth2me.essentials.chat; - -import net.ess3.api.IEssentials; -import org.bukkit.Server; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.player.AsyncPlayerChatEvent; - -import java.util.Map; - -public class EssentialsChatPlayerListenerHighest extends EssentialsChatPlayer { - EssentialsChatPlayerListenerHighest(final Server server, final IEssentials ess, final EssentialsChat essChat, final Map chatStorage) { - super(server, ess, essChat, chatStorage); - } - - @EventHandler(priority = EventPriority.HIGHEST) - @Override - public void onPlayerChat(final AsyncPlayerChatEvent event) { - final ChatStore chatStore = delChatStore(event); - if (isAborted(event) || chatStore == null) { - return; - } - - // This file should handle charging the user for the action before returning control back - charge(event, chatStore); - } -} diff --git a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayerListenerLowest.java b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayerListenerLowest.java deleted file mode 100644 index 8e113e8f6..000000000 --- a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayerListenerLowest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.earth2me.essentials.chat; - -import com.earth2me.essentials.User; -import com.earth2me.essentials.utils.FormatUtil; -import net.ess3.api.IEssentials; -import org.bukkit.ChatColor; -import org.bukkit.Server; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.player.AsyncPlayerChatEvent; -import org.bukkit.scoreboard.Team; - -import java.util.Locale; -import java.util.Map; - -public class EssentialsChatPlayerListenerLowest extends EssentialsChatPlayer { - EssentialsChatPlayerListenerLowest(final Server server, final IEssentials ess, final EssentialsChat essChat, final Map chatStorage) { - super(server, ess, essChat, chatStorage); - } - - @EventHandler(priority = EventPriority.LOWEST) - @Override - public void onPlayerChat(final AsyncPlayerChatEvent event) { - if (isAborted(event)) { - return; - } - - final User user = ess.getUser(event.getPlayer()); - - if (user == null) { - event.setCancelled(true); - return; - } - - final ChatStore chatStore = new ChatStore(ess, user, getChatType(user, event.getMessage())); - setChatStore(event, chatStore); - - // This listener should apply the general chat formatting only...then return control back the event handler - event.setMessage(FormatUtil.formatMessage(user, "essentials.chat", event.getMessage())); - - if (ChatColor.stripColor(event.getMessage()).length() == 0) { - event.setCancelled(true); - return; - } - - final String group = user.getGroup(); - final String world = user.getWorld().getName(); - final String username = user.getName(); - final String nickname = user.getFormattedNickname(); - - final Player player = user.getBase(); - final String prefix = FormatUtil.replaceFormat(ess.getPermissionsHandler().getPrefix(player)); - final String suffix = FormatUtil.replaceFormat(ess.getPermissionsHandler().getSuffix(player)); - final Team team = player.getScoreboard().getPlayerTeam(player); - - String format = ess.getSettings().getChatFormat(group); - format = format.replace("{0}", group); - format = format.replace("{1}", ess.getSettings().getWorldAlias(world)); - format = format.replace("{2}", world.substring(0, 1).toUpperCase(Locale.ENGLISH)); - format = format.replace("{3}", team == null ? "" : team.getPrefix()); - format = format.replace("{4}", team == null ? "" : team.getSuffix()); - format = format.replace("{5}", team == null ? "" : team.getDisplayName()); - format = format.replace("{6}", prefix); - format = format.replace("{7}", suffix); - format = format.replace("{8}", username); - format = format.replace("{9}", nickname == null ? username : nickname); - synchronized (format) { - event.setFormat(format); - } - } -} diff --git a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayerListenerNormal.java b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayerListenerNormal.java deleted file mode 100644 index 7726dbaca..000000000 --- a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/EssentialsChatPlayerListenerNormal.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.earth2me.essentials.chat; - -import com.earth2me.essentials.User; -import net.ess3.api.IEssentials; -import net.ess3.api.events.LocalChatSpyEvent; -import org.bukkit.Location; -import org.bukkit.Server; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.player.AsyncPlayerChatEvent; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.logging.Level; - -import static com.earth2me.essentials.I18n.tl; - -public class EssentialsChatPlayerListenerNormal extends EssentialsChatPlayer { - EssentialsChatPlayerListenerNormal(final Server server, final IEssentials ess, final EssentialsChat essChat, final Map chatStorage) { - super(server, ess, essChat, chatStorage); - } - - @EventHandler(priority = EventPriority.NORMAL) - @Override - public void onPlayerChat(final AsyncPlayerChatEvent event) { - if (isAborted(event)) { - return; - } - - // This file should handle detection of the local chat features; if local chat is enabled, we need to handle it here - long radius = ess.getSettings().getChatRadius(); - if (radius < 1) { - return; - } - radius *= radius; - - final ChatStore chatStore = getChatStore(event); - final User user = chatStore.getUser(); - chatStore.setRadius(radius); - - if (event.getMessage().length() > 1) { - if (chatStore.getType().isEmpty()) { - if (!user.isAuthorized("essentials.chat.local")) { - user.sendMessage(tl("notAllowedToLocal")); - event.setCancelled(true); - return; - } - - if (user.isToggleShout() && event.getMessage().length() > 1 && event.getMessage().charAt(0) == ess.getSettings().getChatShout()) { - event.setMessage(event.getMessage().substring(1)); - } - - event.getRecipients().removeIf(player -> !ess.getUser(player).isAuthorized("essentials.chat.receive.local")); - } else { - final String permission = "essentials.chat." + chatStore.getType(); - - if (user.isAuthorized(permission)) { - if (event.getMessage().charAt(0) == ess.getSettings().getChatShout() || (event.getMessage().charAt(0) == ess.getSettings().getChatQuestion() && ess.getSettings().isChatQuestionEnabled())) { - event.setMessage(event.getMessage().substring(1)); - } - event.setFormat(tl(chatStore.getType() + "Format", event.getFormat())); - event.getRecipients().removeIf(player -> !ess.getUser(player).isAuthorized("essentials.chat.receive." + chatStore.getType())); - return; - } - - user.sendMessage(tl("notAllowedTo" + chatStore.getType().substring(0, 1).toUpperCase(Locale.ENGLISH) + chatStore.getType().substring(1))); - event.setCancelled(true); - return; - } - } - - final Location loc = user.getLocation(); - final World world = loc.getWorld(); - - if (!charge(event, chatStore)) { - return; - } - - final Set outList = event.getRecipients(); - final Set spyList = new HashSet<>(); - - try { - outList.add(event.getPlayer()); - } catch (final UnsupportedOperationException ex) { - if (ess.getSettings().isDebug()) { - essChat.getLogger().log(Level.INFO, "Plugin triggered custom chat event, local chat handling aborted.", ex); - } - return; - } - - final String format = event.getFormat(); - event.setFormat(tl("chatTypeLocal").concat(event.getFormat())); - - final Iterator it = outList.iterator(); - while (it.hasNext()) { - final Player onlinePlayer = it.next(); - final User onlineUser = ess.getUser(onlinePlayer); - if (!onlineUser.equals(user)) { - boolean abort = false; - final Location playerLoc = onlineUser.getLocation(); - if (playerLoc.getWorld() != world) { - abort = true; - } else { - final double delta = playerLoc.distanceSquared(loc); - if (delta > chatStore.getRadius()) { - abort = true; - } - } - if (abort) { - if (onlineUser.isAuthorized("essentials.chat.spy")) { - spyList.add(onlinePlayer); - } - it.remove(); - } - } - } - - if (outList.size() < 2) { - user.sendMessage(tl("localNoOne")); - } - - final LocalChatSpyEvent spyEvent = new LocalChatSpyEvent(event.isAsynchronous(), event.getPlayer(), format, event.getMessage(), spyList); - server.getPluginManager().callEvent(spyEvent); - - if (!spyEvent.isCancelled()) { - for (final Player onlinePlayer : spyEvent.getRecipients()) { - onlinePlayer.sendMessage(String.format(spyEvent.getFormat(), user.getDisplayName(), spyEvent.getMessage())); - } - } - } -} diff --git a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/AbstractChatHandler.java b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/AbstractChatHandler.java new file mode 100644 index 000000000..cbb55dcc4 --- /dev/null +++ b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/AbstractChatHandler.java @@ -0,0 +1,304 @@ +package com.earth2me.essentials.chat.processing; + +import com.earth2me.essentials.ChargeException; +import com.earth2me.essentials.Essentials; +import com.earth2me.essentials.Trade; +import com.earth2me.essentials.User; +import com.earth2me.essentials.chat.EssentialsChat; +import com.earth2me.essentials.utils.FormatUtil; +import net.ess3.api.events.LocalChatSpyEvent; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.scoreboard.Team; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; +import java.util.Set; +import java.util.logging.Level; + +import static com.earth2me.essentials.I18n.tl; + +public abstract class AbstractChatHandler { + + protected final Essentials ess; + protected final EssentialsChat essChat; + protected final Server server; + protected final ChatProcessingCache cache; + + protected AbstractChatHandler(Essentials ess, EssentialsChat essChat) { + this.ess = ess; + this.essChat = essChat; + this.server = ess.getServer(); + this.cache = new ChatProcessingCache(); + } + + /** + * Apply chat formatting from config and from translations according to chat type. + *

+ * Handled at {@link org.bukkit.event.EventPriority#LOWEST} on both preview and chat events. + */ + protected void handleChatFormat(AsyncPlayerChatEvent event) { + if (isAborted(event)) { + return; + } + + final User user = ess.getUser(event.getPlayer()); + + if (user == null) { + event.setCancelled(true); + return; + } + + // Reuse cached IntermediateChat if available + ChatProcessingCache.IntermediateChat chat = cache.getIntermediateChat(event.getPlayer()); + if (chat == null) { + chat = new ChatProcessingCache.IntermediateChat(user, getChatType(user, event.getMessage()), event.getMessage()); + cache.setIntermediateChat(event.getPlayer(), chat); + } + + final long configRadius = ess.getSettings().getChatRadius(); + chat.setRadius(Math.max(configRadius, 0)); + + // This listener should apply the general chat formatting only...then return control back the event handler + event.setMessage(FormatUtil.formatMessage(user, "essentials.chat", event.getMessage())); + + if (ChatColor.stripColor(event.getMessage()).length() == 0) { + event.setCancelled(true); + return; + } + + final String group = user.getGroup(); + final String world = user.getWorld().getName(); + final String username = user.getName(); + final String nickname = user.getFormattedNickname(); + + final Player player = user.getBase(); + final String prefix = FormatUtil.replaceFormat(ess.getPermissionsHandler().getPrefix(player)); + final String suffix = FormatUtil.replaceFormat(ess.getPermissionsHandler().getSuffix(player)); + final Team team = player.getScoreboard().getPlayerTeam(player); + + String format = ess.getSettings().getChatFormat(group); + format = format.replace("{0}", group); + format = format.replace("{1}", ess.getSettings().getWorldAlias(world)); + format = format.replace("{2}", world.substring(0, 1).toUpperCase(Locale.ENGLISH)); + format = format.replace("{3}", team == null ? "" : team.getPrefix()); + format = format.replace("{4}", team == null ? "" : team.getSuffix()); + format = format.replace("{5}", team == null ? "" : team.getDisplayName()); + format = format.replace("{6}", prefix); + format = format.replace("{7}", suffix); + format = format.replace("{8}", username); + format = format.replace("{9}", nickname == null ? username : nickname); + + // Local, shout and question chat types are only enabled when there's a valid radius + if (chat.getRadius() > 0 && event.getMessage().length() > 1) { + if (chat.getType().isEmpty()) { + if (user.isToggleShout() && event.getMessage().charAt(0) == ess.getSettings().getChatShout()) { + event.setMessage(event.getMessage().substring(1)); + } + format = tl("chatTypeLocal").concat(format); + } else { + if (event.getMessage().charAt(0) == ess.getSettings().getChatShout() || (event.getMessage().charAt(0) == ess.getSettings().getChatQuestion() && ess.getSettings().isChatQuestionEnabled())) { + event.setMessage(event.getMessage().substring(1)); + } + format = tl(chat.getType() + "Format", format); + } + } + + // Long live pointless synchronized blocks! + synchronized (format) { + event.setFormat(format); + } + + chat.setFormatResult(event.getFormat()); + chat.setMessageResult(event.getMessage()); + } + + /** + * Handle the recipient filtering and permissions checks for local chat, if enabled. + *

+ * Runs at {@link org.bukkit.event.EventPriority#NORMAL} priority on submitted chat events only. + */ + protected void handleChatRecipients(AsyncPlayerChatEvent event) { + if (isAborted(event)) { + return; + } + + final ChatProcessingCache.Chat chat = cache.getIntermediateOrElseProcessedChat(event.getPlayer()); + + // If local chat is enabled, handle the recipients here; else we have nothing to do + if (chat.getRadius() < 1) { + return; + } + final long radiusSquared = chat.getRadius() * chat.getRadius(); + + final User user = chat.getUser(); + + if (event.getMessage().length() > 1) { + if (chat.getType().isEmpty()) { + if (!user.isAuthorized("essentials.chat.local")) { + user.sendMessage(tl("notAllowedToLocal")); + event.setCancelled(true); + return; + } + + event.getRecipients().removeIf(player -> !ess.getUser(player).isAuthorized("essentials.chat.receive.local")); + } else { + final String permission = "essentials.chat." + chat.getType(); + + if (user.isAuthorized(permission)) { + event.getRecipients().removeIf(player -> !ess.getUser(player).isAuthorized("essentials.chat.receive." + chat.getType())); + return; + } + + user.sendMessage(tl("notAllowedTo" + chat.getType().substring(0, 1).toUpperCase(Locale.ENGLISH) + chat.getType().substring(1))); + event.setCancelled(true); + return; + } + } + + final Location loc = user.getLocation(); + final World world = loc.getWorld(); + + final Set outList = event.getRecipients(); + final Set spyList = new HashSet<>(); + + try { + outList.add(event.getPlayer()); + } catch (final UnsupportedOperationException ex) { + if (ess.getSettings().isDebug()) { + essChat.getLogger().log(Level.INFO, "Plugin triggered custom chat event, local chat handling aborted.", ex); + } + return; + } + + final Iterator it = outList.iterator(); + while (it.hasNext()) { + final Player onlinePlayer = it.next(); + final User onlineUser = ess.getUser(onlinePlayer); + if (!onlineUser.equals(user)) { + boolean abort = false; + final Location playerLoc = onlineUser.getLocation(); + if (playerLoc.getWorld() != world) { + abort = true; + } else { + final double delta = playerLoc.distanceSquared(loc); + if (delta > radiusSquared) { + abort = true; + } + } + if (abort) { + if (onlineUser.isAuthorized("essentials.chat.spy")) { + spyList.add(onlinePlayer); + } + it.remove(); + } + } + } + + if (outList.size() < 2) { + user.sendMessage(tl("localNoOne")); + } + + // Strip local chat prefix to preserve API behaviour + final String localPrefix = tl("chatTypeLocal"); + String baseFormat = event.getFormat(); + if (event.getFormat().startsWith(localPrefix)) { + baseFormat = baseFormat.substring(localPrefix.length()); + } + + final LocalChatSpyEvent spyEvent = new LocalChatSpyEvent(event.isAsynchronous(), event.getPlayer(), baseFormat, event.getMessage(), spyList); + server.getPluginManager().callEvent(spyEvent); + + if (!spyEvent.isCancelled()) { + for (final Player onlinePlayer : spyEvent.getRecipients()) { + onlinePlayer.sendMessage(String.format(spyEvent.getFormat(), user.getDisplayName(), spyEvent.getMessage())); + } + } + } + + /** + * Finalise the formatting stage of chat processing. + *

+ * Handled at {@link org.bukkit.event.EventPriority#HIGHEST} during previews, and immediately after + * {@link #handleChatFormat(AsyncPlayerChatEvent)} when previews are not available. + */ + protected void handleChatPostFormat(AsyncPlayerChatEvent event) { + final ChatProcessingCache.IntermediateChat intermediateChat = cache.clearIntermediateChat(event.getPlayer()); + if (isAborted(event) || intermediateChat == null) { + return; + } + + // in case of modifications by other plugins during the preview + intermediateChat.setFormatResult(event.getFormat()); + intermediateChat.setMessageResult(event.getMessage()); + + final ChatProcessingCache.ProcessedChat processed = new ChatProcessingCache.ProcessedChat(ess, intermediateChat); + cache.setProcessedChat(event.getPlayer(), processed); + } + + /** + * Run costs for chat and clean up the cached {@link com.earth2me.essentials.chat.processing.ChatProcessingCache.ProcessedChat} + */ + protected void handleChatSubmit(AsyncPlayerChatEvent event) { + if (isAborted(event)) { + return; + } + + // This file should handle charging the user for the action before returning control back + charge(event, cache.getProcessedChat(event.getPlayer())); + + cache.clearProcessedChat(event.getPlayer()); + } + + boolean isAborted(final AsyncPlayerChatEvent event) { + return event.isCancelled(); + } + + String getChatType(final User user, final String message) { + if (message.length() == 0) { + //Ignore empty chat events generated by plugins + return ""; + } + + final char prefix = message.charAt(0); + if (prefix == ess.getSettings().getChatShout()) { + if (user.isToggleShout()) { + return ""; + } + return message.length() > 1 ? "shout" : ""; + } else if (ess.getSettings().isChatQuestionEnabled() && prefix == ess.getSettings().getChatQuestion()) { + return message.length() > 1 ? "question" : ""; + } else if (user.isToggleShout()) { + return message.length() > 1 ? "shout" : ""; + } else { + return ""; + } + } + + private void charge(final User user, final Trade charge) throws ChargeException { + charge.charge(user); + } + + boolean charge(final AsyncPlayerChatEvent event, final ChatProcessingCache.ProcessedChat chat) { + try { + charge(chat.getUser(), chat.getCharge()); + } catch (final ChargeException e) { + ess.showError(chat.getUser().getSource(), e, "\\ chat " + chat.getLongType()); + event.setCancelled(true); + return false; + } + return true; + } + + protected interface ChatListener extends Listener { + @SuppressWarnings("unused") + void onPlayerChat(AsyncPlayerChatEvent event); + } + +} diff --git a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/ChatProcessingCache.java b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/ChatProcessingCache.java new file mode 100644 index 000000000..f2238007f --- /dev/null +++ b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/ChatProcessingCache.java @@ -0,0 +1,144 @@ +package com.earth2me.essentials.chat.processing; + +import com.earth2me.essentials.Trade; +import com.earth2me.essentials.User; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import net.ess3.api.IEssentials; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class ChatProcessingCache { + + private final Map intermediateChats = Collections.synchronizedMap(new HashMap<>()); + + private final Cache processedChats = CacheBuilder.newBuilder() + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(); + + public IntermediateChat getIntermediateChat(final Player player) { + return intermediateChats.get(player); + } + + public void setIntermediateChat(final Player player, final IntermediateChat intermediateChat) { + intermediateChats.put(player, intermediateChat); + } + + public IntermediateChat clearIntermediateChat(final Player player) { + return intermediateChats.remove(player); + } + + public ProcessedChat getProcessedChat(final Player player) { + return processedChats.getIfPresent(player); + } + + public void setProcessedChat(final Player player, final ProcessedChat chat) { + processedChats.put(player, chat); + } + + public ProcessedChat clearProcessedChat(final Player player) { + final ProcessedChat chat = processedChats.getIfPresent(player); + processedChats.invalidate(player); + return chat; + } + + public Chat getIntermediateOrElseProcessedChat(final Player player) { + final IntermediateChat chat = getIntermediateChat(player); + if (chat != null) { + return chat; + } + return getProcessedChat(player); + } + + public static abstract class Chat { + private final User user; + private final String type; + private final String originalMessage; + protected long radius; + + protected Chat(User user, String type, String originalMessage) { + this.user = user; + this.type = type; + this.originalMessage = originalMessage; + } + + public User getUser() { + return user; + } + + public String getType() { + return type; + } + + public String getOriginalMessage() { + return originalMessage; + } + + public long getRadius() { + return radius; + } + + public final String getLongType() { + return type.length() == 0 ? "chat" : "chat-" + type; + } + } + + public static class ProcessedChat extends Chat { + private final String message; + private final String format; + private final Trade charge; + + public ProcessedChat(final IEssentials ess, final IntermediateChat sourceChat) { + super(sourceChat.getUser(), sourceChat.getType(), sourceChat.getOriginalMessage()); + this.message = sourceChat.messageResult; + this.format = sourceChat.formatResult; + this.charge = new Trade(getLongType(), ess); + } + + public String getMessage() { + return message; + } + + public String getFormat() { + return format; + } + + public Trade getCharge() { + return charge; + } + } + + public static class IntermediateChat extends Chat { + private String messageResult; + private String formatResult; + + public IntermediateChat(final User user, final String type, final String originalMessage) { + super(user, type, originalMessage); + } + + public void setRadius(final long radius) { + this.radius = radius; + } + + public String getMessageResult() { + return messageResult; + } + + public void setMessageResult(String messageResult) { + this.messageResult = messageResult; + } + + public String getFormatResult() { + return formatResult; + } + + public void setFormatResult(String formatResult) { + this.formatResult = formatResult; + } + } + +} diff --git a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/LegacyChatHandler.java b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/LegacyChatHandler.java new file mode 100644 index 000000000..b10cf9ca2 --- /dev/null +++ b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/LegacyChatHandler.java @@ -0,0 +1,46 @@ +package com.earth2me.essentials.chat.processing; + +import com.earth2me.essentials.Essentials; +import com.earth2me.essentials.chat.EssentialsChat; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.plugin.PluginManager; + +public class LegacyChatHandler extends AbstractChatHandler { + public LegacyChatHandler(Essentials ess, EssentialsChat essChat) { + super(ess, essChat); + } + + public void registerListeners() { + final PluginManager pm = essChat.getServer().getPluginManager(); + pm.registerEvents(new ChatLowest(), essChat); + pm.registerEvents(new ChatNormal(), essChat); + pm.registerEvents(new ChatHighest(), essChat); + } + + private class ChatLowest implements ChatListener { + @Override + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerChat(AsyncPlayerChatEvent event) { + handleChatFormat(event); + } + } + + private class ChatNormal implements ChatListener { + @Override + @EventHandler(priority = EventPriority.NORMAL) + public void onPlayerChat(AsyncPlayerChatEvent event) { + handleChatRecipients(event); + } + } + + private class ChatHighest implements ChatListener { + @Override + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerChat(AsyncPlayerChatEvent event) { + handleChatPostFormat(event); + handleChatSubmit(event); + } + } +} diff --git a/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/SignedChatHandler.java b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/SignedChatHandler.java new file mode 100644 index 000000000..9b36e4f5f --- /dev/null +++ b/EssentialsChat/src/main/java/com/earth2me/essentials/chat/processing/SignedChatHandler.java @@ -0,0 +1,120 @@ +package com.earth2me.essentials.chat.processing; + +import com.earth2me.essentials.Essentials; +import com.earth2me.essentials.I18n; +import com.earth2me.essentials.chat.EssentialsChat; +import com.earth2me.essentials.utils.VersionUtil; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.AsyncPlayerChatPreviewEvent; +import org.bukkit.plugin.PluginManager; + +public class SignedChatHandler extends AbstractChatHandler { + + public SignedChatHandler(Essentials ess, EssentialsChat essChat) { + super(ess, essChat); + } + + public boolean tryRegisterListeners() { + if (VersionUtil.getServerBukkitVersion().isLowerThan(VersionUtil.v1_19_2_R01)) { + return false; + } + + try { + final Class previewClass = Class.forName("org.bukkit.event.player.AsyncPlayerChatPreviewEvent"); + if (!AsyncPlayerChatEvent.class.isAssignableFrom(previewClass)) { + essChat.getLogger().severe(I18n.tl("essChatNoSecureMsg", essChat.getDescription().getVersion())); + return false; + } + } catch (ClassNotFoundException e) { + essChat.getLogger().severe(I18n.tl("essChatNoSecureMsg", essChat.getDescription().getVersion())); + return false; + } + + final PluginManager pm = essChat.getServer().getPluginManager(); + pm.registerEvents(new PreviewLowest(), essChat); + pm.registerEvents(new PreviewHighest(), essChat); + pm.registerEvents(new ChatLowest(), essChat); + pm.registerEvents(new ChatNormal(), essChat); + pm.registerEvents(new ChatHighest(), essChat); + return true; + } + + private void handleChatApplyPreview(AsyncPlayerChatEvent event) { + final ChatProcessingCache.ProcessedChat chat = cache.getProcessedChat(event.getPlayer()); + if (chat == null) { + handleChatFormat(event); + handleChatPostFormat(event); + } else { + event.setFormat(chat.getFormat()); + event.setMessage(chat.getMessage()); + } + } + + private void handleChatConfirmPreview(AsyncPlayerChatEvent event) { + if (!ess.getSettings().isDebug()) return; + + final ChatProcessingCache.ProcessedChat chat = cache.getProcessedChat(event.getPlayer()); + if (chat == null) { + // Can't confirm preview for some reason + essChat.getLogger().info("Processed chat missing for " + event.getPlayer()); + } else { + if (!event.getFormat().equals(chat.getFormat())) { + // Chat format modified by another plugin + essChat.getLogger().info("Chat format has been modified for " + event.getPlayer()); + essChat.getLogger().info("Expected '" + chat.getFormat() + "', got '" + event.getFormat()); + } + if (!event.getMessage().equals(chat.getMessage())) { + // Chat message modified by another plugin + essChat.getLogger().info("Chat message has been modified for " + event.getPlayer()); + essChat.getLogger().info("Expected '" + chat.getMessage() + "', got '" + event.getMessage()); + } + } + } + + private interface PreviewListener extends Listener { + void onPlayerChatPreview(AsyncPlayerChatPreviewEvent event); + } + + private class PreviewLowest implements PreviewListener { + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerChatPreview(AsyncPlayerChatPreviewEvent event) { + handleChatFormat(event); + } + } + + private class PreviewHighest implements PreviewListener { + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerChatPreview(AsyncPlayerChatPreviewEvent event) { + handleChatPostFormat(event); + } + } + + private class ChatLowest implements ChatListener { + @Override + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerChat(AsyncPlayerChatEvent event) { + handleChatApplyPreview(event); + } + } + + private class ChatNormal implements ChatListener { + @Override + @EventHandler(priority = EventPriority.NORMAL) + public void onPlayerChat(AsyncPlayerChatEvent event) { + handleChatRecipients(event); + } + } + + private class ChatHighest implements ChatListener { + @Override + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerChat(AsyncPlayerChatEvent event) { + handleChatConfirmPreview(event); + handleChatSubmit(event); + } + } + +}