Merge branch 'mentions-ingame'

This commit is contained in:
Vankka 2024-08-10 14:08:01 +03:00
commit 9cf654e23d
No known key found for this signature in database
GPG Key ID: 62E48025ED4E7EBB
26 changed files with 691 additions and 253 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 { 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.api.component.MinecraftComponent;
import com.discordsrv.bukkit.component.PaperComponentHandle; 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 io.papermc.paper.event.player.AsyncChatEvent;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; 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 { 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 { private static PaperComponentHandle<AsyncChatEvent> makeGet() {
COMPONENT_HANDLE = new PaperComponentHandle<>( return new PaperComponentHandle<>(
AsyncChatEvent.class, AsyncChatEvent.class,
"message", "message",
null 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 IBukkitChatForwarder listener;
private final Logger logger;
public PaperChatListener(IBukkitChatForwarder listener) { public PaperChatListener(IBukkitChatForwarder listener, Logger logger) {
this.listener = listener; 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) @EventHandler(priority = EventPriority.MONITOR)
public void onAsyncChat(AsyncChatEvent event) { public void onAsyncChatForward(AsyncChatEvent event) {
MinecraftComponent component = COMPONENT_HANDLE.getComponent(event); MinecraftComponent component = GET_MESSAGE_HANDLE.getComponent(event);
listener.publishEvent(event, event.getPlayer(), component, event.isCancelled()); 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.configurate.manager.MessagesConfigManager;
import com.discordsrv.common.config.messages.MessagesConfig; import com.discordsrv.common.config.messages.MessagesConfig;
import com.discordsrv.common.feature.debug.data.OnlineMode; 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 net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.Server; import org.bukkit.Server;
import org.bukkit.plugin.ServicePriority; import org.bukkit.plugin.ServicePriority;

View File

@ -19,10 +19,12 @@
package com.discordsrv.bukkit.listener.chat; package com.discordsrv.bukkit.listener.chat;
import com.discordsrv.api.component.MinecraftComponent; 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.api.events.message.receive.game.GameChatMessageReceiveEvent;
import com.discordsrv.bukkit.BukkitDiscordSRV; import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.bukkit.component.PaperComponentHandle; import com.discordsrv.bukkit.component.PaperComponentHandle;
import com.discordsrv.common.abstraction.player.IPlayer; import com.discordsrv.common.abstraction.player.IPlayer;
import com.discordsrv.common.core.logging.NamedLogger;
import com.discordsrv.common.feature.channel.global.GlobalChannel; import com.discordsrv.common.feature.channel.global.GlobalChannel;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Event; import org.bukkit.event.Event;
@ -33,8 +35,8 @@ public class BukkitChatForwarder implements IBukkitChatForwarder {
public static Listener get(BukkitDiscordSRV discordSRV) { public static Listener get(BukkitDiscordSRV discordSRV) {
// TODO: config option // TODO: config option
//noinspection ConstantConditions,PointlessBooleanExpression //noinspection ConstantConditions,PointlessBooleanExpression
if (1 == 2 && PaperComponentHandle.IS_PAPER_ADVENTURE) { if (1 == 1 && PaperComponentHandle.IS_PAPER_ADVENTURE) {
return new PaperChatListener(new BukkitChatForwarder(discordSRV)); return new PaperChatListener(new BukkitChatForwarder(discordSRV), new NamedLogger(discordSRV, "CHAT_LISTENER"));
} }
return new BukkitChatListener(new BukkitChatForwarder(discordSRV)); return new BukkitChatListener(new BukkitChatForwarder(discordSRV));
@ -47,7 +49,21 @@ public class BukkitChatForwarder implements IBukkitChatForwarder {
} }
@Override @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); IPlayer srvPlayer = discordSRV.playerProvider().player(player);
discordSRV.scheduler().run(() -> discordSRV.eventBus().publish( discordSRV.scheduler().run(() -> discordSRV.eventBus().publish(
new GameChatMessageReceiveEvent( new GameChatMessageReceiveEvent(

View File

@ -24,6 +24,7 @@ import net.kyori.adventure.platform.bukkit.BukkitComponentSerializer;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
public class BukkitChatListener implements Listener { public class BukkitChatListener implements Listener {
@ -33,11 +34,22 @@ public class BukkitChatListener implements Listener {
this.forwarder = forwarder; this.forwarder = forwarder;
} }
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onAsyncPlayerChat(org.bukkit.event.player.AsyncPlayerChatEvent event) { public void onAsyncPlayerChatAnnotate(AsyncPlayerChatEvent event) {
MinecraftComponent component = ComponentUtil.toAPI( MinecraftComponent component = ComponentUtil.toAPI(
BukkitComponentSerializer.legacy().deserialize(event.getMessage())); 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.LinkingModule;
import com.discordsrv.common.feature.linking.impl.MinecraftAuthenticationLinker; import com.discordsrv.common.feature.linking.impl.MinecraftAuthenticationLinker;
import com.discordsrv.common.feature.linking.impl.StorageLinker; 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.DiscordChatMessageModule;
import com.discordsrv.common.feature.messageforwarding.discord.DiscordMessageMirroringModule; import com.discordsrv.common.feature.messageforwarding.discord.DiscordMessageMirroringModule;
import com.discordsrv.common.feature.messageforwarding.game.JoinMessageModule; import com.discordsrv.common.feature.messageforwarding.game.JoinMessageModule;
import com.discordsrv.common.feature.messageforwarding.game.LeaveMessageModule; import com.discordsrv.common.feature.messageforwarding.game.LeaveMessageModule;
import com.discordsrv.common.feature.messageforwarding.game.StartMessageModule; import com.discordsrv.common.feature.messageforwarding.game.StartMessageModule;
import com.discordsrv.common.feature.messageforwarding.game.StopMessageModule; 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.profile.ProfileManager;
import com.discordsrv.common.feature.update.UpdateChecker; import com.discordsrv.common.feature.update.UpdateChecker;
import com.discordsrv.common.helper.ChannelConfigHelper; import com.discordsrv.common.helper.ChannelConfigHelper;
@ -593,6 +594,7 @@ public abstract class AbstractDiscordSRV<
registerModule(MentionCachingModule::new); registerModule(MentionCachingModule::new);
registerModule(LinkingModule::new); registerModule(LinkingModule::new);
registerModule(PresenceUpdaterModule::new); registerModule(PresenceUpdaterModule::new);
registerModule(MentionGameRenderingModule::new);
// Integrations // Integrations
registerIntegration("com.discordsrv.common.integration.LuckPermsIntegration"); registerIntegration("com.discordsrv.common.integration.LuckPermsIntegration");

View File

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

View File

@ -113,7 +113,7 @@ public abstract class BroadcastCommand implements GameCommandExecutor, GameComma
} }
} catch (IllegalArgumentException ignored) { } catch (IllegalArgumentException ignored) {
BaseChannelConfig channelConfig = discordSRV.channelConfig().resolve(null, channel); 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) { if (config != null) {
future = discordSRV.destinations().lookupDestination(config.destination(), true, false); future = discordSRV.destinations().lookupDestination(config.destination(), true, false);
@ -203,7 +203,7 @@ public abstract class BroadcastCommand implements GameCommandExecutor, GameComma
.applyPlaceholderService() .applyPlaceholderService()
.build(); .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 @Override
public String getContent(String content) { public String getContent(String content) {
Component component = GsonComponentSerializer.gson().deserialize(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.annotation.Untranslated;
import com.discordsrv.common.config.configurate.manager.abstraction.ConfigurateConfigManager; import com.discordsrv.common.config.configurate.manager.abstraction.ConfigurateConfigManager;
import com.discordsrv.common.config.main.generic.DiscordIgnoresConfig; 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.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.objectmapping.meta.Comment;
@ -64,9 +63,6 @@ public class DiscordToMinecraftChatConfig {
@Comment("Users, bots, roles and webhooks to ignore") @Comment("Users, bots, roles and webhooks to ignore")
public DiscordIgnoresConfig ignores = new DiscordIgnoresConfig(); 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" @Comment("How should unicode emoji be shown in-game:\n"
+ "- hide: hides emojis 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" + "- 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 @ConfigSerializable
public static class Mentions { 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" @Comment("If role mentions should be rendered on Discord\n\n"
+ "The player needs one of the below permission to trigger notifications:\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" + "- 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") + "The player needs the discordsrv.mention.everyone permission to render the mention and trigger a notification")
public boolean everyone = false; 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.configurate.annotation.Order;
import com.discordsrv.common.config.main.channels.*; 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.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.objectmapping.meta.Comment;
@ -32,6 +33,9 @@ public class BaseChannelConfig {
@Order(0) @Order(0)
public DiscordToMinecraftChatConfig discordToMinecraft = new DiscordToMinecraftChatConfig(); public DiscordToMinecraftChatConfig discordToMinecraft = new DiscordToMinecraftChatConfig();
@Comment("The representations of Discord mentions in-game")
public MentionsConfig mentions = new MentionsConfig();
public JoinMessageConfig joinMessages() { public JoinMessageConfig joinMessages() {
return new JoinMessageConfig(); 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%", "[hover:show_text:Click to go to channel][click:open_url:%channel_jump_url%][color:#5865F2]#%channel_name%",
"[color:#5865F2]#Unknown" "[color:#5865F2]#Unknown"
); );
public Format user = new Format( public FormatUser user = new FormatUser(
"[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%", "[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]'%]"
"[color:#5865F2]@Unknown user" + "[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% > ..."; 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; 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.MinecraftComponent;
import com.discordsrv.api.component.MinecraftComponentAdapter; import com.discordsrv.api.component.MinecraftComponentAdapter;
import com.discordsrv.api.component.MinecraftComponentFactory; 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.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.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.renderer.DiscordSRVMinecraftRenderer;
import com.discordsrv.common.core.component.translation.Translation; import com.discordsrv.common.core.component.translation.Translation;
import com.discordsrv.common.core.component.translation.TranslationRegistry; import com.discordsrv.common.core.component.translation.TranslationRegistry;
import com.discordsrv.common.core.logging.Logger; import com.discordsrv.common.core.logging.Logger;
import com.discordsrv.common.core.logging.NamedLogger; import com.discordsrv.common.core.logging.NamedLogger;
import com.discordsrv.common.util.ComponentUtil;
import dev.vankka.enhancedlegacytext.EnhancedLegacyText; import dev.vankka.enhancedlegacytext.EnhancedLegacyText;
import dev.vankka.mcdiscordreserializer.discord.DiscordSerializer; import dev.vankka.mcdiscordreserializer.discord.DiscordSerializer;
import dev.vankka.mcdiscordreserializer.discord.DiscordSerializerOptions; import dev.vankka.mcdiscordreserializer.discord.DiscordSerializerOptions;
import dev.vankka.mcdiscordreserializer.minecraft.MinecraftSerializer; import dev.vankka.mcdiscordreserializer.minecraft.MinecraftSerializer;
import dev.vankka.mcdiscordreserializer.minecraft.MinecraftSerializerOptions; 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.Component;
import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.flattener.ComponentFlattener; import net.kyori.adventure.text.flattener.ComponentFlattener;
@ -155,10 +164,79 @@ public class ComponentFactory implements MinecraftComponentFactory {
return EnhancedLegacyText.get().parse(textInput); 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 DiscordContentComponent.of("<#" + Long.toUnsignedString(id) + ">", 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 DiscordContentComponent.of("<@" + Long.toUnsignedString(id) + ">", 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 DiscordContentComponent.of("<@&" + Long.toUnsignedString(id) + ">", ComponentUtil.fromAPI(
discordSRV.componentFactory()
.textBuilder(role != null ? format.format : format.unknownFormat)
.addContext(role)
.applyPlaceholderService()
.build()
));
}
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 DiscordContentComponent.of(emoji.asJDA().getAsMention(), ComponentUtil.fromAPI(event.getRenderedEmojiFromProcessing()));
}
switch (behaviour) {
case NAME:
return DiscordContentComponent.of(emoji.asJDA().getAsMention(), 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)); return DiscordSRVMinecraftRenderer.getWithContext(guild, config, () -> minecraftSerializer().serialize(discordMessage));
} }
public String discordSerialize(Component component) {
Component mapped = DiscordContentComponent.remapToDiscord(component);
return discordSerializer().serialize(mapped);
}
public MinecraftSerializer minecraftSerializer() { public MinecraftSerializer minecraftSerializer() {
return minecraftSerializer; return minecraftSerializer;
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 <a href="https://github.com/KyoriPowered/adventure/pull/842">adventure #842</a>
*
* @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<Component> 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<Component> newChildren = new ArrayList<>();
for (Component child : children) {
newChildren.add(remapToDiscord(child));
}
return component.children(newChildren);
}
}

View File

@ -18,14 +18,9 @@
package com.discordsrv.common.core.component.renderer; 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.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.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.config.main.generic.MentionsConfig;
import com.discordsrv.common.util.ComponentUtil; import com.discordsrv.common.util.ComponentUtil;
import dev.vankka.mcdiscordreserializer.renderer.implementation.DefaultMinecraftRenderer; 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.Component;
import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.ClickEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -53,7 +47,7 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer {
public static <T> T getWithContext( public static <T> T getWithContext(
DiscordGuild guild, DiscordGuild guild,
DiscordToMinecraftChatConfig config, BaseChannelConfig config,
Supplier<T> supplier Supplier<T> supplier
) { ) {
Context oldValue = CONTEXT.get(); Context oldValue = CONTEXT.get();
@ -116,57 +110,20 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer {
return component.append(Component.text("<#" + id + ">")); return component.append(Component.text("<#" + id + ">"));
} }
Component mention = makeChannelMention(MiscUtil.parseLong(id), format); return component.append(discordSRV.componentFactory().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()
);
} }
@Override @Override
public @NotNull Component appendUserMention(@NotNull Component component, @NotNull String id) { public @NotNull Component appendUserMention(@NotNull Component component, @NotNull String id) {
Context context = CONTEXT.get(); 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; DiscordGuild guild = context != null ? context.guild : null;
if (format == null || guild == null) { if (format == null || guild == null) {
return component.append(Component.text("<@" + id + ">")); return component.append(Component.text("<@" + id + ">"));
} }
long userId = MiscUtil.parseLong(id); long userId = MiscUtil.parseLong(id);
return component.append(makeUserMention(userId, format, guild)); return component.append(discordSRV.componentFactory().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()
);
} }
@Override @Override
@ -178,19 +135,7 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer {
} }
long roleId = MiscUtil.parseLong(id); long roleId = MiscUtil.parseLong(id);
return component.append(makeRoleMention(roleId, format)); return component.append(discordSRV.componentFactory().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()
);
} }
@Override @Override
@ -206,7 +151,7 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer {
} }
long emojiId = MiscUtil.parseLong(id); long emojiId = MiscUtil.parseLong(id);
Component emoteMention = makeEmoteMention(emojiId, behaviour); Component emoteMention = discordSRV.componentFactory().makeEmoteMention(emojiId, behaviour);
if (emoteMention == null) { if (emoteMention == null) {
return component; return component;
} }
@ -214,34 +159,12 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer {
return component.append(emoteMention); 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 static class Context {
private final DiscordGuild guild; 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.guild = guild;
this.config = config; this.config = config;
} }

View File

@ -49,7 +49,7 @@ public class ComponentResultStringifier implements PlaceholderResultMapper {
case PLAIN: case PLAIN:
return discordSRV.componentFactory().plainSerializer().serialize(component); return discordSRV.componentFactory().plainSerializer().serialize(component);
case DISCORD: case DISCORD:
return new FormattedText(discordSRV.componentFactory().discordSerializer().serialize(component)); return new FormattedText(discordSRV.componentFactory().discordSerialize(component));
case ANSI: case ANSI:
return discordSRV.componentFactory().ansiSerializer().serialize(component); return discordSRV.componentFactory().ansiSerializer().serialize(component);
case LEGACY: case LEGACY:

View File

@ -282,7 +282,7 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
return null; return null;
} }
Component component = discordSRV.componentFactory().minecraftSerialize(getGuild(), config.discordToMinecraft, content); Component component = discordSRV.componentFactory().minecraftSerialize(getGuild(), config, content);
String replyFormat = config.discordToMinecraft.replyFormat; String replyFormat = config.discordToMinecraft.replyFormat;
return ComponentUtil.fromAPI( return ComponentUtil.fromAPI(

View File

@ -71,7 +71,7 @@ public class ConsoleMessage {
public String asMarkdown() { public String asMarkdown() {
Component component = builder.build(); Component component = builder.build();
return discordSRV.componentFactory().discordSerializer().serialize(component); return discordSRV.componentFactory().discordSerialize(component);
} }
public String asAnsi() { 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/>. * 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.discord.connection.details.DiscordGatewayIntent;
import com.discordsrv.api.eventbus.Subscribe; import com.discordsrv.api.eventbus.Subscribe;
import com.discordsrv.common.DiscordSRV; 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.MinecraftToDiscordChatConfig;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.core.module.type.AbstractModule; 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 com.github.benmanes.caffeine.cache.Cache;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member; 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.RoleCreateEvent;
import net.dv8tion.jda.api.events.role.RoleDeleteEvent; import net.dv8tion.jda.api.events.role.RoleDeleteEvent;
import net.dv8tion.jda.api.events.role.update.RoleUpdateNameEvent; import net.dv8tion.jda.api.events.role.update.RoleUpdateNameEvent;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class MentionCachingModule extends AbstractModule<DiscordSRV> { 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, 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>> roleMentions = new ConcurrentHashMap<>();
private final Map<Long, Map<Long, CachedMention>> channelMentions = new ConcurrentHashMap<>(); private final Map<Long, Map<Long, CachedMention>> channelMentions = new ConcurrentHashMap<>();
@ -91,8 +99,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
continue; continue;
} }
MinecraftToDiscordChatConfig.Mentions mentions = config.mentions; if (config.mentions.anyCaching()) {
if (mentions.roles || mentions.users || mentions.channels) {
return true; return true;
} }
} }
@ -106,10 +113,58 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
channelMentions.clear(); 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 @Subscribe
public void onGuildDelete(GuildLeaveEvent event) { public void onGuildDelete(GuildLeaveEvent event) {
long guildId = event.getGuild().getIdLong(); long guildId = event.getGuild().getIdLong();
memberMentions.remove(guildId); memberMentions.remove(guildId);
memberMentionsCache.remove(guildId);
roleMentions.remove(guildId); roleMentions.remove(guildId);
channelMentions.remove(guildId); channelMentions.remove(guildId);
} }
@ -118,27 +173,31 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
// Member // 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<>(); CompletableFuture<List<Member>> memberFuture = new CompletableFuture<>();
guild.retrieveMembersByPrefix(mention.substring(1), 100) guild.retrieveMembersByPrefix(mention.substring(1), 100)
.onSuccess(memberFuture::complete).onError(memberFuture::completeExceptionally); .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 -> { return memberFuture.thenApply(members -> {
List<CachedMention> cachedMentions = new ArrayList<>(); List<CachedMention> cachedMentions = new ArrayList<>();
for (Member member : members) { 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); cachedMentions.add(cachedMention);
} }
return cachedMentions; return cachedMentions;
}); });
} }
public Map<Long, CachedMention> getMemberMentions(Guild guild) { private Map<Long, CachedMention> getMemberMentions(Guild guild) {
return memberMentions.computeIfAbsent(guild.getIdLong(), key -> { return memberMentions.computeIfAbsent(guild.getIdLong(), key -> {
Map<Long, CachedMention> mentions = new LinkedHashMap<>(); Map<Long, CachedMention> mentions = new LinkedHashMap<>();
for (Member member : guild.getMembers()) { for (Member member : guild.getMembers()) {
@ -152,6 +211,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
return new CachedMention( return new CachedMention(
"@" + member.getUser().getName(), "@" + member.getUser().getName(),
member.getAsMention(), member.getAsMention(),
CachedMention.Type.USER,
member.getIdLong() member.getIdLong()
); );
} }
@ -187,7 +247,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
// Role // Role
// //
public Map<Long, CachedMention> getRoleMentions(Guild guild) { private Map<Long, CachedMention> getRoleMentions(Guild guild) {
return roleMentions.computeIfAbsent(guild.getIdLong(), key -> { return roleMentions.computeIfAbsent(guild.getIdLong(), key -> {
Map<Long, CachedMention> mentions = new LinkedHashMap<>(); Map<Long, CachedMention> mentions = new LinkedHashMap<>();
for (Role role : guild.getRoles()) { for (Role role : guild.getRoles()) {
@ -201,6 +261,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
return new CachedMention( return new CachedMention(
"@" + role.getName(), "@" + role.getName(),
role.getAsMention(), role.getAsMention(),
CachedMention.Type.ROLE,
role.getIdLong() role.getIdLong()
); );
} }
@ -227,7 +288,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
// Channel // Channel
// //
public Map<Long, CachedMention> getChannelMentions(Guild guild) { private Map<Long, CachedMention> getChannelMentions(Guild guild) {
return channelMentions.computeIfAbsent(guild.getIdLong(), key -> { return channelMentions.computeIfAbsent(guild.getIdLong(), key -> {
Map<Long, CachedMention> mentions = new LinkedHashMap<>(); Map<Long, CachedMention> mentions = new LinkedHashMap<>();
for (GuildChannel channel : guild.getChannels()) { for (GuildChannel channel : guild.getChannels()) {
@ -246,6 +307,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
return new CachedMention( return new CachedMention(
"#" + channel.getName(), "#" + channel.getName(),
channel.getAsMention(), channel.getAsMention(),
CachedMention.Type.CHANNEL,
channel.getIdLong() channel.getIdLong()
); );
} }
@ -279,53 +341,4 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
GuildChannel channel = (GuildChannel) event.getChannel(); GuildChannel channel = (GuildChannel) event.getChannel();
getChannelMentions(event.getGuild()).remove(channel.getIdLong()); 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; return;
} }
Component messageComponent = discordSRV.componentFactory().minecraftSerialize(guild, chatConfig, finalMessage); Component messageComponent = discordSRV.componentFactory().minecraftSerialize(guild, channelConfig, finalMessage);
if (ComponentUtil.isEmpty(messageComponent) && !attachments) { if (ComponentUtil.isEmpty(messageComponent) && !attachments) {
// Check empty-ness again after rendering // Check empty-ness again after rendering
return; return;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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.channel.GameChannel;
import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel; import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel;
@ -25,20 +25,20 @@ import com.discordsrv.api.discord.entity.message.AllowedMention;
import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage;
import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessageCluster; import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessageCluster;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage; 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.EventPriority;
import com.discordsrv.api.eventbus.Subscribe; import com.discordsrv.api.eventbus.Subscribe;
import com.discordsrv.api.events.message.forward.game.GameChatMessageForwardedEvent; import com.discordsrv.api.events.message.forward.game.GameChatMessageForwardedEvent;
import com.discordsrv.api.events.message.receive.game.GameChatMessageReceiveEvent; import com.discordsrv.api.events.message.receive.game.GameChatMessageReceiveEvent;
import com.discordsrv.api.placeholder.format.FormattedText; import com.discordsrv.api.placeholder.format.FormattedText;
import com.discordsrv.api.placeholder.format.PlainPlaceholderFormat;
import com.discordsrv.api.placeholder.util.Placeholders; import com.discordsrv.api.placeholder.util.Placeholders;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.abstraction.player.IPlayer; import com.discordsrv.common.abstraction.player.IPlayer;
import com.discordsrv.common.config.main.channels.MinecraftToDiscordChatConfig; 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.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.permission.game.Permission;
import com.discordsrv.common.util.CompletableFutureUtil;
import com.discordsrv.common.util.ComponentUtil; import com.discordsrv.common.util.ComponentUtil;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.Role;
@ -47,9 +47,6 @@ import org.jetbrains.annotations.NotNull;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; 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> { public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<MinecraftToDiscordChatConfig, GameChatMessageReceiveEvent> {
@ -114,8 +111,6 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<Mine
@Override @Override
public void setPlaceholders(MinecraftToDiscordChatConfig config, GameChatMessageReceiveEvent event, SendableDiscordMessage.Formatter formatter) {} public void setPlaceholders(MinecraftToDiscordChatConfig config, GameChatMessageReceiveEvent event, SendableDiscordMessage.Formatter formatter) {}
private final Pattern MENTION_PATTERN = Pattern.compile("@\\S+");
private CompletableFuture<SendableDiscordMessage> getMessageForGuild( private CompletableFuture<SendableDiscordMessage> getMessageForGuild(
MinecraftToDiscordChatConfig config, MinecraftToDiscordChatConfig config,
SendableDiscordMessage.Builder format, SendableDiscordMessage.Builder format,
@ -124,29 +119,10 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<Mine
IPlayer player, IPlayer player,
Object[] context Object[] context
) { ) {
MinecraftToDiscordChatConfig.Mentions mentionConfig = config.mentions;
MentionCachingModule mentionCaching = discordSRV.getModule(MentionCachingModule.class); MentionCachingModule mentionCaching = discordSRV.getModule(MentionCachingModule.class);
if (mentionCaching != null) {
if (mentionCaching != null && mentionConfig.users && mentionConfig.uncachedUsers return mentionCaching.lookup(config.mentions, guild, player, message)
&& player.hasPermission(Permission.MENTION_USER_LOOKUP)) { .thenApply(mentions -> getMessageForGuildWithMentions(config, format, guild, message, player, context, mentions));
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);
});
}
} }
return CompletableFuture.completedFuture(getMessageForGuildWithMentions(config, format, guild, message, player, context, null)); return CompletableFuture.completedFuture(getMessageForGuildWithMentions(config, format, guild, message, player, context, null));
@ -159,31 +135,9 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<Mine
Component message, Component message,
IPlayer player, IPlayer player,
Object[] context, Object[] context,
Set<MentionCachingModule.CachedMention> memberMentions List<CachedMention> mentions
) { ) {
MinecraftToDiscordChatConfig.Mentions mentionConfig = config.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<>(); List<AllowedMention> allowedMentions = new ArrayList<>();
if (mentionConfig.users && player.hasPermission(Permission.MENTION_USER)) { if (mentionConfig.users && player.hasPermission(Permission.MENTION_USER)) {
@ -211,29 +165,24 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<Mine
.toFormatter() .toFormatter()
.addContext(context) .addContext(context)
.addPlaceholder("message", () -> { .addPlaceholder("message", () -> {
String convertedComponent = convertComponent(config, message); String content = PlainPlaceholderFormat.supplyWith(
Placeholders channelMessagePlaceholders = new Placeholders( PlainPlaceholderFormat.Formatting.DISCORD,
DiscordFormattingUtil.escapeMentions(convertedComponent)); () -> discordSRV.placeholderService().getResultAsCharSequence(message).toString()
);
Placeholders messagePlaceholders = new Placeholders(content);
config.contentRegexFilters.forEach(messagePlaceholders::replaceAll);
// From longest to shortest if (mentions != null) {
orderedMentions.forEach(mention -> channelMessagePlaceholders.replaceAll(mention.search(), mention.mention())); mentions.forEach(mention -> messagePlaceholders.replaceAll(mention.search(), mention.mention()));
}
String finalMessage = channelMessagePlaceholders.toString(); String finalMessage = messagePlaceholders.toString();
return new FormattedText(preventEveryoneMentions(everyone, finalMessage)); return new FormattedText(preventEveryoneMentions(everyone, finalMessage));
}) })
.applyPlaceholderService() .applyPlaceholderService()
.build(); .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) { private String preventEveryoneMentions(boolean everyoneAllowed, String message) {
if (everyoneAllowed) { if (everyoneAllowed) {
// Nothing to do // 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.console.Console;
import com.discordsrv.common.feature.debug.data.OnlineMode; import com.discordsrv.common.feature.debug.data.OnlineMode;
import com.discordsrv.common.feature.debug.data.VersionInfo; 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 dev.vankka.dependencydownload.classpath.ClasspathAppender;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;

View File

@ -122,7 +122,7 @@ dependencyResolutionManagement {
library('adventure-serializer-ansi', 'net.kyori', 'adventure-text-serializer-ansi').versionRef('adventure') library('adventure-serializer-ansi', 'net.kyori', 'adventure-text-serializer-ansi').versionRef('adventure')
// Adventure Platform // 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-bukkit', 'net.kyori', 'adventure-platform-bukkit').versionRef('adventure-platform')
library('adventure-platform-bungee', 'net.kyori', 'adventure-platform-bungeecord').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') library('adventure-serializer-bungee', 'net.kyori', 'adventure-text-serializer-bungeecord').versionRef('adventure-platform')