Add support for Paper's chat events (#6033)

Fixes #4970
Fixes #5887
This commit is contained in:
Josh Roy 2025-02-14 15:14:44 -05:00 committed by GitHub
parent 4e6478224c
commit cb00783ede
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 362 additions and 47 deletions

View File

@ -1,6 +1,7 @@
package com.earth2me.essentials.utils;
import net.ess3.api.IEssentials;
import net.ess3.provider.AbstractChatEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.format.NamedTextColor;
@ -20,6 +21,7 @@ public final class AdventureUtil {
static {
final LegacyComponentSerializer.Builder builder = LegacyComponentSerializer.builder()
.flattener(ComponentFlattener.basic())
.extractUrls(AbstractChatEvent.URL_PATTERN)
.useUnusualXRepeatedCharacterHexFormat();
if (VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_16_1_R01)) {
builder.hexColors();

View File

@ -1,6 +1,7 @@
package com.earth2me.essentials.utils;
import net.ess3.api.IUser;
import net.ess3.provider.AbstractChatEvent;
import org.bukkit.ChatColor;
import org.bukkit.Color;
@ -24,7 +25,6 @@ public final class FormatUtil {
private static final Pattern REPLACE_ALL_RGB_PATTERN = Pattern.compile("(&)?&#([0-9a-fA-F]{6})");
//Used to prepare xmpp output
private static final Pattern LOGCOLOR_PATTERN = Pattern.compile("\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]");
private static final Pattern URL_PATTERN = Pattern.compile("((?:(?:https?)://)?[\\w-_\\.]{2,})\\.([a-zA-Z]{2,3}(?:/\\S+)?)");
//Used to strip ANSI control codes from console
private static final Pattern ANSI_CONTROL_PATTERN = Pattern.compile("[\\x1B\\x9B][\\[\\]()#;?]*(?:(?:(?:;[-a-zA-Z\\d/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d/#&.:=?%@~_]*)*)?\\x07|(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~])");
private static final Pattern PAPER_CONTROL_PATTERN = Pattern.compile("(?i)" + (char) 0x7f + "[0-9A-FK-ORX]");
@ -297,9 +297,9 @@ public final class FormatUtil {
if (input == null) {
return null;
}
String text = URL_PATTERN.matcher(input).replaceAll("$1 $2");
while (URL_PATTERN.matcher(text).find()) {
text = URL_PATTERN.matcher(text).replaceAll("$1 $2");
String text = AbstractChatEvent.URL_PATTERN.matcher(input).replaceAll("$1 $2");
while (AbstractChatEvent.URL_PATTERN.matcher(text).find()) {
text = AbstractChatEvent.URL_PATTERN.matcher(text).replaceAll("$1 $2");
}
return text;
}

View File

@ -3,8 +3,10 @@ package com.earth2me.essentials.chat;
import com.earth2me.essentials.Essentials;
import com.earth2me.essentials.EssentialsLogger;
import com.earth2me.essentials.chat.processing.ChatHandler;
import com.earth2me.essentials.chat.processing.PaperChatHandler;
import com.earth2me.essentials.metrics.MetricsWrapper;
import com.earth2me.essentials.utils.AdventureUtil;
import com.earth2me.essentials.utils.VersionUtil;
import net.ess3.api.IEssentials;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@ -32,8 +34,13 @@ public class EssentialsChat extends JavaPlugin {
return;
}
final ChatHandler legacyHandler = new ChatHandler((Essentials) ess, this);
legacyHandler.registerListeners();
if (VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_16_5_R01) && VersionUtil.isPaper()) {
final PaperChatHandler paperHandler = new PaperChatHandler((Essentials) ess, this);
paperHandler.registerListeners();
} else {
final ChatHandler legacyHandler = new ChatHandler((Essentials) ess, this);
legacyHandler.registerListeners();
}
if (metrics == null) {
metrics = new MetricsWrapper(this, 3814, false);

View File

@ -8,6 +8,7 @@ import com.earth2me.essentials.chat.EssentialsChat;
import com.earth2me.essentials.utils.AdventureUtil;
import com.earth2me.essentials.utils.FormatUtil;
import net.ess3.api.events.LocalChatSpyEvent;
import net.ess3.provider.AbstractChatEvent;
import net.essentialsx.api.v2.ChatType;
import net.essentialsx.api.v2.events.chat.ChatEvent;
import net.essentialsx.api.v2.events.chat.GlobalChatEvent;
@ -22,10 +23,8 @@ 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.tlLiteral;
@ -48,7 +47,7 @@ public abstract class AbstractChatHandler {
* <p>
* Handled at {@link org.bukkit.event.EventPriority#LOWEST} on both preview and chat events.
*/
protected void handleChatFormat(AsyncPlayerChatEvent event) {
protected void handleChatFormat(AbstractChatEvent event) {
if (isAborted(event)) {
return;
}
@ -73,8 +72,9 @@ public abstract class AbstractChatHandler {
final long configRadius = ess.getSettings().getChatRadius();
chat.setRadius(Math.max(configRadius, 0));
final String formatted = FormatUtil.formatMessage(user, "essentials.chat", event.getMessage());
// 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()));
event.setMessage(formatted);
if (ChatColor.stripColor(event.getMessage()).isEmpty()) {
event.setCancelled(true);
@ -110,6 +110,12 @@ public abstract class AbstractChatHandler {
event.setMessage(event.getMessage().substring(1));
}
// Prevent messages like "!&c" or "?&c" from being sent which would cause an empty message
if (ChatColor.stripColor(event.getMessage()).isEmpty()) {
event.setCancelled(true);
return;
}
if (chat.getType() == ChatType.UNKNOWN) {
format = AdventureUtil.miniToLegacy(tlLiteral("chatTypeLocal")).concat(format);
} else {
@ -128,7 +134,7 @@ public abstract class AbstractChatHandler {
* <p>
* Runs at {@link org.bukkit.event.EventPriority#NORMAL} priority on submitted chat events only.
*/
protected void handleChatRecipients(AsyncPlayerChatEvent event) {
protected void handleChatRecipients(AbstractChatEvent event) {
if (isAborted(event)) {
return;
}
@ -151,12 +157,12 @@ public abstract class AbstractChatHandler {
return;
}
event.getRecipients().removeIf(player -> !ess.getUser(player).isAuthorized("essentials.chat.receive.local"));
event.removeRecipients(player -> !ess.getUser(player).isAuthorized("essentials.chat.receive.local"));
} else {
final String permission = "essentials.chat." + chat.getType().key();
if (user.isAuthorized(permission)) {
event.getRecipients().removeIf(player -> !ess.getUser(player).isAuthorized("essentials.chat.receive." + chat.getType().key()));
event.removeRecipients(player -> !ess.getUser(player).isAuthorized("essentials.chat.receive." + chat.getType().key()));
callChatEvent(event, chat.getType(), null);
} else {
@ -171,22 +177,10 @@ public abstract class AbstractChatHandler {
final Location loc = user.getLocation();
final World world = loc.getWorld();
final Set<Player> outList = event.getRecipients();
final Set<Player> 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<Player> it = outList.iterator();
while (it.hasNext()) {
final Player onlinePlayer = it.next();
final User onlineUser = ess.getUser(onlinePlayer);
event.removeRecipients(player -> {
final User onlineUser = ess.getUser(player);
if (!onlineUser.equals(user)) {
boolean abort = false;
final Location playerLoc = onlineUser.getLocation();
@ -200,12 +194,13 @@ public abstract class AbstractChatHandler {
}
if (abort) {
if (onlineUser.isAuthorized("essentials.chat.spy")) {
spyList.add(onlinePlayer);
spyList.add(player);
}
it.remove();
return true;
}
}
}
return false;
});
callChatEvent(event, ChatType.LOCAL, chat.getRadius());
@ -213,7 +208,7 @@ public abstract class AbstractChatHandler {
return;
}
if (outList.size() < 2) {
if (event.recipients().size() < 2) {
user.sendTl("localNoOne");
}
@ -242,17 +237,22 @@ public abstract class AbstractChatHandler {
* @param chatType Chat type which determines which event will be created and called.
* @param radius If chat is a local chat, this is a non-squared radius used to calculate recipients, otherwise {@code null}.
*/
protected void callChatEvent(final AsyncPlayerChatEvent event, final ChatType chatType, final Long radius) {
protected void callChatEvent(final AbstractChatEvent event, final ChatType chatType, final Long radius) {
final ChatEvent chatEvent;
if (chatType == ChatType.LOCAL) {
chatEvent = new LocalChatEvent(event.isAsynchronous(), event.getPlayer(), event.getFormat(), event.getMessage(), event.getRecipients(), radius);
chatEvent = new LocalChatEvent(event.isAsynchronous(), event.getPlayer(), event.getFormat(), event.getMessage(), event.recipients(), radius);
} else {
chatEvent = new GlobalChatEvent(event.isAsynchronous(), chatType, event.getPlayer(), event.getFormat(), event.getMessage(), event.getRecipients());
chatEvent = new GlobalChatEvent(event.isAsynchronous(), chatType, event.getPlayer(), event.getFormat(), event.getMessage(), event.recipients());
}
server.getPluginManager().callEvent(chatEvent);
event.removeRecipients(player -> !chatEvent.getRecipients().contains(player));
for (final Player recipient : chatEvent.getRecipients()) {
event.addRecipient(recipient);
}
event.setFormat(chatEvent.getFormat());
event.setMessage(chatEvent.getMessage());
event.setCancelled(chatEvent.isCancelled());
@ -262,9 +262,9 @@ public abstract class AbstractChatHandler {
* Finalise the formatting stage of chat processing.
* <p>
* Handled at {@link org.bukkit.event.EventPriority#HIGHEST} during previews, and immediately after
* {@link #handleChatFormat(AsyncPlayerChatEvent)} when previews are not available.
* {@link #handleChatFormat(AbstractChatEvent)} when previews are not available.
*/
protected void handleChatPostFormat(AsyncPlayerChatEvent event) {
protected void handleChatPostFormat(AbstractChatEvent event) {
if (isAborted(event)) {
cache.clearProcessedChat(event.getPlayer());
}
@ -273,7 +273,7 @@ public abstract class AbstractChatHandler {
/**
* Run costs for chat and clean up the cached {@link com.earth2me.essentials.chat.processing.ChatProcessingCache.ProcessedChat}
*/
protected void handleChatSubmit(AsyncPlayerChatEvent event) {
protected void handleChatSubmit(AbstractChatEvent event) {
if (isAborted(event)) {
return;
}
@ -284,7 +284,7 @@ public abstract class AbstractChatHandler {
cache.clearProcessedChat(event.getPlayer());
}
boolean isAborted(final AsyncPlayerChatEvent event) {
boolean isAborted(final AbstractChatEvent event) {
return event.isCancelled();
}
@ -320,7 +320,7 @@ public abstract class AbstractChatHandler {
charge.charge(user);
}
boolean charge(final AsyncPlayerChatEvent event, final ChatProcessingCache.ProcessedChat chat) {
boolean charge(final AbstractChatEvent event, final ChatProcessingCache.ProcessedChat chat) {
try {
charge(chat.getUser(), chat.getCharge());
} catch (final ChargeException e) {

View File

@ -2,6 +2,7 @@ package com.earth2me.essentials.chat.processing;
import com.earth2me.essentials.Essentials;
import com.earth2me.essentials.chat.EssentialsChat;
import net.ess3.provider.AbstractChatEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.player.AsyncPlayerChatEvent;
@ -19,28 +20,33 @@ public class ChatHandler extends AbstractChatHandler {
pm.registerEvents(new ChatHighest(), essChat);
}
private class ChatLowest implements ChatListener {
private AbstractChatEvent wrap(final AsyncPlayerChatEvent event) {
return new SpigotChatEvent(event);
}
private final class ChatLowest implements ChatListener {
@Override
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerChat(AsyncPlayerChatEvent event) {
handleChatFormat(event);
handleChatFormat(wrap(event));
}
}
private class ChatNormal implements ChatListener {
private final class ChatNormal implements ChatListener {
@Override
@EventHandler(priority = EventPriority.NORMAL)
public void onPlayerChat(AsyncPlayerChatEvent event) {
handleChatRecipients(event);
handleChatRecipients(wrap(event));
}
}
private class ChatHighest implements ChatListener {
private final class ChatHighest implements ChatListener {
@Override
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerChat(AsyncPlayerChatEvent event) {
handleChatPostFormat(event);
handleChatSubmit(event);
final AbstractChatEvent absEvent = wrap(event);
handleChatPostFormat(absEvent);
handleChatSubmit(absEvent);
}
}
}

View File

@ -0,0 +1,36 @@
package com.earth2me.essentials.chat.processing;
import com.earth2me.essentials.Essentials;
import com.earth2me.essentials.chat.EssentialsChat;
import net.ess3.provider.AbstractChatEvent;
import net.ess3.provider.providers.PaperChatListenerProvider;
import org.bukkit.plugin.PluginManager;
public class PaperChatHandler extends AbstractChatHandler {
public PaperChatHandler(Essentials ess, EssentialsChat essChat) {
super(ess, essChat);
}
public void registerListeners() {
final PluginManager pm = essChat.getServer().getPluginManager();
pm.registerEvents(new ChatListener(), essChat);
}
public final class ChatListener extends PaperChatListenerProvider {
@Override
public void onChatLowest(AbstractChatEvent event) {
handleChatFormat(event);
}
@Override
public void onChatNormal(AbstractChatEvent event) {
handleChatRecipients(event);
}
@Override
public void onChatHighest(AbstractChatEvent event) {
handleChatPostFormat(event);
handleChatSubmit(event);
}
}
}

View File

@ -0,0 +1,72 @@
package com.earth2me.essentials.chat.processing;
import net.ess3.provider.AbstractChatEvent;
import org.bukkit.entity.Player;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import java.util.Collections;
import java.util.Set;
import java.util.function.Predicate;
public class SpigotChatEvent implements AbstractChatEvent {
private final AsyncPlayerChatEvent event;
public SpigotChatEvent(AsyncPlayerChatEvent event) {
this.event = event;
}
@Override
public boolean isAsynchronous() {
return event.isAsynchronous();
}
@Override
public boolean isCancelled() {
return event.isCancelled();
}
@Override
public void setCancelled(boolean toCancel) {
event.setCancelled(toCancel);
}
@Override
public String getFormat() {
return event.getFormat();
}
@Override
public void setFormat(String format) {
event.setFormat(format);
}
@Override
public String getMessage() {
return event.getMessage();
}
@Override
public void setMessage(String message) {
event.setMessage(message);
}
@Override
public Player getPlayer() {
return event.getPlayer();
}
@Override
public Set<Player> recipients() {
return Collections.unmodifiableSet(event.getRecipients());
}
@Override
public void removeRecipients(Predicate<Player> predicate) {
event.getRecipients().removeIf(predicate);
}
@Override
public void addRecipient(Player player) {
event.getRecipients().add(player);
}
}

View File

@ -1 +1 @@
const val RUN_PAPER_MINECRAFT_VERSION = "1.21.1"
const val RUN_PAPER_MINECRAFT_VERSION = "1.21.4"

View File

@ -0,0 +1,33 @@
package net.ess3.provider;
import org.bukkit.entity.Player;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
public interface AbstractChatEvent {
Pattern URL_PATTERN = Pattern.compile("((?:(?:https?)://)?[\\w-_\\.]{2,})\\.([a-zA-Z]{2,3}(?:/\\S+)?)");
boolean isAsynchronous();
boolean isCancelled();
void setCancelled(boolean toCancel);
String getFormat();
void setFormat(String format);
String getMessage();
void setMessage(String message);
Player getPlayer();
Set<Player> recipients();
void removeRecipients(Predicate<Player> predicate);
void addRecipient(Player player);
}

View File

@ -0,0 +1,83 @@
package net.ess3.provider.providers;
import io.papermc.paper.event.player.AsyncChatEvent;
import net.ess3.provider.AbstractChatEvent;
import net.kyori.adventure.audience.Audience;
import org.bukkit.entity.Player;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
public class PaperChatEvent implements AbstractChatEvent {
private final AsyncChatEvent event;
private String fakeFormat;
private String fakeMessage;
public PaperChatEvent(final AsyncChatEvent event) {
this.event = event;
this.fakeMessage = event.signedMessage().message();
}
@Override
public boolean isAsynchronous() {
return event.isAsynchronous();
}
@Override
public boolean isCancelled() {
return event.isCancelled();
}
@Override
public void setCancelled(boolean toCancel) {
event.setCancelled(toCancel);
}
@Override
public String getFormat() {
return fakeFormat;
}
@Override
public void setFormat(String format) {
this.fakeFormat = format;
}
@Override
public String getMessage() {
return fakeMessage;
}
@Override
public void setMessage(String message) {
this.fakeMessage = message;
}
@Override
public Player getPlayer() {
return event.getPlayer();
}
@Override
public Set<Player> recipients() {
final Set<Player> recipients = new HashSet<>();
for (final Audience recipient : event.viewers()) {
if (recipient instanceof Player) {
recipients.add((Player) recipient);
}
}
return Collections.unmodifiableSet(recipients);
}
@Override
public void removeRecipients(Predicate<Player> predicate) {
event.viewers().removeIf(recipient -> recipient instanceof Player && predicate.test((Player) recipient));
}
@Override
public void addRecipient(Player player) {
event.viewers().add(player);
}
}

View File

@ -0,0 +1,76 @@
package net.ess3.provider.providers;
import io.papermc.paper.chat.ChatRenderer;
import io.papermc.paper.event.player.AsyncChatEvent;
import net.ess3.provider.AbstractChatEvent;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import java.util.HashMap;
public abstract class PaperChatListenerProvider implements Listener {
private final LegacyComponentSerializer serializer;
private final HashMap<AsyncChatEvent, PaperChatEvent> eventMap = new HashMap<>();
public PaperChatListenerProvider() {
this.serializer = LegacyComponentSerializer.builder()
.flattener(ComponentFlattener.basic())
.extractUrls(AbstractChatEvent.URL_PATTERN)
.useUnusualXRepeatedCharacterHexFormat().build();
}
public abstract void onChatLowest(final AbstractChatEvent event);
public abstract void onChatNormal(final AbstractChatEvent event);
public abstract void onChatHighest(final AbstractChatEvent event);
@EventHandler(priority = EventPriority.LOWEST)
public void onLowest(final AsyncChatEvent event) {
onChatLowest(wrap(event));
}
@EventHandler(priority = EventPriority.NORMAL)
public void onNormal(final AsyncChatEvent event) {
onChatNormal(wrap(event));
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onHighest(final AsyncChatEvent event) {
final PaperChatEvent paperChatEvent = wrap(event);
onChatHighest(paperChatEvent);
final TextComponent format = serializer.deserialize(paperChatEvent.getFormat());
final TextComponent eventMessage = serializer.deserialize(paperChatEvent.getMessage());
if (!event.isCancelled()) {
event.renderer(ChatRenderer.viewerUnaware((player, displayName, message) ->
format.replaceText(builder -> builder
.match("%(\\d)\\$s").replacement((index, match) -> {
if (index.group(1).equals("1")) {
return displayName;
}
return eventMessage;
})
)));
}
eventMap.remove(event);
}
private PaperChatEvent wrap(final AsyncChatEvent event) {
PaperChatEvent paperChatEvent = eventMap.get(event);
if (paperChatEvent != null) {
return paperChatEvent;
}
paperChatEvent = new PaperChatEvent(event);
eventMap.put(event, paperChatEvent);
return paperChatEvent;
}
}