Rendering mentions in-game

This commit is contained in:
Vankka 2024-07-28 02:18:17 +03:00
parent 03cbd45993
commit fb426b870c
No known key found for this signature in database
GPG Key ID: 62E48025ED4E7EBB
25 changed files with 992 additions and 251 deletions

View File

@ -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<MinecraftComponent>, 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;
}
}

View File

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

View File

@ -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<AsyncChatEvent> COMPONENT_HANDLE;
private static final PaperComponentHandle<AsyncChatEvent> GET_MESSAGE_HANDLE = makeGet();
private static final MethodHandle SET_MESSAGE_HANDLE = makeSet();
static {
COMPONENT_HANDLE = new PaperComponentHandle<>(
private static PaperComponentHandle<AsyncChatEvent> 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());
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -248,7 +248,7 @@ public class ExecuteCommand implements Consumer<DiscordChatInputInteractionEvent
switch (outputMode) {
default:
case MARKDOWN:
discord = discordSRV.componentFactory().discordSerializer().serialize(component);
discord = discordSRV.componentFactory().discordSerialize(component);
break;
case ANSI:
discord = discordSRV.componentFactory().ansiSerializer().serialize(component);

View File

@ -113,7 +113,7 @@ public abstract class BroadcastCommand implements GameCommandExecutor, GameComma
}
} catch (IllegalArgumentException ignored) {
BaseChannelConfig channelConfig = discordSRV.channelConfig().resolve(null, channel);
CC config = channelConfig instanceof IChannelConfig ? (CC) channelConfig : null;
CC config = channelConfig != null ? (CC) channelConfig : null;
if (config != null) {
future = discordSRV.destinations().lookupDestination(config.destination(), true, false);
@ -203,7 +203,7 @@ public abstract class BroadcastCommand implements GameCommandExecutor, GameComma
.applyPlaceholderService()
.build();
return discordSRV.componentFactory().discordSerializer().serialize(ComponentUtil.fromAPI(component));
return discordSRV.componentFactory().discordSerialize(ComponentUtil.fromAPI(component));
}
}
@ -216,7 +216,7 @@ public abstract class BroadcastCommand implements GameCommandExecutor, GameComma
@Override
public String getContent(String content) {
Component component = GsonComponentSerializer.gson().deserialize(content);
return discordSRV.componentFactory().discordSerializer().serialize(component);
return discordSRV.componentFactory().discordSerialize(component);
}
}
}

View File

@ -21,7 +21,6 @@ package com.discordsrv.common.config.main.channels;
import com.discordsrv.common.config.configurate.annotation.Untranslated;
import com.discordsrv.common.config.configurate.manager.abstraction.ConfigurateConfigManager;
import com.discordsrv.common.config.main.generic.DiscordIgnoresConfig;
import com.discordsrv.common.config.main.generic.MentionsConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ -64,9 +63,6 @@ public class DiscordToMinecraftChatConfig {
@Comment("Users, bots, roles and webhooks to ignore")
public DiscordIgnoresConfig ignores = new DiscordIgnoresConfig();
@Comment("The representations of Discord mentions in-game")
public MentionsConfig mentions = new MentionsConfig();
@Comment("How should unicode emoji be shown in-game:\n"
+ "- hide: hides emojis in-game\n"
+ "- show: shows emojis in-game as is (emojis may not be visible without resource packs)\n"

View File

@ -65,6 +65,9 @@ public class MinecraftToDiscordChatConfig implements IMessageConfig {
@ConfigSerializable
public static class Mentions {
@Comment("Should mentions be rendered in Minecraft when sent in Minecraft")
public boolean renderMentionsInGame = true;
@Comment("If role mentions should be rendered on Discord\n\n"
+ "The player needs one of the below permission to trigger notifications:\n"
+ "- discordsrv.mention.roles.mentionable (for roles which have \"Allow anyone to @mention this role\" enabled)\n"
@ -87,6 +90,9 @@ public class MinecraftToDiscordChatConfig implements IMessageConfig {
+ "The player needs the discordsrv.mention.everyone permission to render the mention and trigger a notification")
public boolean everyone = false;
public boolean anyCaching() {
return roles || channels || users;
}
}
}

View File

@ -20,6 +20,7 @@ package com.discordsrv.common.config.main.channels.base;
import com.discordsrv.common.config.configurate.annotation.Order;
import com.discordsrv.common.config.main.channels.*;
import com.discordsrv.common.config.main.generic.MentionsConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ -32,6 +33,9 @@ public class BaseChannelConfig {
@Order(0)
public DiscordToMinecraftChatConfig discordToMinecraft = new DiscordToMinecraftChatConfig();
@Comment("The representations of Discord mentions in-game")
public MentionsConfig mentions = new MentionsConfig();
public JoinMessageConfig joinMessages() {
return new JoinMessageConfig();
}

View File

@ -33,9 +33,14 @@ public class MentionsConfig {
"[hover:show_text:Click to go to channel][click:open_url:%channel_jump_url%][color:#5865F2]#%channel_name%",
"[color:#5865F2]#Unknown"
);
public Format user = new Format(
"[hover:show_text:Username: @%user_tag%\nRoles: %user_roles:', '|text:'[color:gray][italics:on]None[color][italics]'%][color:#5865F2]@%user_effective_server_name|user_effective_name%",
"[color:#5865F2]@Unknown user"
public FormatUser user = new FormatUser(
"[hover:show_text:Username: @%user_tag% [italics:on][color:gray](Shift+Click to mention)[color][italics:off]\nRoles: %user_roles:', '|text:'[color:gray][italics:on]None[color][italics]'%]"
+ "[insert:@%user_tag%][color:#5865F2]"
+ "@%user_effective_server_name|user_effective_name%",
"[color:#5865F2]@Unknown user",
"[hover:show_text:Username: @%user_tag% [italics:on][color:gray](Shift+Click to mention)[color][italics:off]]"
+ "[insert:@%user_tag%][color:#5865F2]"
+ "@%user_effective_name%"
);
public String messageUrl = "[hover:show_text:Click to go to message][click:open_url:%jump_url%][color:#5865F2]#%channel_name% > ...";
@ -71,4 +76,19 @@ public class MentionsConfig {
this.unknownFormat = unknownFormat;
}
}
@ConfigSerializable
public static class FormatUser extends Format {
@Comment("The format shown in-game for users that cannot be linked to a specific Discord server")
public String formatGlobal = "";
@SuppressWarnings("unused") // Configurate
public FormatUser() {}
public FormatUser(String format, String unknownFormat, String globalFormat) {
super(format, unknownFormat);
this.formatGlobal = globalFormat;
}
}
}

View File

@ -22,19 +22,28 @@ import com.discordsrv.api.component.GameTextBuilder;
import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.api.component.MinecraftComponentAdapter;
import com.discordsrv.api.component.MinecraftComponentFactory;
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.core.component.renderer.DiscordSRVMinecraftRenderer;
import com.discordsrv.common.core.component.translation.Translation;
import com.discordsrv.common.core.component.translation.TranslationRegistry;
import com.discordsrv.common.core.logging.Logger;
import com.discordsrv.common.core.logging.NamedLogger;
import com.discordsrv.common.util.ComponentUtil;
import dev.vankka.enhancedlegacytext.EnhancedLegacyText;
import dev.vankka.mcdiscordreserializer.discord.DiscordSerializer;
import dev.vankka.mcdiscordreserializer.discord.DiscordSerializerOptions;
import dev.vankka.mcdiscordreserializer.minecraft.MinecraftSerializer;
import dev.vankka.mcdiscordreserializer.minecraft.MinecraftSerializerOptions;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.flattener.ComponentFlattener;
@ -155,10 +164,86 @@ public class ComponentFactory implements MinecraftComponentFactory {
return EnhancedLegacyText.get().parse(textInput);
}
public Component minecraftSerialize(DiscordGuild guild, DiscordToMinecraftChatConfig config, String discordMessage) {
// Mentions
@NotNull
public Component makeChannelMention(long id, MentionsConfig.Format format) {
JDA jda = discordSRV.jda();
GuildChannel guildChannel = jda != null ? jda.getGuildChannelById(id) : null;
return DiscordMentionComponent.of("<#" + Long.toUnsignedString(id) + ">").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;
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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.
* <p>
* Possibly removable after <a href="https://github.com/KyoriPowered/adventure/pull/842">adventure #842</a>
*
* @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<Component> children;
private final Style style;
private final String mention;
private DiscordMentionComponent(List<? extends ComponentLike> 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<Component> children() {
return children;
}
@Override
public @NotNull DiscordMentionComponent children(@NotNull List<? extends ComponentLike> 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<Component> children;
private Style.Builder styleBuilder;
private String mention;
private Builder(List<Component> 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<? extends ComponentLike> components) {
for (Component child : children) {
append(child);
}
return this;
}
@Override
public @NotNull List<Component> 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<Style.Builder> 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<Style.Merge> 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<? super ComponentBuilder<?, ?>> action) {
this.apply(action);
if (this.children == Collections.<Component>emptyList()) {
return this;
}
ListIterator<Component> 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<BuildableComponent<?, ?>, ? extends BuildableComponent<?, ?>> function) {
if (this.children == Collections.<Component>emptyList()) {
return this;
}
final ListIterator<Component> 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<BuildableComponent<?, ?>, ? extends BuildableComponent<?, ?>> function) {
if (this.children == Collections.<Component>emptyList()) {
return this;
}
final ListIterator<Component> 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;
}
}
}

View File

@ -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> T getWithContext(
DiscordGuild guild,
DiscordToMinecraftChatConfig config,
BaseChannelConfig config,
Supplier<T> 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;
}

View File

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

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View File

@ -16,14 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<DiscordSRV> {
private static final Pattern USER_MENTION_PATTERN = Pattern.compile("@[a-z0-9_.]{2,32}");
private final Map<Long, Map<Long, CachedMention>> memberMentions = new ConcurrentHashMap<>();
private final Map<Long, Cache<Long, CachedMention>> memberMentionsCache = new ConcurrentHashMap<>();
private final Map<Long, Cache<String, CachedMention>> memberMentionsCache = new ConcurrentHashMap<>();
private final Map<Long, Map<Long, CachedMention>> roleMentions = new ConcurrentHashMap<>();
private final Map<Long, Map<Long, CachedMention>> channelMentions = new ConcurrentHashMap<>();
@ -91,8 +99,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
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<DiscordSRV> {
channelMentions.clear();
}
public CompletableFuture<List<CachedMention>> lookup(
MinecraftToDiscordChatConfig.Mentions config,
Guild guild,
IPlayer player,
Component message
) {
List<CachedMention> mentions = new ArrayList<>();
if (config.users) {
mentions.addAll(getMemberMentions(guild).values());
}
List<CompletableFuture<List<CachedMention>>> 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<DiscordSRV> {
// Member
//
public CompletableFuture<List<CachedMention>> lookupMemberMentions(Guild guild, String mention) {
private CompletableFuture<List<CachedMention>> lookupMemberMentions(Guild guild, String mention) {
Cache<String, CachedMention> 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<List<Member>> memberFuture = new CompletableFuture<>();
guild.retrieveMembersByPrefix(mention.substring(1), 100)
.onSuccess(memberFuture::complete).onError(memberFuture::completeExceptionally);
Cache<Long, CachedMention> cache = memberMentionsCache.computeIfAbsent(guild.getIdLong(), key -> discordSRV.caffeineBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build()
);
return memberFuture.thenApply(members -> {
List<CachedMention> 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<Long, CachedMention> getMemberMentions(Guild guild) {
private Map<Long, CachedMention> getMemberMentions(Guild guild) {
return memberMentions.computeIfAbsent(guild.getIdLong(), key -> {
Map<Long, CachedMention> mentions = new LinkedHashMap<>();
for (Member member : guild.getMembers()) {
@ -152,6 +211,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
return new CachedMention(
"@" + member.getUser().getName(),
member.getAsMention(),
CachedMention.Type.USER,
member.getIdLong()
);
}
@ -187,7 +247,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
// Role
//
public Map<Long, CachedMention> getRoleMentions(Guild guild) {
private Map<Long, CachedMention> getRoleMentions(Guild guild) {
return roleMentions.computeIfAbsent(guild.getIdLong(), key -> {
Map<Long, CachedMention> mentions = new LinkedHashMap<>();
for (Role role : guild.getRoles()) {
@ -201,6 +261,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
return new CachedMention(
"@" + role.getName(),
role.getAsMention(),
CachedMention.Type.ROLE,
role.getIdLong()
);
}
@ -227,7 +288,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
// Channel
//
public Map<Long, CachedMention> getChannelMentions(Guild guild) {
private Map<Long, CachedMention> getChannelMentions(Guild guild) {
return channelMentions.computeIfAbsent(guild.getIdLong(), key -> {
Map<Long, CachedMention> mentions = new LinkedHashMap<>();
for (GuildChannel channel : guild.getChannels()) {
@ -246,6 +307,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
return new CachedMention(
"#" + channel.getName(),
channel.getAsMention(),
CachedMention.Type.CHANNEL,
channel.getIdLong()
);
}
@ -279,53 +341,4 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
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 + "}";
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<DiscordSRV> {
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<DiscordGuildMessageChannel> channels = discordSRV.destinations()
.lookupDestination(((IChannelConfig) config).destination(), true, true)
.join();
Set<DiscordGuild> guilds = new LinkedHashSet<>();
for (DiscordGuildMessageChannel channel : channels) {
guilds.add(channel.getGuild());
}
Component component = ComponentUtil.fromAPI(event.getMessage());
List<CachedMention> 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());
}
}

View File

@ -204,7 +204,7 @@ public class DiscordChatMessageModule extends AbstractModule<DiscordSRV> {
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;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<MinecraftToDiscordChatConfig, GameChatMessageReceiveEvent> {
@ -114,8 +112,6 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<Mine
@Override
public void setPlaceholders(MinecraftToDiscordChatConfig config, GameChatMessageReceiveEvent event, SendableDiscordMessage.Formatter formatter) {}
private final Pattern MENTION_PATTERN = Pattern.compile("@\\S+");
private CompletableFuture<SendableDiscordMessage> getMessageForGuild(
MinecraftToDiscordChatConfig config,
SendableDiscordMessage.Builder format,
@ -124,29 +120,10 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<Mine
IPlayer player,
Object[] context
) {
MinecraftToDiscordChatConfig.Mentions mentionConfig = config.mentions;
MentionCachingModule mentionCaching = discordSRV.getModule(MentionCachingModule.class);
if (mentionCaching != null && mentionConfig.users && mentionConfig.uncachedUsers
&& player.hasPermission(Permission.MENTION_USER_LOOKUP)) {
List<CompletableFuture<List<MentionCachingModule.CachedMention>>> 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<MentionCachingModule.CachedMention> mentions = new LinkedHashSet<>();
for (List<MentionCachingModule.CachedMention> 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<Mine
Component message,
IPlayer player,
Object[] context,
Set<MentionCachingModule.CachedMention> memberMentions
List<CachedMention> mentions
) {
MinecraftToDiscordChatConfig.Mentions mentionConfig = config.mentions;
Set<MentionCachingModule.CachedMention> 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<MentionCachingModule.CachedMention> orderedMentions = mentions.stream()
.sorted(Comparator.comparingInt(mention -> ((MentionCachingModule.CachedMention) mention).searchLength()).reversed())
.collect(Collectors.toList());
List<AllowedMention> allowedMentions = new ArrayList<>();
if (mentionConfig.users && player.hasPermission(Permission.MENTION_USER)) {
@ -211,29 +166,24 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<Mine
.toFormatter()
.addContext(context)
.addPlaceholder("message", () -> {
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

View File

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