diff --git a/api/src/main/java/com/discordsrv/api/discord/DiscordAPI.java b/api/src/main/java/com/discordsrv/api/discord/DiscordAPI.java index 3d33193a..f5a652d3 100644 --- a/api/src/main/java/com/discordsrv/api/discord/DiscordAPI.java +++ b/api/src/main/java/com/discordsrv/api/discord/DiscordAPI.java @@ -25,6 +25,7 @@ package com.discordsrv.api.discord; import com.discordsrv.api.discord.entity.DiscordUser; import com.discordsrv.api.discord.entity.channel.*; +import com.discordsrv.api.discord.entity.guild.DiscordCustomEmoji; import com.discordsrv.api.discord.entity.guild.DiscordGuild; import com.discordsrv.api.discord.entity.guild.DiscordRole; import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand; @@ -143,6 +144,13 @@ public interface DiscordAPI { @Nullable DiscordRole getRoleById(long id); + /** + * Gets a custom emoji for a Discord server by id, the provided entity should not be stored for long periods of time. + * @param id the id for the custom emoji + * @return the Discord custom emoji + */ + DiscordCustomEmoji getEmojiById(long id); + /** * Registers a Discord command. * @param command the command to register diff --git a/api/src/main/java/com/discordsrv/api/event/events/message/process/discord/DiscordChatMessageCustomEmojiRenderEvent.java b/api/src/main/java/com/discordsrv/api/event/events/message/process/discord/DiscordChatMessageCustomEmojiRenderEvent.java new file mode 100644 index 00000000..a33a587a --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/event/events/message/process/discord/DiscordChatMessageCustomEmojiRenderEvent.java @@ -0,0 +1,54 @@ +package com.discordsrv.api.event.events.message.process.discord; + +import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.discord.entity.guild.DiscordCustomEmoji; +import com.discordsrv.api.event.events.Event; +import com.discordsrv.api.event.events.Processable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Renders a given {@link DiscordCustomEmoji} into a {@link MinecraftComponent} that can be displayed in Minecraft. + * @see #process(MinecraftComponent) + */ +public class DiscordChatMessageCustomEmojiRenderEvent implements Event, Processable.Argument { + + private final DiscordCustomEmoji emoji; + private MinecraftComponent rendered = null; + + public DiscordChatMessageCustomEmojiRenderEvent(@NotNull DiscordCustomEmoji emoji) { + this.emoji = emoji; + } + + @NotNull + public DiscordCustomEmoji getEmoji() { + return emoji; + } + + /** + * Gets the rendered representation of the emoji. + * @return the rendered representation of the emoji if this event has been processed otherwise {@code null} + */ + @Nullable + public MinecraftComponent getRenderedEmojiFromProcessing() { + return rendered; + } + + @Override + public boolean isProcessed() { + return rendered != null; + } + + /** + * Marks this event as processed, with the given {@link MinecraftComponent} being the representation of {@link DiscordCustomEmoji} in game. + * @param renderedEmote the rendered emote + */ + @Override + public void process(@NotNull MinecraftComponent renderedEmote) { + if (rendered != null) { + throw new IllegalStateException("Cannot process an already processed event"); + } + + rendered = renderedEmote; + } +} diff --git a/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordChatMessageProcessEvent.java b/api/src/main/java/com/discordsrv/api/event/events/message/process/discord/DiscordChatMessageProcessEvent.java similarity index 93% rename from api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordChatMessageProcessEvent.java rename to api/src/main/java/com/discordsrv/api/event/events/message/process/discord/DiscordChatMessageProcessEvent.java index c61b1482..ce746357 100644 --- a/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordChatMessageProcessEvent.java +++ b/api/src/main/java/com/discordsrv/api/event/events/message/process/discord/DiscordChatMessageProcessEvent.java @@ -1,10 +1,11 @@ -package com.discordsrv.api.event.events.message.receive.discord; +package com.discordsrv.api.event.events.message.process.discord; import com.discordsrv.api.channel.GameChannel; import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.event.events.Cancellable; import com.discordsrv.api.event.events.Processable; +import com.discordsrv.api.event.events.message.receive.discord.DiscordChatMessageReceiveEvent; /** * Indicates that a Discord message is about to be processed, this will run once per {@link GameChannel} destination, diff --git a/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordChatMessageReceiveEvent.java b/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordChatMessageReceiveEvent.java index 042176d1..b0ab565e 100644 --- a/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordChatMessageReceiveEvent.java +++ b/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordChatMessageReceiveEvent.java @@ -29,11 +29,12 @@ import com.discordsrv.api.discord.entity.channel.DiscordThreadChannel; import com.discordsrv.api.discord.entity.guild.DiscordGuild; import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.event.events.Cancellable; +import com.discordsrv.api.event.events.message.process.discord.DiscordChatMessageProcessEvent; import org.jetbrains.annotations.NotNull; /** * Indicates that a Discord message has been received and will be processed unless cancelled. - * This runs once per Discord message, before {@link DiscordChatMessageProcessEvent}. + * This runs once per Discord message, before {@link DiscordChatMessageProcessEvent}(s). */ public class DiscordChatMessageReceiveEvent implements Cancellable { diff --git a/api/src/main/java/com/discordsrv/api/event/events/message/receive/game/AbstractGameMessageReceiveEvent.java b/api/src/main/java/com/discordsrv/api/event/events/message/receive/game/AbstractGameMessageReceiveEvent.java index f1ce9328..565a3922 100644 --- a/api/src/main/java/com/discordsrv/api/event/events/message/receive/game/AbstractGameMessageReceiveEvent.java +++ b/api/src/main/java/com/discordsrv/api/event/events/message/receive/game/AbstractGameMessageReceiveEvent.java @@ -40,7 +40,7 @@ public abstract class AbstractGameMessageReceiveEvent implements Cancellable, Pr /** * Gets the event that triggered this event to occur. This varies depending on platform and different plugin integrations. - * @return an event object, that isn't guaranteed to be of the same type every time or {@code null} + * @return an event object, that isn't guaranteed to be of the same type every time, or {@code null} */ @Nullable public Object getTriggeringEvent() { diff --git a/common/src/main/java/com/discordsrv/common/component/renderer/DiscordSRVMinecraftRenderer.java b/common/src/main/java/com/discordsrv/common/component/renderer/DiscordSRVMinecraftRenderer.java index 785d9e2a..813c4047 100644 --- a/common/src/main/java/com/discordsrv/common/component/renderer/DiscordSRVMinecraftRenderer.java +++ b/common/src/main/java/com/discordsrv/common/component/renderer/DiscordSRVMinecraftRenderer.java @@ -20,9 +20,11 @@ package com.discordsrv.common.component.renderer; import com.discordsrv.api.component.GameTextBuilder; 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.event.events.message.process.discord.DiscordChatMessageCustomEmojiRenderEvent; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.config.main.channels.DiscordToMinecraftChatConfig; @@ -181,6 +183,42 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer { )); } + @Override + public @NotNull Component appendEmoteMention( + @NotNull Component component, + @NotNull String name, + @NotNull String id + ) { + Context context = CONTEXT.get(); + DiscordToMinecraftChatConfig.EmoteBehaviour behaviour = context != null ? context.config.customEmojiBehaviour : null; + if (behaviour == null || behaviour == DiscordToMinecraftChatConfig.EmoteBehaviour.HIDE) { + return component; + } + + System.out.println(name); + long emojiId = MiscUtil.parseLong(id); + DiscordCustomEmoji emoji = discordSRV.discordAPI().getEmojiById(emojiId); + if (emoji == null) { + return component; + } + + DiscordChatMessageCustomEmojiRenderEvent event = new DiscordChatMessageCustomEmojiRenderEvent(emoji); + discordSRV.eventBus().publish(event); + + if (event.isProcessed()) { + Component rendered = ComponentUtil.fromAPI(event.getRenderedEmojiFromProcessing()); + return component.append(rendered); + } + + switch (behaviour) { + case NAME: + return component.append(Component.text(":" + emoji.getName() + ":")); + case BLANK: + default: + return component; + } + } + private static class Context { private final DiscordGuild guild; diff --git a/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java b/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java index b9ff43bb..c8865041 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java @@ -18,7 +18,6 @@ package com.discordsrv.common.config.main.channels; -import com.discordsrv.common.config.configurate.annotation.Constants; import com.discordsrv.common.config.configurate.annotation.Untranslated; import com.discordsrv.common.config.main.generic.DiscordIgnoresConfig; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @@ -58,10 +57,6 @@ public class DiscordToMinecraftChatConfig { @Comment("The representations of Discord mentions in-game") public Mentions mentions = new Mentions(); - @Comment("The amount of milliseconds to delay processing Discord messages, if the message is deleted in that time it will not be processed.\n" - + "This can be used together with Discord moderation bots, to filter forwarded messages") - public long delayMillis = 0L; - @ConfigSerializable public static class Mentions { @@ -90,7 +85,35 @@ public class DiscordToMinecraftChatConfig { this.unknownFormat = unknownFormat; } } - } + @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" + //+ "- name: shows the name of the emoji in-game (for example :smiley:)" + ) + public EmojiBehaviour unicodeEmojiBehaviour = EmojiBehaviour.HIDE; + + public enum EmojiBehaviour { + HIDE, + SHOW + // TODO: add and implement name + } + + @Comment("How should custom emoji be shown in-game:\n" + + "- hide: custom emoji will not be shown in-game\n" + + "- blank: custom emoji will only be shown in-game if it is rendered by a 3rd party plugin\n" + + "- name: shows the name of the custom emoji in-game (for example :discordsrv:), unless rendered by a 3rd party plugin") + public EmoteBehaviour customEmojiBehaviour = EmoteBehaviour.BLANK; + + public enum EmoteBehaviour { + HIDE, + BLANK, + NAME + } + + @Comment("The amount of milliseconds to delay processing Discord messages, if the message is deleted in that time it will not be processed.\n" + + "This can be used together with Discord moderation bots, to filter forwarded messages") + public long delayMillis = 0L; + } diff --git a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java index ba459826..e7a645d8 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java @@ -23,6 +23,7 @@ import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; import com.discordsrv.api.discord.connection.jda.errorresponse.ErrorCallbackContext; import com.discordsrv.api.discord.entity.DiscordUser; import com.discordsrv.api.discord.entity.channel.*; +import com.discordsrv.api.discord.entity.guild.DiscordCustomEmoji; import com.discordsrv.api.discord.entity.guild.DiscordGuild; import com.discordsrv.api.discord.entity.guild.DiscordRole; import com.discordsrv.api.discord.entity.interaction.command.CommandType; @@ -36,6 +37,7 @@ import com.discordsrv.common.config.main.generic.ThreadConfig; import com.discordsrv.common.config.main.generic.DestinationConfig; import com.discordsrv.common.discord.api.entity.DiscordUserImpl; import com.discordsrv.common.discord.api.entity.channel.*; +import com.discordsrv.common.discord.api.entity.guild.DiscordCustomEmojiImpl; import com.discordsrv.common.discord.api.entity.guild.DiscordGuildImpl; import com.discordsrv.common.discord.api.entity.guild.DiscordGuildMemberImpl; import com.discordsrv.common.discord.api.entity.guild.DiscordRoleImpl; @@ -49,6 +51,7 @@ import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.concrete.*; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.emoji.CustomEmoji; import net.dv8tion.jda.api.exceptions.ErrorResponseException; import net.dv8tion.jda.api.requests.ErrorResponse; import org.checkerframework.checker.index.qual.NonNegative; @@ -498,6 +501,15 @@ public class DiscordAPIImpl implements DiscordAPI { return new DiscordRoleImpl(discordSRV, jda); } + @Override + public DiscordCustomEmoji getEmojiById(long id) { + return mapJDAEntity(jda -> jda.getEmojiById(id), this::getEmoji); + } + + public DiscordCustomEmoji getEmoji(CustomEmoji jda) { + return new DiscordCustomEmojiImpl(jda); + } + @Override public DiscordCommand.RegistrationResult registerCommand(DiscordCommand command) { return commandRegistry.register(command, false); diff --git a/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordCustomEmojiImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordCustomEmojiImpl.java new file mode 100644 index 00000000..c44c1d06 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/discord/api/entity/guild/DiscordCustomEmojiImpl.java @@ -0,0 +1,28 @@ +package com.discordsrv.common.discord.api.entity.guild; + +import com.discordsrv.api.discord.entity.guild.DiscordCustomEmoji; +import net.dv8tion.jda.api.entities.emoji.CustomEmoji; + +public class DiscordCustomEmojiImpl implements DiscordCustomEmoji { + + private final CustomEmoji jda; + + public DiscordCustomEmojiImpl(CustomEmoji jda) { + this.jda = jda; + } + + @Override + public CustomEmoji asJDA() { + return jda; + } + + @Override + public long getId() { + return jda.getIdLong(); + } + + @Override + public String getName() { + return jda.getName(); + } +} diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java index 237cf46a..46733571 100644 --- a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java @@ -32,7 +32,7 @@ import com.discordsrv.api.discord.events.message.DiscordMessageReceiveEvent; import com.discordsrv.api.discord.events.message.DiscordMessageUpdateEvent; import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.events.message.forward.discord.DiscordChatMessageForwardedEvent; -import com.discordsrv.api.event.events.message.receive.discord.DiscordChatMessageProcessEvent; +import com.discordsrv.api.event.events.message.process.discord.DiscordChatMessageProcessEvent; import com.discordsrv.api.event.events.message.receive.discord.DiscordChatMessageReceiveEvent; import com.discordsrv.api.placeholder.util.Placeholders; import com.discordsrv.common.DiscordSRV; @@ -60,6 +60,10 @@ public class DiscordChatMessageModule extends AbstractModule { // Notably this excludes, 0x09 HT (\t), 0x0A LF (\n), 0x0B VT (\v) and 0x0D CR (\r) (which may be used for text formatting) private static final Pattern ASCII_CONTROL_FILTER = Pattern.compile("[\\u0000-\\u0008\\u000C\\u000E-\\u001F\\u007F]"); + // A regex filter matching the unicode regular expression character category "Other Symbol" + // https://unicode.org/reports/tr18/#General_Category_Property + private static final Pattern EMOJI_FILTER = Pattern.compile("\\p{So}"); + private final Map sends = new ConcurrentHashMap<>(); public DiscordChatMessageModule(DiscordSRV discordSRV) { @@ -188,16 +192,25 @@ public class DiscordChatMessageModule extends AbstractModule { Placeholders message = new Placeholders(event.getContent()); message.replaceAll(ASCII_CONTROL_FILTER, ""); + if (chatConfig.unicodeEmojiBehaviour == DiscordToMinecraftChatConfig.EmojiBehaviour.HIDE) { + message.replaceAll(EMOJI_FILTER, ""); + } chatConfig.contentRegexFilters.forEach(message::replaceAll); String finalMessage = message.toString(); - if (StringUtils.isEmpty(finalMessage)) { + if (finalMessage.trim().isEmpty()) { + // No sending empty messages return; } Component messageComponent = DiscordSRVMinecraftRenderer.getWithContext(guild, chatConfig, () -> discordSRV.componentFactory().minecraftSerializer().serialize(finalMessage)); + if (discordSRV.componentFactory().plainSerializer().serialize(messageComponent).trim().isEmpty()) { + // Check empty-ness again after rendering + return; + } + GameTextBuilder componentBuilder = discordSRV.componentFactory() .textBuilder(format) .addContext(discordMessage, author, channel, channelConfig)