From fb426b870c256596d1f6a4205c16a517ee163f35 Mon Sep 17 00:00:00 2001 From: Vankka Date: Sun, 28 Jul 2024 02:18:17 +0300 Subject: [PATCH 1/3] Rendering mentions in-game --- .../message/render/GameChatRenderEvent.java | 99 +++++ .../listener/chat/IBukkitChatForwarder.java | 3 +- .../listener/chat/PaperChatListener.java | 51 ++- .../discordsrv/bukkit/BukkitDiscordSRV.java | 2 +- .../listener/chat/BukkitChatForwarder.java | 22 +- .../listener/chat/BukkitChatListener.java | 18 +- .../discordsrv/common/AbstractDiscordSRV.java | 4 +- .../commands/subcommand/ExecuteCommand.java | 2 +- .../commands/subcommand/BroadcastCommand.java | 6 +- .../DiscordToMinecraftChatConfig.java | 4 - .../MinecraftToDiscordChatConfig.java | 6 + .../main/channels/base/BaseChannelConfig.java | 4 + .../config/main/generic/MentionsConfig.java | 26 +- .../core/component/ComponentFactory.java | 89 ++++- .../component/DiscordMentionComponent.java | 364 ++++++++++++++++++ .../renderer/DiscordSRVMinecraftRenderer.java | 95 +---- .../result/ComponentResultStringifier.java | 2 +- .../message/ReceivedDiscordMessageImpl.java | 2 +- .../console/message/ConsoleMessage.java | 2 +- .../common/feature/mention/CachedMention.java | 87 +++++ .../MentionCachingModule.java | 139 ++++--- .../mention/MentionGameRenderingModule.java | 126 ++++++ .../discord/DiscordChatMessageModule.java | 2 +- .../MinecraftToDiscordChatModule.java | 86 +---- .../com/discordsrv/common/MockDiscordSRV.java | 2 +- 25 files changed, 992 insertions(+), 251 deletions(-) create mode 100644 api/src/main/java/com/discordsrv/api/events/message/render/GameChatRenderEvent.java create mode 100644 common/src/main/java/com/discordsrv/common/core/component/DiscordMentionComponent.java create mode 100644 common/src/main/java/com/discordsrv/common/feature/mention/CachedMention.java rename common/src/main/java/com/discordsrv/common/feature/{messageforwarding/game/minecrafttodiscord => mention}/MentionCachingModule.java (74%) create mode 100644 common/src/main/java/com/discordsrv/common/feature/mention/MentionGameRenderingModule.java rename common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/{minecrafttodiscord => }/MinecraftToDiscordChatModule.java (68%) diff --git a/api/src/main/java/com/discordsrv/api/events/message/render/GameChatRenderEvent.java b/api/src/main/java/com/discordsrv/api/events/message/render/GameChatRenderEvent.java new file mode 100644 index 00000000..c460037d --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/events/message/render/GameChatRenderEvent.java @@ -0,0 +1,99 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.events.message.render; + +import com.discordsrv.api.channel.GameChannel; +import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.events.Cancellable; +import com.discordsrv.api.events.PlayerEvent; +import com.discordsrv.api.events.Processable; +import com.discordsrv.api.player.DiscordSRVPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class GameChatRenderEvent implements PlayerEvent, Processable.Argument, Cancellable { + + private final Object triggeringEvent; + private final DiscordSRVPlayer player; + private final GameChannel channel; + private final MinecraftComponent message; + private MinecraftComponent annotatedMessage; + private boolean cancelled = false; + + public GameChatRenderEvent( + @Nullable Object triggeringEvent, + @NotNull DiscordSRVPlayer player, + @NotNull GameChannel channel, + @NotNull MinecraftComponent message + ) { + this.triggeringEvent = triggeringEvent; + this.player = player; + this.channel = channel; + this.message = message; + } + + public Object getTriggeringEvent() { + return triggeringEvent; + } + + @Override + public DiscordSRVPlayer getPlayer() { + return player; + } + + public GameChannel getChannel() { + return channel; + } + + public MinecraftComponent getMessage() { + return message; + } + + public MinecraftComponent getAnnotatedMessage() { + return annotatedMessage; + } + + @Override + public boolean isProcessed() { + return annotatedMessage != null; + } + + @Override + public void process(MinecraftComponent annotatedMessage) { + if (isProcessed()) { + throw new IllegalStateException("Already processed"); + } + this.annotatedMessage = annotatedMessage; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } +} diff --git a/bukkit/paper/src/main/java/com/discordsrv/bukkit/listener/chat/IBukkitChatForwarder.java b/bukkit/paper/src/main/java/com/discordsrv/bukkit/listener/chat/IBukkitChatForwarder.java index f1f37f9a..790b719e 100644 --- a/bukkit/paper/src/main/java/com/discordsrv/bukkit/listener/chat/IBukkitChatForwarder.java +++ b/bukkit/paper/src/main/java/com/discordsrv/bukkit/listener/chat/IBukkitChatForwarder.java @@ -24,5 +24,6 @@ import org.bukkit.event.Event; public interface IBukkitChatForwarder { - void publishEvent(Event event, Player player, MinecraftComponent component, boolean cancelled); + MinecraftComponent annotateChatMessage(Event event, Player player, MinecraftComponent component); + void forwardMessage(Event event, Player player, MinecraftComponent component, boolean cancelled); } diff --git a/bukkit/paper/src/main/java/com/discordsrv/bukkit/listener/chat/PaperChatListener.java b/bukkit/paper/src/main/java/com/discordsrv/bukkit/listener/chat/PaperChatListener.java index 283e5022..abcd9a4a 100644 --- a/bukkit/paper/src/main/java/com/discordsrv/bukkit/listener/chat/PaperChatListener.java +++ b/bukkit/paper/src/main/java/com/discordsrv/bukkit/listener/chat/PaperChatListener.java @@ -20,32 +20,69 @@ package com.discordsrv.bukkit.listener.chat; import com.discordsrv.api.component.MinecraftComponent; import com.discordsrv.bukkit.component.PaperComponentHandle; +import com.discordsrv.common.core.logging.Logger; +import com.discordsrv.unrelocate.net.kyori.adventure.text.Component; import io.papermc.paper.event.player.AsyncChatEvent; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + public class PaperChatListener implements Listener { - private static final PaperComponentHandle COMPONENT_HANDLE; + private static final PaperComponentHandle GET_MESSAGE_HANDLE = makeGet(); + private static final MethodHandle SET_MESSAGE_HANDLE = makeSet(); - static { - COMPONENT_HANDLE = new PaperComponentHandle<>( + private static PaperComponentHandle makeGet() { + return new PaperComponentHandle<>( AsyncChatEvent.class, "message", null ); } + @SuppressWarnings("JavaLangInvokeHandleSignature") // Unrelocate + private static MethodHandle makeSet() { + try { + return MethodHandles.lookup().findVirtual( + AsyncChatEvent.class, + "message", + MethodType.methodType(void.class, Component.class) + ); + } catch (NoSuchMethodException | IllegalAccessException ignored) {} + return null; + } private final IBukkitChatForwarder listener; + private final Logger logger; - public PaperChatListener(IBukkitChatForwarder listener) { + public PaperChatListener(IBukkitChatForwarder listener, Logger logger) { this.listener = listener; + this.logger = logger; + } + + @EventHandler(priority = EventPriority.LOW) + public void onAsyncChatRender(AsyncChatEvent event) { + if (SET_MESSAGE_HANDLE == null) { + return; + } + + MinecraftComponent component = GET_MESSAGE_HANDLE.getComponent(event); + MinecraftComponent annotated = listener.annotateChatMessage(event, event.getPlayer(), component); + if (annotated != null) { + try { + SET_MESSAGE_HANDLE.invoke(event, annotated.asAdventure()); + } catch (Throwable t) { + logger.debug("Failed to render Minecraft message", t); + } + } } @EventHandler(priority = EventPriority.MONITOR) - public void onAsyncChat(AsyncChatEvent event) { - MinecraftComponent component = COMPONENT_HANDLE.getComponent(event); - listener.publishEvent(event, event.getPlayer(), component, event.isCancelled()); + public void onAsyncChatForward(AsyncChatEvent event) { + MinecraftComponent component = GET_MESSAGE_HANDLE.getComponent(event); + listener.forwardMessage(event, event.getPlayer(), component, event.isCancelled()); } } diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java index 09f41643..1b2ec896 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java @@ -48,7 +48,7 @@ import com.discordsrv.common.config.configurate.manager.MainConfigManager; import com.discordsrv.common.config.configurate.manager.MessagesConfigManager; import com.discordsrv.common.config.messages.MessagesConfig; import com.discordsrv.common.feature.debug.data.OnlineMode; -import com.discordsrv.common.feature.messageforwarding.game.minecrafttodiscord.MinecraftToDiscordChatModule; +import com.discordsrv.common.feature.messageforwarding.game.MinecraftToDiscordChatModule; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import org.bukkit.Server; import org.bukkit.plugin.ServicePriority; diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/listener/chat/BukkitChatForwarder.java b/bukkit/src/main/java/com/discordsrv/bukkit/listener/chat/BukkitChatForwarder.java index cd49f9ab..d252a50b 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/listener/chat/BukkitChatForwarder.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/listener/chat/BukkitChatForwarder.java @@ -19,10 +19,12 @@ package com.discordsrv.bukkit.listener.chat; import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.events.message.render.GameChatRenderEvent; import com.discordsrv.api.events.message.receive.game.GameChatMessageReceiveEvent; import com.discordsrv.bukkit.BukkitDiscordSRV; import com.discordsrv.bukkit.component.PaperComponentHandle; import com.discordsrv.common.abstraction.player.IPlayer; +import com.discordsrv.common.core.logging.NamedLogger; import com.discordsrv.common.feature.channel.global.GlobalChannel; import org.bukkit.entity.Player; import org.bukkit.event.Event; @@ -33,8 +35,8 @@ public class BukkitChatForwarder implements IBukkitChatForwarder { public static Listener get(BukkitDiscordSRV discordSRV) { // TODO: config option //noinspection ConstantConditions,PointlessBooleanExpression - if (1 == 2 && PaperComponentHandle.IS_PAPER_ADVENTURE) { - return new PaperChatListener(new BukkitChatForwarder(discordSRV)); + if (1 == 1 && PaperComponentHandle.IS_PAPER_ADVENTURE) { + return new PaperChatListener(new BukkitChatForwarder(discordSRV), new NamedLogger(discordSRV, "CHAT_LISTENER")); } return new BukkitChatListener(new BukkitChatForwarder(discordSRV)); @@ -47,7 +49,21 @@ public class BukkitChatForwarder implements IBukkitChatForwarder { } @Override - public void publishEvent(Event event, Player player, MinecraftComponent component, boolean cancelled) { + public MinecraftComponent annotateChatMessage(Event event, Player player, MinecraftComponent component) { + IPlayer srvPlayer = discordSRV.playerProvider().player(player); + GameChatRenderEvent annotateEvent = new GameChatRenderEvent( + event, + srvPlayer, + new GlobalChannel(discordSRV), + component + ); + + discordSRV.eventBus().publish(annotateEvent); + return annotateEvent.getAnnotatedMessage(); + } + + @Override + public void forwardMessage(Event event, Player player, MinecraftComponent component, boolean cancelled) { IPlayer srvPlayer = discordSRV.playerProvider().player(player); discordSRV.scheduler().run(() -> discordSRV.eventBus().publish( new GameChatMessageReceiveEvent( diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/listener/chat/BukkitChatListener.java b/bukkit/src/main/java/com/discordsrv/bukkit/listener/chat/BukkitChatListener.java index 30694091..34aca905 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/listener/chat/BukkitChatListener.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/listener/chat/BukkitChatListener.java @@ -24,6 +24,7 @@ import net.kyori.adventure.platform.bukkit.BukkitComponentSerializer; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; public class BukkitChatListener implements Listener { @@ -33,11 +34,22 @@ public class BukkitChatListener implements Listener { this.forwarder = forwarder; } - @EventHandler(priority = EventPriority.MONITOR) - public void onAsyncPlayerChat(org.bukkit.event.player.AsyncPlayerChatEvent event) { + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void onAsyncPlayerChatAnnotate(AsyncPlayerChatEvent event) { MinecraftComponent component = ComponentUtil.toAPI( BukkitComponentSerializer.legacy().deserialize(event.getMessage())); - forwarder.publishEvent(event, event.getPlayer(), component, event.isCancelled()); + MinecraftComponent annotated = forwarder.annotateChatMessage(event, event.getPlayer(), component); + if (annotated != null) { + event.setMessage(BukkitComponentSerializer.legacy().serialize(ComponentUtil.fromAPI(annotated))); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onAsyncPlayerChatForward(AsyncPlayerChatEvent event) { + MinecraftComponent component = ComponentUtil.toAPI( + BukkitComponentSerializer.legacy().deserialize(event.getMessage())); + + forwarder.forwardMessage(event, event.getPlayer(), component, event.isCancelled()); } } diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index 5a1f983b..360b1d54 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -71,13 +71,14 @@ import com.discordsrv.common.feature.linking.LinkProvider; import com.discordsrv.common.feature.linking.LinkingModule; import com.discordsrv.common.feature.linking.impl.MinecraftAuthenticationLinker; import com.discordsrv.common.feature.linking.impl.StorageLinker; +import com.discordsrv.common.feature.mention.MentionGameRenderingModule; import com.discordsrv.common.feature.messageforwarding.discord.DiscordChatMessageModule; import com.discordsrv.common.feature.messageforwarding.discord.DiscordMessageMirroringModule; import com.discordsrv.common.feature.messageforwarding.game.JoinMessageModule; import com.discordsrv.common.feature.messageforwarding.game.LeaveMessageModule; import com.discordsrv.common.feature.messageforwarding.game.StartMessageModule; import com.discordsrv.common.feature.messageforwarding.game.StopMessageModule; -import com.discordsrv.common.feature.messageforwarding.game.minecrafttodiscord.MentionCachingModule; +import com.discordsrv.common.feature.mention.MentionCachingModule; import com.discordsrv.common.feature.profile.ProfileManager; import com.discordsrv.common.feature.update.UpdateChecker; import com.discordsrv.common.helper.ChannelConfigHelper; @@ -593,6 +594,7 @@ public abstract class AbstractDiscordSRV< registerModule(MentionCachingModule::new); registerModule(LinkingModule::new); registerModule(PresenceUpdaterModule::new); + registerModule(MentionGameRenderingModule::new); // Integrations registerIntegration("com.discordsrv.common.integration.LuckPermsIntegration"); diff --git a/common/src/main/java/com/discordsrv/common/command/discord/commands/subcommand/ExecuteCommand.java b/common/src/main/java/com/discordsrv/common/command/discord/commands/subcommand/ExecuteCommand.java index 46a81d4f..283e7b31 100644 --- a/common/src/main/java/com/discordsrv/common/command/discord/commands/subcommand/ExecuteCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/discord/commands/subcommand/ExecuteCommand.java @@ -248,7 +248,7 @@ public class ExecuteCommand implements Consumer").append(ComponentUtil.fromAPI( + discordSRV.componentFactory() + .textBuilder(guildChannel != null ? format.format : format.unknownFormat) + .addContext(guildChannel) + .applyPlaceholderService() + .build() + )); + } + + @NotNull + public Component makeUserMention(long id, MentionsConfig.FormatUser format, DiscordGuild guild) { + DiscordUser user = discordSRV.discordAPI().getUserById(id); + DiscordGuildMember member = guild.getMemberById(id); + + return DiscordMentionComponent.of("<@" + Long.toUnsignedString(id) + ">").append(ComponentUtil.fromAPI( + discordSRV.componentFactory() + .textBuilder(user != null ? (member != null ? format.format : format.formatGlobal) : format.unknownFormat) + .addContext(user, member) + .applyPlaceholderService() + .build() + )); + } + + public Component makeRoleMention(long id, MentionsConfig.Format format) { + DiscordRole role = discordSRV.discordAPI().getRoleById(id); + + return DiscordMentionComponent.of("<@&" + Long.toUnsignedString(id) + ">").append(ComponentUtil.fromAPI( + discordSRV.componentFactory() + .textBuilder(role != null ? format.format : format.unknownFormat) + .addContext(role) + .applyPlaceholderService() + .build() + )); + } + + @SuppressWarnings("DataFlowIssue") // isProcessed = processed is not null + public Component makeEmoteMention(long id, MentionsConfig.EmoteBehaviour behaviour) { + DiscordCustomEmoji emoji = discordSRV.discordAPI().getEmojiById(id); + if (emoji == null) { + return null; + } + + DiscordChatMessageCustomEmojiRenderEvent event = new DiscordChatMessageCustomEmojiRenderEvent(emoji); + discordSRV.eventBus().publish(event); + + if (event.isProcessed()) { + return DiscordMentionComponent.of(emoji.asJDA().getAsMention()) + .append(ComponentUtil.fromAPI(event.getRenderedEmojiFromProcessing())); + } + + switch (behaviour) { + case NAME: + return DiscordMentionComponent.of(emoji.asJDA().getAsMention()).append(Component.text(":" + emoji.getName() + ":")); + case BLANK: + default: + return null; + } + } + + public Component minecraftSerialize(DiscordGuild guild, BaseChannelConfig config, String discordMessage) { return DiscordSRVMinecraftRenderer.getWithContext(guild, config, () -> minecraftSerializer().serialize(discordMessage)); } + public String discordSerialize(Component component) { + Component mapped = Component.text().append(component).mapChildrenDeep(comp -> { + if (comp instanceof DiscordMentionComponent) { + return Component.text(((DiscordMentionComponent) comp).mention()); + } + return comp; + }).children().get(0); + return discordSerializer().serialize(mapped); + } + public MinecraftSerializer minecraftSerializer() { return minecraftSerializer; } diff --git a/common/src/main/java/com/discordsrv/common/core/component/DiscordMentionComponent.java b/common/src/main/java/com/discordsrv/common/core/component/DiscordMentionComponent.java new file mode 100644 index 00000000..2c2dfdcd --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/core/component/DiscordMentionComponent.java @@ -0,0 +1,364 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.core.component; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.*; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEventSource; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; + +import static java.util.Objects.requireNonNull; + +/** + * An Adventure {@link Component} that holds a Discord mention whilst being disguised as a {@link TextComponent} + * for compatibility with serializers that don't know how to deal with custom Component types. + *

+ * Possibly removable after adventure #842 + * + * @see ComponentFactory#discordSerialize(Component) + */ +public class DiscordMentionComponent implements TextComponent { + + @NotNull + public static DiscordMentionComponent of(@NotNull String mention) { + return new DiscordMentionComponent(new ArrayList<>(), Style.empty(), mention); + } + + @NotNull + public static Builder builder(@NotNull String mention) { + return new Builder(new ArrayList<>(), Style.empty(), mention); + } + + private final List children; + private final Style style; + private final String mention; + + private DiscordMentionComponent(List children, Style style, String mention) { + this.children = ComponentLike.asComponents(children, IS_NOT_EMPTY); + this.style = style; + this.mention = mention; + } + + @Override + @Deprecated // NOOP + public @NotNull String content() { + return ""; + } + + @Override + @Deprecated // NOOP + public @NotNull DiscordMentionComponent content(@NotNull String content) { + return this; + } + + @Override + public @NotNull Component asComponent() { + return TextComponent.super.asComponent(); + } + + @Override + public @NotNull Builder toBuilder() { + return new Builder(children, style, mention); + } + + @Override + public @Unmodifiable @NotNull List children() { + return children; + } + + @Override + public @NotNull DiscordMentionComponent children(@NotNull List children) { + return new DiscordMentionComponent(children, style, mention); + } + + @Override + public @NotNull Style style() { + return style; + } + + @Override + public @NotNull DiscordMentionComponent style(@NotNull Style style) { + return new DiscordMentionComponent(children, style, mention); + } + + @NotNull + public String mention() { + return mention; + } + + public @NotNull DiscordMentionComponent mention(@NotNull String mention) { + return new DiscordMentionComponent(children, style, mention); + } + + @Override + public String toString() { + return "DiscordMentionComponent{" + + "children=" + children + + ", style=" + style + + ", mention='" + mention + '\'' + + '}'; + } + + public static class Builder implements TextComponent.Builder { + + private final List children; + private Style.Builder styleBuilder; + private String mention; + + private Builder(List children, Style style, String mention) { + this.children = children; + this.styleBuilder = style.toBuilder(); + this.mention = mention; + } + + @Override + @Deprecated // NOOP + public @NotNull String content() { + return ""; + } + + @NotNull + @Override + @Deprecated // NOOP + public DiscordMentionComponent.Builder content(@NotNull String content) { + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder append(@NotNull Component component) { + if (component == Component.empty()) return this; + this.children.add(component); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder append(@NotNull Component @NotNull ... components) { + for (Component component : components) { + append(component); + } + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder append(@NotNull ComponentLike @NotNull ... components) { + for (ComponentLike component : components) { + append(component); + } + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder append(@NotNull Iterable components) { + for (Component child : children) { + append(child); + } + return this; + } + + @Override + public @NotNull List children() { + return children; + } + + @Override + public DiscordMentionComponent.@NotNull Builder style(@NotNull Style style) { + this.styleBuilder = style.toBuilder(); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder style(@NotNull Consumer consumer) { + consumer.accept(styleBuilder); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder font(@Nullable Key font) { + styleBuilder.font(font); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder color(@Nullable TextColor color) { + styleBuilder.color(color); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder colorIfAbsent(@Nullable TextColor color) { + styleBuilder.color(color); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder decoration(@NotNull TextDecoration decoration, TextDecoration.State state) { + styleBuilder.decoration(decoration, state); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder decorationIfAbsent(@NotNull TextDecoration decoration, TextDecoration.State state) { + styleBuilder.decorationIfAbsent(decoration, state); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder clickEvent(@Nullable ClickEvent event) { + styleBuilder.clickEvent(event); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder hoverEvent(@Nullable HoverEventSource source) { + styleBuilder.hoverEvent(source); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder insertion(@Nullable String insertion) { + styleBuilder.insertion(insertion); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder mergeStyle(@NotNull Component that, @NotNull Set merges) { + styleBuilder.merge(that.style(), merges); + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder resetStyle() { + styleBuilder = Style.style(); + return this; + } + + @NotNull + public String mention() { + return mention; + } + + public DiscordMentionComponent.@NotNull Builder mention(@NotNull String mention) { + this.mention = mention; + return this; + } + + @Override + public @NotNull DiscordMentionComponent build() { + return new DiscordMentionComponent(children, styleBuilder.build(), mention); + } + + /* + * Copyright (c) 2017-2023 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + @Override + public DiscordMentionComponent.@NotNull Builder applyDeep(@NotNull Consumer> action) { + this.apply(action); + if (this.children == Collections.emptyList()) { + return this; + } + ListIterator it = this.children.listIterator(); + while (it.hasNext()) { + final Component child = it.next(); + if (!(child instanceof BuildableComponent)) { + continue; + } + final ComponentBuilder childBuilder = ((BuildableComponent) child).toBuilder(); + childBuilder.applyDeep(action); + it.set(childBuilder.build()); + } + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder mapChildren( + @NotNull Function, ? extends BuildableComponent> function) { + if (this.children == Collections.emptyList()) { + return this; + } + final ListIterator it = this.children.listIterator(); + while (it.hasNext()) { + final Component child = it.next(); + if (!(child instanceof BuildableComponent)) { + continue; + } + final BuildableComponent mappedChild = requireNonNull(function.apply((BuildableComponent) child), "mappedChild"); + if (child == mappedChild) { + continue; + } + it.set(mappedChild); + } + return this; + } + + @Override + public DiscordMentionComponent.@NotNull Builder mapChildrenDeep( + @NotNull Function, ? extends BuildableComponent> function) { + if (this.children == Collections.emptyList()) { + return this; + } + final ListIterator it = this.children.listIterator(); + while (it.hasNext()) { + final Component child = it.next(); + if (!(child instanceof BuildableComponent)) { + continue; + } + final BuildableComponent mappedChild = requireNonNull(function.apply((BuildableComponent) child), "mappedChild"); + if (mappedChild.children().isEmpty()) { + if (child == mappedChild) { + continue; + } + it.set(mappedChild); + } else { + final ComponentBuilder builder = mappedChild.toBuilder(); + builder.mapChildrenDeep(function); + it.set(builder.build()); + } + } + return this; + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/core/component/renderer/DiscordSRVMinecraftRenderer.java b/common/src/main/java/com/discordsrv/common/core/component/renderer/DiscordSRVMinecraftRenderer.java index f832be7c..68d9e3fe 100644 --- a/common/src/main/java/com/discordsrv/common/core/component/renderer/DiscordSRVMinecraftRenderer.java +++ b/common/src/main/java/com/discordsrv/common/core/component/renderer/DiscordSRVMinecraftRenderer.java @@ -18,14 +18,9 @@ package com.discordsrv.common.core.component.renderer; -import com.discordsrv.api.discord.entity.DiscordUser; -import com.discordsrv.api.discord.entity.guild.DiscordCustomEmoji; import com.discordsrv.api.discord.entity.guild.DiscordGuild; -import com.discordsrv.api.discord.entity.guild.DiscordGuildMember; -import com.discordsrv.api.discord.entity.guild.DiscordRole; -import com.discordsrv.api.events.message.process.discord.DiscordChatMessageCustomEmojiRenderEvent; import com.discordsrv.common.DiscordSRV; -import com.discordsrv.common.config.main.channels.DiscordToMinecraftChatConfig; +import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.config.main.generic.MentionsConfig; import com.discordsrv.common.util.ComponentUtil; import dev.vankka.mcdiscordreserializer.renderer.implementation.DefaultMinecraftRenderer; @@ -35,7 +30,6 @@ import net.dv8tion.jda.api.utils.MiscUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.function.Supplier; import java.util.regex.Matcher; @@ -53,7 +47,7 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer { public static T getWithContext( DiscordGuild guild, - DiscordToMinecraftChatConfig config, + BaseChannelConfig config, Supplier supplier ) { Context oldValue = CONTEXT.get(); @@ -116,57 +110,20 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer { return component.append(Component.text("<#" + id + ">")); } - Component mention = makeChannelMention(MiscUtil.parseLong(id), format); - if (mention == null) { - return component; - } - - return component.append(mention); - } - - @Nullable - public Component makeChannelMention(long id, MentionsConfig.Format format) { - JDA jda = discordSRV.jda(); - if (jda == null) { - return null; - } - - GuildChannel guildChannel = jda.getGuildChannelById(id); - - return ComponentUtil.fromAPI( - discordSRV.componentFactory() - .textBuilder(guildChannel != null ? format.format : format.unknownFormat) - .addContext(guildChannel) - .applyPlaceholderService() - .build() - ); + return component.append(discordSRV.componentFactory().makeChannelMention(MiscUtil.parseLong(id), format)); } @Override public @NotNull Component appendUserMention(@NotNull Component component, @NotNull String id) { Context context = CONTEXT.get(); - MentionsConfig.Format format = context != null ? context.config.mentions.user : null; + MentionsConfig.FormatUser format = context != null ? context.config.mentions.user : null; DiscordGuild guild = context != null ? context.guild : null; if (format == null || guild == null) { return component.append(Component.text("<@" + id + ">")); } long userId = MiscUtil.parseLong(id); - return component.append(makeUserMention(userId, format, guild)); - } - - @NotNull - public Component makeUserMention(long id, MentionsConfig.Format format, DiscordGuild guild) { - DiscordUser user = discordSRV.discordAPI().getUserById(id); - DiscordGuildMember member = guild.getMemberById(id); - - return ComponentUtil.fromAPI( - discordSRV.componentFactory() - .textBuilder(user != null ? format.format : format.unknownFormat) - .addContext(user, member) - .applyPlaceholderService() - .build() - ); + return component.append(discordSRV.componentFactory().makeUserMention(userId, format, guild)); } @Override @@ -178,19 +135,7 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer { } long roleId = MiscUtil.parseLong(id); - return component.append(makeRoleMention(roleId, format)); - } - - public Component makeRoleMention(long id, MentionsConfig.Format format) { - DiscordRole role = discordSRV.discordAPI().getRoleById(id); - - return ComponentUtil.fromAPI( - discordSRV.componentFactory() - .textBuilder(role != null ? format.format : format.unknownFormat) - .addContext(role) - .applyPlaceholderService() - .build() - ); + return component.append(discordSRV.componentFactory().makeRoleMention(roleId, format)); } @Override @@ -206,7 +151,7 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer { } long emojiId = MiscUtil.parseLong(id); - Component emoteMention = makeEmoteMention(emojiId, behaviour); + Component emoteMention = discordSRV.componentFactory().makeEmoteMention(emojiId, behaviour); if (emoteMention == null) { return component; } @@ -214,34 +159,12 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer { return component.append(emoteMention); } - public Component makeEmoteMention(long id, MentionsConfig.EmoteBehaviour behaviour) { - DiscordCustomEmoji emoji = discordSRV.discordAPI().getEmojiById(id); - if (emoji == null) { - return null; - } - - DiscordChatMessageCustomEmojiRenderEvent event = new DiscordChatMessageCustomEmojiRenderEvent(emoji); - discordSRV.eventBus().publish(event); - - if (event.isProcessed()) { - return ComponentUtil.fromAPI(event.getRenderedEmojiFromProcessing()); - } - - switch (behaviour) { - case NAME: - return Component.text(":" + emoji.getName() + ":"); - case BLANK: - default: - return null; - } - } - private static class Context { private final DiscordGuild guild; - private final DiscordToMinecraftChatConfig config; + private final BaseChannelConfig config; - public Context(DiscordGuild guild, DiscordToMinecraftChatConfig config) { + public Context(DiscordGuild guild, BaseChannelConfig config) { this.guild = guild; this.config = config; } diff --git a/common/src/main/java/com/discordsrv/common/core/placeholder/result/ComponentResultStringifier.java b/common/src/main/java/com/discordsrv/common/core/placeholder/result/ComponentResultStringifier.java index e47fa05e..7c7d227c 100644 --- a/common/src/main/java/com/discordsrv/common/core/placeholder/result/ComponentResultStringifier.java +++ b/common/src/main/java/com/discordsrv/common/core/placeholder/result/ComponentResultStringifier.java @@ -49,7 +49,7 @@ public class ComponentResultStringifier implements PlaceholderResultMapper { case PLAIN: return discordSRV.componentFactory().plainSerializer().serialize(component); case DISCORD: - return new FormattedText(discordSRV.componentFactory().discordSerializer().serialize(component)); + return new FormattedText(discordSRV.componentFactory().discordSerialize(component)); case ANSI: return discordSRV.componentFactory().ansiSerializer().serialize(component); case LEGACY: diff --git a/common/src/main/java/com/discordsrv/common/discord/api/entity/message/ReceivedDiscordMessageImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/entity/message/ReceivedDiscordMessageImpl.java index e129a646..e45e5a9e 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/entity/message/ReceivedDiscordMessageImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/entity/message/ReceivedDiscordMessageImpl.java @@ -282,7 +282,7 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage { return null; } - Component component = discordSRV.componentFactory().minecraftSerialize(getGuild(), config.discordToMinecraft, content); + Component component = discordSRV.componentFactory().minecraftSerialize(getGuild(), config, content); String replyFormat = config.discordToMinecraft.replyFormat; return ComponentUtil.fromAPI( diff --git a/common/src/main/java/com/discordsrv/common/feature/console/message/ConsoleMessage.java b/common/src/main/java/com/discordsrv/common/feature/console/message/ConsoleMessage.java index efed2c66..c3bb8000 100644 --- a/common/src/main/java/com/discordsrv/common/feature/console/message/ConsoleMessage.java +++ b/common/src/main/java/com/discordsrv/common/feature/console/message/ConsoleMessage.java @@ -71,7 +71,7 @@ public class ConsoleMessage { public String asMarkdown() { Component component = builder.build(); - return discordSRV.componentFactory().discordSerializer().serialize(component); + return discordSRV.componentFactory().discordSerialize(component); } public String asAnsi() { diff --git a/common/src/main/java/com/discordsrv/common/feature/mention/CachedMention.java b/common/src/main/java/com/discordsrv/common/feature/mention/CachedMention.java new file mode 100644 index 00000000..debe41e9 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/feature/mention/CachedMention.java @@ -0,0 +1,87 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.feature.mention; + +import java.util.Objects; +import java.util.regex.Pattern; + +public class CachedMention { + + private final Pattern search; + private final int searchLength; + private final String mention; + private final Type type; + private final long id; + + public CachedMention(String search, String mention, Type type, long id) { + this.search = Pattern.compile(search, Pattern.LITERAL); + this.searchLength = search.length(); + this.mention = mention; + this.type = type; + this.id = id; + } + + public String plain() { + return search.pattern(); + } + + public Pattern search() { + return search; + } + + public int searchLength() { + return searchLength; + } + + public String mention() { + return mention; + } + + public Type type() { + return type; + } + + public long id() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CachedMention that = (CachedMention) o; + return type == that.type && id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(id, type); + } + + @Override + public String toString() { + return "CachedMention{pattern=" + search.pattern() + ",mention=" + mention + "}"; + } + + public enum Type { + USER, + CHANNEL, + ROLE + } +} diff --git a/common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/minecrafttodiscord/MentionCachingModule.java b/common/src/main/java/com/discordsrv/common/feature/mention/MentionCachingModule.java similarity index 74% rename from common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/minecrafttodiscord/MentionCachingModule.java rename to common/src/main/java/com/discordsrv/common/feature/mention/MentionCachingModule.java index 8b73d3c7..a6b272bc 100644 --- a/common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/minecrafttodiscord/MentionCachingModule.java +++ b/common/src/main/java/com/discordsrv/common/feature/mention/MentionCachingModule.java @@ -16,14 +16,17 @@ * along with this program. If not, see . */ -package com.discordsrv.common.feature.messageforwarding.game.minecrafttodiscord; +package com.discordsrv.common.feature.mention; import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; import com.discordsrv.api.eventbus.Subscribe; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.abstraction.player.IPlayer; import com.discordsrv.common.config.main.channels.MinecraftToDiscordChatConfig; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.core.module.type.AbstractModule; +import com.discordsrv.common.permission.game.Permission; +import com.discordsrv.common.util.CompletableFutureUtil; import com.github.benmanes.caffeine.cache.Cache; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; @@ -40,18 +43,23 @@ import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameE import net.dv8tion.jda.api.events.role.RoleCreateEvent; import net.dv8tion.jda.api.events.role.RoleDeleteEvent; import net.dv8tion.jda.api.events.role.update.RoleUpdateNameEvent; +import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; public class MentionCachingModule extends AbstractModule { + private static final Pattern USER_MENTION_PATTERN = Pattern.compile("@[a-z0-9_.]{2,32}"); + private final Map> memberMentions = new ConcurrentHashMap<>(); - private final Map> memberMentionsCache = new ConcurrentHashMap<>(); + private final Map> memberMentionsCache = new ConcurrentHashMap<>(); private final Map> roleMentions = new ConcurrentHashMap<>(); private final Map> channelMentions = new ConcurrentHashMap<>(); @@ -91,8 +99,7 @@ public class MentionCachingModule extends AbstractModule { continue; } - MinecraftToDiscordChatConfig.Mentions mentions = config.mentions; - if (mentions.roles || mentions.users || mentions.channels) { + if (config.mentions.anyCaching()) { return true; } } @@ -106,10 +113,58 @@ public class MentionCachingModule extends AbstractModule { channelMentions.clear(); } + public CompletableFuture> lookup( + MinecraftToDiscordChatConfig.Mentions config, + Guild guild, + IPlayer player, + Component message + ) { + List mentions = new ArrayList<>(); + if (config.users) { + mentions.addAll(getMemberMentions(guild).values()); + } + + List>> futures = new ArrayList<>(); + if (config.users && config.uncachedUsers && player.hasPermission(Permission.MENTION_USER_LOOKUP)) { + String messageContent = discordSRV.componentFactory().plainSerializer().serialize(message); + Matcher matcher = USER_MENTION_PATTERN.matcher(messageContent); + while (matcher.find()) { + String mention = matcher.group(); + boolean perfectMatch = false; + for (CachedMention cachedMention : mentions) { + if (cachedMention.search().matcher(mention).matches()) { + perfectMatch = true; + break; + } + } + if (!perfectMatch) { + futures.add(lookupMemberMentions(guild, mention)); + } + } + } + + if (config.roles) { + mentions.addAll(getRoleMentions(guild).values()); + } + if (config.channels) { + mentions.addAll(getChannelMentions(guild).values()); + } + + return CompletableFutureUtil.combine(futures).thenApply(lists -> { + lists.forEach(mentions::addAll); + + // From longest to shortest + return mentions.stream() + .sorted(Comparator.comparingInt(mention -> ((CachedMention) mention).searchLength()).reversed()) + .collect(Collectors.toList()); + }); + } + @Subscribe public void onGuildDelete(GuildLeaveEvent event) { long guildId = event.getGuild().getIdLong(); memberMentions.remove(guildId); + memberMentionsCache.remove(guildId); roleMentions.remove(guildId); channelMentions.remove(guildId); } @@ -118,27 +173,31 @@ public class MentionCachingModule extends AbstractModule { // Member // - public CompletableFuture> lookupMemberMentions(Guild guild, String mention) { + private CompletableFuture> lookupMemberMentions(Guild guild, String mention) { + Cache cache = memberMentionsCache.computeIfAbsent(guild.getIdLong(), key -> discordSRV.caffeineBuilder() + .expireAfterAccess(10, TimeUnit.MINUTES) + .build() + ); + CachedMention cached = cache.getIfPresent(mention); + if (cached != null) { + return CompletableFuture.completedFuture(Collections.singletonList(cached)); + } + CompletableFuture> memberFuture = new CompletableFuture<>(); guild.retrieveMembersByPrefix(mention.substring(1), 100) .onSuccess(memberFuture::complete).onError(memberFuture::completeExceptionally); - Cache cache = memberMentionsCache.computeIfAbsent(guild.getIdLong(), key -> discordSRV.caffeineBuilder() - .expireAfterAccess(10, TimeUnit.MINUTES) - .build() - ); - return memberFuture.thenApply(members -> { List cachedMentions = new ArrayList<>(); for (Member member : members) { - CachedMention cachedMention = cache.get(member.getIdLong(), k -> convertMember(member)); + CachedMention cachedMention = cache.get(member.getUser().getName(), k -> convertMember(member)); cachedMentions.add(cachedMention); } return cachedMentions; }); } - public Map getMemberMentions(Guild guild) { + private Map getMemberMentions(Guild guild) { return memberMentions.computeIfAbsent(guild.getIdLong(), key -> { Map mentions = new LinkedHashMap<>(); for (Member member : guild.getMembers()) { @@ -152,6 +211,7 @@ public class MentionCachingModule extends AbstractModule { return new CachedMention( "@" + member.getUser().getName(), member.getAsMention(), + CachedMention.Type.USER, member.getIdLong() ); } @@ -187,7 +247,7 @@ public class MentionCachingModule extends AbstractModule { // Role // - public Map getRoleMentions(Guild guild) { + private Map getRoleMentions(Guild guild) { return roleMentions.computeIfAbsent(guild.getIdLong(), key -> { Map mentions = new LinkedHashMap<>(); for (Role role : guild.getRoles()) { @@ -201,6 +261,7 @@ public class MentionCachingModule extends AbstractModule { return new CachedMention( "@" + role.getName(), role.getAsMention(), + CachedMention.Type.ROLE, role.getIdLong() ); } @@ -227,7 +288,7 @@ public class MentionCachingModule extends AbstractModule { // Channel // - public Map getChannelMentions(Guild guild) { + private Map getChannelMentions(Guild guild) { return channelMentions.computeIfAbsent(guild.getIdLong(), key -> { Map mentions = new LinkedHashMap<>(); for (GuildChannel channel : guild.getChannels()) { @@ -246,6 +307,7 @@ public class MentionCachingModule extends AbstractModule { return new CachedMention( "#" + channel.getName(), channel.getAsMention(), + CachedMention.Type.CHANNEL, channel.getIdLong() ); } @@ -279,53 +341,4 @@ public class MentionCachingModule extends AbstractModule { GuildChannel channel = (GuildChannel) event.getChannel(); getChannelMentions(event.getGuild()).remove(channel.getIdLong()); } - - public static class CachedMention { - - private final Pattern search; - private final int searchLength; - private final String mention; - private final long id; - - public CachedMention(String search, String mention, long id) { - this.search = Pattern.compile(search, Pattern.LITERAL); - this.searchLength = search.length(); - this.mention = mention; - this.id = id; - } - - public Pattern search() { - return search; - } - - public int searchLength() { - return searchLength; - } - - public String mention() { - return mention; - } - - public long id() { - return id; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CachedMention that = (CachedMention) o; - return id == that.id; - } - - @Override - public int hashCode() { - return Objects.hash(id); - } - - @Override - public String toString() { - return "CachedMention{pattern=" + search.pattern() + ",mention=" + mention + "}"; - } - } } diff --git a/common/src/main/java/com/discordsrv/common/feature/mention/MentionGameRenderingModule.java b/common/src/main/java/com/discordsrv/common/feature/mention/MentionGameRenderingModule.java new file mode 100644 index 00000000..ae7670f8 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/feature/mention/MentionGameRenderingModule.java @@ -0,0 +1,126 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.feature.mention; + +import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel; +import com.discordsrv.api.discord.entity.guild.DiscordGuild; +import com.discordsrv.api.eventbus.Subscribe; +import com.discordsrv.api.events.message.render.GameChatRenderEvent; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.abstraction.player.IPlayer; +import com.discordsrv.common.config.main.channels.MinecraftToDiscordChatConfig; +import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; +import com.discordsrv.common.config.main.channels.base.IChannelConfig; +import com.discordsrv.common.config.main.generic.MentionsConfig; +import com.discordsrv.common.core.logging.NamedLogger; +import com.discordsrv.common.core.module.type.AbstractModule; +import com.discordsrv.common.util.ComponentUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextReplacementConfig; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +public class MentionGameRenderingModule extends AbstractModule { + + public MentionGameRenderingModule(DiscordSRV discordSRV) { + super(discordSRV, new NamedLogger(discordSRV, "MENTION_ANNOTATION")); + } + + @Override + public boolean isEnabled() { + for (BaseChannelConfig channelConfig : discordSRV.channelConfig().getAllChannels()) { + MinecraftToDiscordChatConfig config = channelConfig.minecraftToDiscord; + if (!config.enabled) { + continue; + } + + MinecraftToDiscordChatConfig.Mentions mentions = config.mentions; + if (mentions.renderMentionsInGame && mentions.anyCaching()) { + return true; + } + + } + return false; + } + + @Subscribe + public void onGameChatAnnotate(GameChatRenderEvent event) { + if (checkCancellation(event) || checkProcessor(event)) { + return; + } + + BaseChannelConfig config = discordSRV.channelConfig().get(event.getChannel()); + if (!(config instanceof IChannelConfig) || !config.minecraftToDiscord.mentions.renderMentionsInGame) { + return; + } + + MentionCachingModule module = discordSRV.getModule(MentionCachingModule.class); + if (module == null) { + return; + } + + List channels = discordSRV.destinations() + .lookupDestination(((IChannelConfig) config).destination(), true, true) + .join(); + Set guilds = new LinkedHashSet<>(); + for (DiscordGuildMessageChannel channel : channels) { + guilds.add(channel.getGuild()); + } + + Component component = ComponentUtil.fromAPI(event.getMessage()); + List cachedMentions = new ArrayList<>(); + for (DiscordGuild guild : guilds) { + cachedMentions.addAll( + module.lookup( + config.minecraftToDiscord.mentions, + guild.asJDA(), + (IPlayer) event.getPlayer(), + component + ).join() + ); + } + + MentionsConfig mentionsConfig = config.mentions; + DiscordGuild guild = guilds.size() == 1 ? guilds.iterator().next() : null; + + for (CachedMention cachedMention : cachedMentions) { + component = component.replaceText( + TextReplacementConfig.builder().match(cachedMention.search()) + .replacement(() -> replacement(cachedMention, mentionsConfig, guild)) + .build() + ); + } + event.process(ComponentUtil.toAPI(component)); + } + + private Component replacement(CachedMention mention, MentionsConfig config, DiscordGuild guild) { + switch (mention.type()) { + case ROLE: + return discordSRV.componentFactory().makeRoleMention(mention.id(), config.role); + case USER: + return discordSRV.componentFactory().makeUserMention(mention.id(), config.user, guild); + case CHANNEL: + return discordSRV.componentFactory().makeChannelMention(mention.id(), config.channel); + } + return Component.text(mention.plain()); + } +} diff --git a/common/src/main/java/com/discordsrv/common/feature/messageforwarding/discord/DiscordChatMessageModule.java b/common/src/main/java/com/discordsrv/common/feature/messageforwarding/discord/DiscordChatMessageModule.java index 271b108b..5c8d76bf 100644 --- a/common/src/main/java/com/discordsrv/common/feature/messageforwarding/discord/DiscordChatMessageModule.java +++ b/common/src/main/java/com/discordsrv/common/feature/messageforwarding/discord/DiscordChatMessageModule.java @@ -204,7 +204,7 @@ public class DiscordChatMessageModule extends AbstractModule { return; } - Component messageComponent = discordSRV.componentFactory().minecraftSerialize(guild, chatConfig, finalMessage); + Component messageComponent = discordSRV.componentFactory().minecraftSerialize(guild, channelConfig, finalMessage); if (ComponentUtil.isEmpty(messageComponent) && !attachments) { // Check empty-ness again after rendering return; diff --git a/common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/minecrafttodiscord/MinecraftToDiscordChatModule.java b/common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/MinecraftToDiscordChatModule.java similarity index 68% rename from common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/minecrafttodiscord/MinecraftToDiscordChatModule.java rename to common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/MinecraftToDiscordChatModule.java index 3d5da430..40664d9c 100644 --- a/common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/minecrafttodiscord/MinecraftToDiscordChatModule.java +++ b/common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/MinecraftToDiscordChatModule.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.discordsrv.common.feature.messageforwarding.game.minecrafttodiscord; +package com.discordsrv.common.feature.messageforwarding.game; import com.discordsrv.api.channel.GameChannel; import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel; @@ -31,14 +31,15 @@ import com.discordsrv.api.eventbus.Subscribe; import com.discordsrv.api.events.message.forward.game.GameChatMessageForwardedEvent; import com.discordsrv.api.events.message.receive.game.GameChatMessageReceiveEvent; import com.discordsrv.api.placeholder.format.FormattedText; +import com.discordsrv.api.placeholder.format.PlainPlaceholderFormat; import com.discordsrv.api.placeholder.util.Placeholders; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.abstraction.player.IPlayer; import com.discordsrv.common.config.main.channels.MinecraftToDiscordChatConfig; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; -import com.discordsrv.common.feature.messageforwarding.game.AbstractGameMessageModule; +import com.discordsrv.common.feature.mention.CachedMention; +import com.discordsrv.common.feature.mention.MentionCachingModule; import com.discordsrv.common.permission.game.Permission; -import com.discordsrv.common.util.CompletableFutureUtil; import com.discordsrv.common.util.ComponentUtil; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Role; @@ -47,9 +48,6 @@ import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; public class MinecraftToDiscordChatModule extends AbstractGameMessageModule { @@ -114,8 +112,6 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule getMessageForGuild( MinecraftToDiscordChatConfig config, SendableDiscordMessage.Builder format, @@ -124,29 +120,10 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule>> futures = new ArrayList<>(); - - String messageContent = discordSRV.componentFactory().plainSerializer().serialize(message); - Matcher matcher = MENTION_PATTERN.matcher(messageContent); - while (matcher.find()) { - futures.add(mentionCaching.lookupMemberMentions(guild, matcher.group())); - } - - if (!futures.isEmpty()) { - return CompletableFutureUtil.combine(futures).thenApply(values -> { - Set mentions = new LinkedHashSet<>(); - for (List value : values) { - mentions.addAll(value); - } - - return getMessageForGuildWithMentions(config, format, guild, message, player, context, mentions); - }); - } + if (mentionCaching != null) { + return mentionCaching.lookup(config.mentions, guild, player, message) + .thenApply(mentions -> getMessageForGuildWithMentions(config, format, guild, message, player, context, mentions)); } return CompletableFuture.completedFuture(getMessageForGuildWithMentions(config, format, guild, message, player, context, null)); @@ -159,31 +136,9 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule memberMentions + List mentions ) { MinecraftToDiscordChatConfig.Mentions mentionConfig = config.mentions; - Set mentions = new LinkedHashSet<>(); - - if (memberMentions != null) { - mentions.addAll(memberMentions); - } - - MentionCachingModule mentionCaching = discordSRV.getModule(MentionCachingModule.class); - if (mentionCaching != null) { - if (mentionConfig.roles) { - mentions.addAll(mentionCaching.getRoleMentions(guild).values()); - } - if (mentionConfig.channels) { - mentions.addAll(mentionCaching.getChannelMentions(guild).values()); - } - if (mentionConfig.users) { - mentions.addAll(mentionCaching.getMemberMentions(guild).values()); - } - } - - List orderedMentions = mentions.stream() - .sorted(Comparator.comparingInt(mention -> ((MentionCachingModule.CachedMention) mention).searchLength()).reversed()) - .collect(Collectors.toList()); List allowedMentions = new ArrayList<>(); if (mentionConfig.users && player.hasPermission(Permission.MENTION_USER)) { @@ -211,29 +166,24 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule { - String convertedComponent = convertComponent(config, message); - Placeholders channelMessagePlaceholders = new Placeholders( - DiscordFormattingUtil.escapeMentions(convertedComponent)); + String content = PlainPlaceholderFormat.supplyWith( + PlainPlaceholderFormat.Formatting.DISCORD, + () -> discordSRV.placeholderService().getResultAsCharSequence(message).toString() + ); + Placeholders messagePlaceholders = new Placeholders(DiscordFormattingUtil.escapeMentions(content)); + config.contentRegexFilters.forEach(messagePlaceholders::replaceAll); - // From longest to shortest - orderedMentions.forEach(mention -> channelMessagePlaceholders.replaceAll(mention.search(), mention.mention())); + if (mentions != null) { + mentions.forEach(mention -> messagePlaceholders.replaceAll(mention.search(), mention.mention())); + } - String finalMessage = channelMessagePlaceholders.toString(); + String finalMessage = messagePlaceholders.toString(); return new FormattedText(preventEveryoneMentions(everyone, finalMessage)); }) .applyPlaceholderService() .build(); } - public String convertComponent(MinecraftToDiscordChatConfig config, Component component) { - String content = discordSRV.placeholderService().getResultAsCharSequence(component).toString(); - - Placeholders messagePlaceholders = new Placeholders(content); - config.contentRegexFilters.forEach(messagePlaceholders::replaceAll); - - return messagePlaceholders.toString(); - } - private String preventEveryoneMentions(boolean everyoneAllowed, String message) { if (everyoneAllowed) { // Nothing to do diff --git a/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java b/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java index a95f9d60..0a7d71d5 100644 --- a/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java +++ b/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java @@ -45,7 +45,7 @@ import com.discordsrv.common.core.storage.impl.MemoryStorage; import com.discordsrv.common.feature.console.Console; import com.discordsrv.common.feature.debug.data.OnlineMode; import com.discordsrv.common.feature.debug.data.VersionInfo; -import com.discordsrv.common.feature.messageforwarding.game.minecrafttodiscord.MinecraftToDiscordChatModule; +import com.discordsrv.common.feature.messageforwarding.game.MinecraftToDiscordChatModule; import dev.vankka.dependencydownload.classpath.ClasspathAppender; import net.kyori.adventure.audience.Audience; import org.apache.commons.lang3.StringUtils; From 6a07529dfc40a26d1458f1c68c3c75b5cd3342a8 Mon Sep 17 00:00:00 2001 From: Vankka Date: Sat, 10 Aug 2024 14:07:39 +0300 Subject: [PATCH 2/3] Rethink --- .../core/component/ComponentFactory.java | 19 +- .../component/DiscordContentComponent.java | 69 ++++ .../component/DiscordMentionComponent.java | 364 ------------------ .../game/MinecraftToDiscordChatModule.java | 3 +- 4 files changed, 76 insertions(+), 379 deletions(-) create mode 100644 common/src/main/java/com/discordsrv/common/core/component/DiscordContentComponent.java delete mode 100644 common/src/main/java/com/discordsrv/common/core/component/DiscordMentionComponent.java diff --git a/common/src/main/java/com/discordsrv/common/core/component/ComponentFactory.java b/common/src/main/java/com/discordsrv/common/core/component/ComponentFactory.java index 9dbe39ed..5cb12d12 100644 --- a/common/src/main/java/com/discordsrv/common/core/component/ComponentFactory.java +++ b/common/src/main/java/com/discordsrv/common/core/component/ComponentFactory.java @@ -171,7 +171,7 @@ public class ComponentFactory implements MinecraftComponentFactory { JDA jda = discordSRV.jda(); GuildChannel guildChannel = jda != null ? jda.getGuildChannelById(id) : null; - return DiscordMentionComponent.of("<#" + Long.toUnsignedString(id) + ">").append(ComponentUtil.fromAPI( + return DiscordContentComponent.of("<#" + Long.toUnsignedString(id) + ">", ComponentUtil.fromAPI( discordSRV.componentFactory() .textBuilder(guildChannel != null ? format.format : format.unknownFormat) .addContext(guildChannel) @@ -185,7 +185,7 @@ public class ComponentFactory implements MinecraftComponentFactory { DiscordUser user = discordSRV.discordAPI().getUserById(id); DiscordGuildMember member = guild.getMemberById(id); - return DiscordMentionComponent.of("<@" + Long.toUnsignedString(id) + ">").append(ComponentUtil.fromAPI( + return DiscordContentComponent.of("<@" + Long.toUnsignedString(id) + ">", ComponentUtil.fromAPI( discordSRV.componentFactory() .textBuilder(user != null ? (member != null ? format.format : format.formatGlobal) : format.unknownFormat) .addContext(user, member) @@ -197,7 +197,7 @@ public class ComponentFactory implements MinecraftComponentFactory { public Component makeRoleMention(long id, MentionsConfig.Format format) { DiscordRole role = discordSRV.discordAPI().getRoleById(id); - return DiscordMentionComponent.of("<@&" + Long.toUnsignedString(id) + ">").append(ComponentUtil.fromAPI( + return DiscordContentComponent.of("<@&" + Long.toUnsignedString(id) + ">", ComponentUtil.fromAPI( discordSRV.componentFactory() .textBuilder(role != null ? format.format : format.unknownFormat) .addContext(role) @@ -206,7 +206,6 @@ public class ComponentFactory implements MinecraftComponentFactory { )); } - @SuppressWarnings("DataFlowIssue") // isProcessed = processed is not null public Component makeEmoteMention(long id, MentionsConfig.EmoteBehaviour behaviour) { DiscordCustomEmoji emoji = discordSRV.discordAPI().getEmojiById(id); if (emoji == null) { @@ -217,13 +216,12 @@ public class ComponentFactory implements MinecraftComponentFactory { discordSRV.eventBus().publish(event); if (event.isProcessed()) { - return DiscordMentionComponent.of(emoji.asJDA().getAsMention()) - .append(ComponentUtil.fromAPI(event.getRenderedEmojiFromProcessing())); + return DiscordContentComponent.of(emoji.asJDA().getAsMention(), ComponentUtil.fromAPI(event.getRenderedEmojiFromProcessing())); } switch (behaviour) { case NAME: - return DiscordMentionComponent.of(emoji.asJDA().getAsMention()).append(Component.text(":" + emoji.getName() + ":")); + return DiscordContentComponent.of(emoji.asJDA().getAsMention(), Component.text(":" + emoji.getName() + ":")); case BLANK: default: return null; @@ -235,12 +233,7 @@ public class ComponentFactory implements MinecraftComponentFactory { } public String discordSerialize(Component component) { - Component mapped = Component.text().append(component).mapChildrenDeep(comp -> { - if (comp instanceof DiscordMentionComponent) { - return Component.text(((DiscordMentionComponent) comp).mention()); - } - return comp; - }).children().get(0); + Component mapped = DiscordContentComponent.remapToDiscord(component); return discordSerializer().serialize(mapped); } diff --git a/common/src/main/java/com/discordsrv/common/core/component/DiscordContentComponent.java b/common/src/main/java/com/discordsrv/common/core/component/DiscordContentComponent.java new file mode 100644 index 00000000..f4257a09 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/core/component/DiscordContentComponent.java @@ -0,0 +1,69 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.core.component; + +import com.discordsrv.api.discord.util.DiscordFormattingUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.TextComponent; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * Possibly removable after adventure #842 + * + * @see ComponentFactory#discordSerialize(Component) + */ +public class DiscordContentComponent { + + private static final String PREFIX = "DiscordSRV:discord_content:"; + + @NotNull + public static TextComponent of(@NotNull String discordContent, ComponentLike gameComponent) { + return Component.text() + .append(Component.text().insertion(PREFIX + discordContent)) + .append(gameComponent) + .build(); + } + + public static Component remapToDiscord(@NotNull Component component) { + List children = component.children(); + if (component instanceof TextComponent) { + if (children.size() == 2) { + Component first = children.get(0); + String insertion = first.insertion(); + if (insertion != null && insertion.startsWith(PREFIX)) { + return Component.text(insertion.substring(PREFIX.length())); + } + } + + String content = ((TextComponent) component).content(); + component = ((TextComponent) component).content(DiscordFormattingUtil.escapeMentions(content)); + } + + List newChildren = new ArrayList<>(); + for (Component child : children) { + newChildren.add(remapToDiscord(child)); + } + + return component.children(newChildren); + } +} diff --git a/common/src/main/java/com/discordsrv/common/core/component/DiscordMentionComponent.java b/common/src/main/java/com/discordsrv/common/core/component/DiscordMentionComponent.java deleted file mode 100644 index 2c2dfdcd..00000000 --- a/common/src/main/java/com/discordsrv/common/core/component/DiscordMentionComponent.java +++ /dev/null @@ -1,364 +0,0 @@ -/* - * This file is part of DiscordSRV, licensed under the GPLv3 License - * Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.discordsrv.common.core.component; - -import net.kyori.adventure.key.Key; -import net.kyori.adventure.text.*; -import net.kyori.adventure.text.event.ClickEvent; -import net.kyori.adventure.text.event.HoverEventSource; -import net.kyori.adventure.text.format.Style; -import net.kyori.adventure.text.format.TextColor; -import net.kyori.adventure.text.format.TextDecoration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.Unmodifiable; - -import java.util.*; -import java.util.function.Consumer; -import java.util.function.Function; - -import static java.util.Objects.requireNonNull; - -/** - * An Adventure {@link Component} that holds a Discord mention whilst being disguised as a {@link TextComponent} - * for compatibility with serializers that don't know how to deal with custom Component types. - *

- * Possibly removable after adventure #842 - * - * @see ComponentFactory#discordSerialize(Component) - */ -public class DiscordMentionComponent implements TextComponent { - - @NotNull - public static DiscordMentionComponent of(@NotNull String mention) { - return new DiscordMentionComponent(new ArrayList<>(), Style.empty(), mention); - } - - @NotNull - public static Builder builder(@NotNull String mention) { - return new Builder(new ArrayList<>(), Style.empty(), mention); - } - - private final List children; - private final Style style; - private final String mention; - - private DiscordMentionComponent(List children, Style style, String mention) { - this.children = ComponentLike.asComponents(children, IS_NOT_EMPTY); - this.style = style; - this.mention = mention; - } - - @Override - @Deprecated // NOOP - public @NotNull String content() { - return ""; - } - - @Override - @Deprecated // NOOP - public @NotNull DiscordMentionComponent content(@NotNull String content) { - return this; - } - - @Override - public @NotNull Component asComponent() { - return TextComponent.super.asComponent(); - } - - @Override - public @NotNull Builder toBuilder() { - return new Builder(children, style, mention); - } - - @Override - public @Unmodifiable @NotNull List children() { - return children; - } - - @Override - public @NotNull DiscordMentionComponent children(@NotNull List children) { - return new DiscordMentionComponent(children, style, mention); - } - - @Override - public @NotNull Style style() { - return style; - } - - @Override - public @NotNull DiscordMentionComponent style(@NotNull Style style) { - return new DiscordMentionComponent(children, style, mention); - } - - @NotNull - public String mention() { - return mention; - } - - public @NotNull DiscordMentionComponent mention(@NotNull String mention) { - return new DiscordMentionComponent(children, style, mention); - } - - @Override - public String toString() { - return "DiscordMentionComponent{" + - "children=" + children + - ", style=" + style + - ", mention='" + mention + '\'' + - '}'; - } - - public static class Builder implements TextComponent.Builder { - - private final List children; - private Style.Builder styleBuilder; - private String mention; - - private Builder(List children, Style style, String mention) { - this.children = children; - this.styleBuilder = style.toBuilder(); - this.mention = mention; - } - - @Override - @Deprecated // NOOP - public @NotNull String content() { - return ""; - } - - @NotNull - @Override - @Deprecated // NOOP - public DiscordMentionComponent.Builder content(@NotNull String content) { - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder append(@NotNull Component component) { - if (component == Component.empty()) return this; - this.children.add(component); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder append(@NotNull Component @NotNull ... components) { - for (Component component : components) { - append(component); - } - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder append(@NotNull ComponentLike @NotNull ... components) { - for (ComponentLike component : components) { - append(component); - } - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder append(@NotNull Iterable components) { - for (Component child : children) { - append(child); - } - return this; - } - - @Override - public @NotNull List children() { - return children; - } - - @Override - public DiscordMentionComponent.@NotNull Builder style(@NotNull Style style) { - this.styleBuilder = style.toBuilder(); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder style(@NotNull Consumer consumer) { - consumer.accept(styleBuilder); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder font(@Nullable Key font) { - styleBuilder.font(font); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder color(@Nullable TextColor color) { - styleBuilder.color(color); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder colorIfAbsent(@Nullable TextColor color) { - styleBuilder.color(color); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder decoration(@NotNull TextDecoration decoration, TextDecoration.State state) { - styleBuilder.decoration(decoration, state); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder decorationIfAbsent(@NotNull TextDecoration decoration, TextDecoration.State state) { - styleBuilder.decorationIfAbsent(decoration, state); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder clickEvent(@Nullable ClickEvent event) { - styleBuilder.clickEvent(event); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder hoverEvent(@Nullable HoverEventSource source) { - styleBuilder.hoverEvent(source); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder insertion(@Nullable String insertion) { - styleBuilder.insertion(insertion); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder mergeStyle(@NotNull Component that, @NotNull Set merges) { - styleBuilder.merge(that.style(), merges); - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder resetStyle() { - styleBuilder = Style.style(); - return this; - } - - @NotNull - public String mention() { - return mention; - } - - public DiscordMentionComponent.@NotNull Builder mention(@NotNull String mention) { - this.mention = mention; - return this; - } - - @Override - public @NotNull DiscordMentionComponent build() { - return new DiscordMentionComponent(children, styleBuilder.build(), mention); - } - - /* - * Copyright (c) 2017-2023 KyoriPowered - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - @Override - public DiscordMentionComponent.@NotNull Builder applyDeep(@NotNull Consumer> action) { - this.apply(action); - if (this.children == Collections.emptyList()) { - return this; - } - ListIterator it = this.children.listIterator(); - while (it.hasNext()) { - final Component child = it.next(); - if (!(child instanceof BuildableComponent)) { - continue; - } - final ComponentBuilder childBuilder = ((BuildableComponent) child).toBuilder(); - childBuilder.applyDeep(action); - it.set(childBuilder.build()); - } - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder mapChildren( - @NotNull Function, ? extends BuildableComponent> function) { - if (this.children == Collections.emptyList()) { - return this; - } - final ListIterator it = this.children.listIterator(); - while (it.hasNext()) { - final Component child = it.next(); - if (!(child instanceof BuildableComponent)) { - continue; - } - final BuildableComponent mappedChild = requireNonNull(function.apply((BuildableComponent) child), "mappedChild"); - if (child == mappedChild) { - continue; - } - it.set(mappedChild); - } - return this; - } - - @Override - public DiscordMentionComponent.@NotNull Builder mapChildrenDeep( - @NotNull Function, ? extends BuildableComponent> function) { - if (this.children == Collections.emptyList()) { - return this; - } - final ListIterator it = this.children.listIterator(); - while (it.hasNext()) { - final Component child = it.next(); - if (!(child instanceof BuildableComponent)) { - continue; - } - final BuildableComponent mappedChild = requireNonNull(function.apply((BuildableComponent) child), "mappedChild"); - if (mappedChild.children().isEmpty()) { - if (child == mappedChild) { - continue; - } - it.set(mappedChild); - } else { - final ComponentBuilder builder = mappedChild.toBuilder(); - builder.mapChildrenDeep(function); - it.set(builder.build()); - } - } - return this; - } - } -} diff --git a/common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/MinecraftToDiscordChatModule.java b/common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/MinecraftToDiscordChatModule.java index 40664d9c..9aa73768 100644 --- a/common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/MinecraftToDiscordChatModule.java +++ b/common/src/main/java/com/discordsrv/common/feature/messageforwarding/game/MinecraftToDiscordChatModule.java @@ -25,7 +25,6 @@ import com.discordsrv.api.discord.entity.message.AllowedMention; import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessageCluster; import com.discordsrv.api.discord.entity.message.SendableDiscordMessage; -import com.discordsrv.api.discord.util.DiscordFormattingUtil; import com.discordsrv.api.eventbus.EventPriority; import com.discordsrv.api.eventbus.Subscribe; import com.discordsrv.api.events.message.forward.game.GameChatMessageForwardedEvent; @@ -170,7 +169,7 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule discordSRV.placeholderService().getResultAsCharSequence(message).toString() ); - Placeholders messagePlaceholders = new Placeholders(DiscordFormattingUtil.escapeMentions(content)); + Placeholders messagePlaceholders = new Placeholders(content); config.contentRegexFilters.forEach(messagePlaceholders::replaceAll); if (mentions != null) { From 868317dd317cc2409e9a6ecd3a4525d8e5272e5f Mon Sep 17 00:00:00 2001 From: Vankka Date: Sat, 10 Aug 2024 14:07:48 +0300 Subject: [PATCH 3/3] Upgrade adventure-platform-bukkit --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 34fea245..13f866c1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -122,7 +122,7 @@ dependencyResolutionManagement { library('adventure-serializer-ansi', 'net.kyori', 'adventure-text-serializer-ansi').versionRef('adventure') // Adventure Platform - version('adventure-platform', '4.3.3-SNAPSHOT') + version('adventure-platform', '4.3.4') library('adventure-platform-bukkit', 'net.kyori', 'adventure-platform-bukkit').versionRef('adventure-platform') library('adventure-platform-bungee', 'net.kyori', 'adventure-platform-bungeecord').versionRef('adventure-platform') library('adventure-serializer-bungee', 'net.kyori', 'adventure-text-serializer-bungeecord').versionRef('adventure-platform')