diff --git a/api/src/main/java/com/discordsrv/api/component/EnhancedTextBuilder.java b/api/src/main/java/com/discordsrv/api/component/EnhancedTextBuilder.java index 8613c434..34c6fecf 100644 --- a/api/src/main/java/com/discordsrv/api/component/EnhancedTextBuilder.java +++ b/api/src/main/java/com/discordsrv/api/component/EnhancedTextBuilder.java @@ -28,6 +28,9 @@ import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * Minecraft equivalent for {@link com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage.Formatter}. + */ public interface EnhancedTextBuilder { EnhancedTextBuilder addContext(Object... context); diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/message/SendableDiscordMessage.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/message/SendableDiscordMessage.java index 679832a6..de7ab93e 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/message/SendableDiscordMessage.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/message/SendableDiscordMessage.java @@ -192,8 +192,17 @@ public interface SendableDiscordMessage { */ @NotNull SendableDiscordMessage build(); + + /** + * Creates a copy of this {@link Builder}. + * @return a copy of this builder + */ + Builder clone(); } + /** + * Discord equivalent for {@link com.discordsrv.api.component.EnhancedTextBuilder}. + */ interface Formatter { Formatter addContext(Object... context); diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/message/impl/SendableDiscordMessageImpl.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/message/impl/SendableDiscordMessageImpl.java index 815b8fef..2afd4b6a 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/message/impl/SendableDiscordMessageImpl.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/message/impl/SendableDiscordMessageImpl.java @@ -157,5 +157,17 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage { public @NotNull SendableDiscordMessage build() { return new SendableDiscordMessageImpl(content, embeds, allowedMentions, webhookUsername, webhookAvatarUrl); } + + @SuppressWarnings("MethodDoesntCallSuperMethod") + @Override + public Builder clone() { + BuilderImpl clone = new BuilderImpl(); + clone.setContent(content); + embeds.forEach(clone::addEmbed); + allowedMentions.forEach(clone::addAllowedMention); + clone.setWebhookUsername(webhookUsername); + clone.setWebhookAvatarUrl(webhookAvatarUrl); + return clone; + } } } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/util/DiscordFormattingUtil.java b/api/src/main/java/com/discordsrv/api/discord/api/util/DiscordFormattingUtil.java new file mode 100644 index 00000000..b4115674 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/api/util/DiscordFormattingUtil.java @@ -0,0 +1,43 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2021 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.discord.api.util; + +public final class DiscordFormattingUtil { + + private DiscordFormattingUtil() {} + + public static String escapeContent(String content) { + content = escapeChars(content, '*', '_', '|', '`', '~', '>'); + return content; + } + + private static String escapeChars(String input, char... characters) { + for (char character : characters) { + input = input.replace( + String.valueOf(character), + "\\" + character); + } + return input; + } +} diff --git a/api/src/main/java/com/discordsrv/api/placeholder/FormattedText.java b/api/src/main/java/com/discordsrv/api/placeholder/FormattedText.java new file mode 100644 index 00000000..85aa3c80 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/placeholder/FormattedText.java @@ -0,0 +1,56 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2021 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.placeholder; + +/** + * Represents content that doesn't need to be processed for the purposes of DiscordSRV's processing. + */ +public class FormattedText implements CharSequence { + + private final CharSequence text; + + public FormattedText(CharSequence text) { + this.text = text; + } + + @Override + public int length() { + return text.length(); + } + + @Override + public char charAt(int index) { + return text.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return text.subSequence(start, end); + } + + @Override + public String toString() { + return text.toString(); + } +} diff --git a/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java b/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java index ae5e7f19..41128c9e 100644 --- a/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java +++ b/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java @@ -49,4 +49,9 @@ public interface DiscordSRVPlayer { @NotNull UUID uuid(); + + @Placeholder("totally_my_username") // TODO: remove + default String totallyMyUsername() { + return "*hi"; + } } diff --git a/build.gradle b/build.gradle index b81823a6..92f48753 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ ext { // Configurate configurateVersion = '4.1.1' // Adventure & Adventure Platform - adventureVersion = '4.8.1' + adventureVersion = '4.9.1' adventurePlatformVersion = '4.0.0-SNAPSHOT' } diff --git a/bukkit/build.gradle b/bukkit/build.gradle index 3acf47e9..cc1c380d 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -23,7 +23,6 @@ dependencies { // Adventure runtimeDownloadApi 'net.kyori:adventure-platform-bukkit:' + rootProject.adventurePlatformVersion - runtimeDownloadApi 'net.kyori:adventure-text-serializer-craftbukkit:' + rootProject.adventurePlatformVersion } shadowJar { diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/listener/BukkitChatListener.java b/bukkit/src/main/java/com/discordsrv/bukkit/listener/BukkitChatListener.java index afee0118..8f2397b4 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/listener/BukkitChatListener.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/listener/BukkitChatListener.java @@ -24,7 +24,7 @@ import com.discordsrv.bukkit.BukkitDiscordSRV; import com.discordsrv.common.channel.DefaultGlobalChannel; import com.discordsrv.common.component.util.ComponentUtil; import io.papermc.paper.event.player.AsyncChatEvent; -import net.kyori.adventure.text.serializer.craftbukkit.BukkitComponentSerializer; +import net.kyori.adventure.platform.bukkit.BukkitComponentSerializer; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java index de1b641b..1956245e 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java @@ -23,8 +23,8 @@ import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.player.IPlayer; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.platform.bukkit.BukkitComponentSerializer; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.craftbukkit.BukkitComponentSerializer; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; diff --git a/common/src/main/java/com/discordsrv/common/discord/api/message/SendableDiscordMessageFormatterImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/message/SendableDiscordMessageFormatterImpl.java index 2110e603..65253f65 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/message/SendableDiscordMessageFormatterImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/message/SendableDiscordMessageFormatterImpl.java @@ -18,7 +18,10 @@ package com.discordsrv.common.discord.api.message; +import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed; import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; +import com.discordsrv.api.discord.api.util.DiscordFormattingUtil; +import com.discordsrv.api.placeholder.FormattedText; import com.discordsrv.api.placeholder.PlaceholderService; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.placeholder.converter.ComponentResultConverter; @@ -39,7 +42,7 @@ public class SendableDiscordMessageFormatterImpl implements SendableDiscordMessa public SendableDiscordMessageFormatterImpl(DiscordSRV discordSRV, SendableDiscordMessage.Builder builder) { this.discordSRV = discordSRV; - this.builder = builder; + this.builder = builder.clone(); } @Override @@ -50,7 +53,7 @@ public class SendableDiscordMessageFormatterImpl implements SendableDiscordMessa @Override public SendableDiscordMessage.Formatter addReplacement(Pattern target, Function replacement) { - this.replacements.put(target, replacement); + this.replacements.put(target, wrapFunction(replacement)); return this; } @@ -64,16 +67,90 @@ public class SendableDiscordMessageFormatterImpl implements SendableDiscordMessa return new Placeholders(input) .addAll(replacements) .replaceAll(PlaceholderService.PATTERN, - matcher -> discordSRV.placeholderService().getResultAsString(matcher, context)) + wrapFunction( + matcher -> discordSRV.placeholderService().getResultAsString(matcher, context))) .get(); }; - - ComponentResultConverter.plain(() -> - builder.setWebhookUsername(placeholders.apply(builder.getWebhookUsername()))); builder.setContent(placeholders.apply(builder.getContent())); - // TODO: rest of the content, escaping unwanted characters + List embeds = new ArrayList<>(builder.getEmbeds()); + builder.getEmbeds().clear(); + + for (DiscordMessageEmbed embed : embeds) { + DiscordMessageEmbed.Builder embedBuilder = embed.toBuilder(); + + // TODO: check which parts allow formatting more thoroughly + ComponentResultConverter.plainComponents(() -> { + embedBuilder.setAuthor( + placeholders.apply( + embedBuilder.getAuthorName()), + placeholders.apply( + embedBuilder.getAuthorUrl()), + placeholders.apply( + embedBuilder.getAuthorImageUrl())); + + embedBuilder.setTitle( + placeholders.apply( + embedBuilder.getTitle()), + placeholders.apply( + embedBuilder.getTitleUrl())); + + embedBuilder.setThumbnailUrl( + placeholders.apply( + embedBuilder.getThumbnailUrl())); + + embedBuilder.setImageUrl( + placeholders.apply( + embedBuilder.getImageUrl())); + + embedBuilder.setFooter( + placeholders.apply( + embedBuilder.getFooter()), + placeholders.apply( + embedBuilder.getFooterImageUrl())); + }); + + embedBuilder.setDescription( + placeholders.apply( + embedBuilder.getDescription()) + ); + + List fields = new ArrayList<>(embedBuilder.getFields()); + embedBuilder.getFields().clear(); + + fields.forEach(field -> embedBuilder.addField( + placeholders.apply( + field.getTitle()), + placeholders.apply( + field.getValue()), + field.isInline() + )); + + builder.addEmbed(embedBuilder.build()); + } + + ComponentResultConverter.plainComponents(() -> { + builder.setWebhookUsername(placeholders.apply(builder.getWebhookUsername())); + builder.setWebhookAvatarUrl(placeholders.apply(builder.getWebhookAvatarUrl())); + }); return builder.build(); } + + private Function wrapFunction(Function function) { + return matcher -> { + Object result = function.apply(matcher); + if (result instanceof FormattedText) { + // Process as regular text + return result.toString(); + } else if (result instanceof CharSequence) { + // Escape content + return DiscordFormattingUtil.escapeContent( + result.toString()); + } + + // Use default behaviour for everything else + return result; + }; + } } diff --git a/common/src/main/java/com/discordsrv/common/listener/DiscordChatListener.java b/common/src/main/java/com/discordsrv/common/listener/DiscordChatListener.java index 130d0cfd..19d31064 100644 --- a/common/src/main/java/com/discordsrv/common/listener/DiscordChatListener.java +++ b/common/src/main/java/com/discordsrv/common/listener/DiscordChatListener.java @@ -56,7 +56,6 @@ public class DiscordChatListener extends AbstractListener { } DiscordTextChannel channel = event.getChannel(); - Component message = MinecraftSerializer.INSTANCE.serialize(event.getMessageContent()); OrDefault> channelPair = discordSRV.channelConfig().orDefault(channel); GameChannel gameChannel = channelPair.get(Pair::getKey); @@ -73,6 +72,8 @@ public class DiscordChatListener extends AbstractListener { } DiscordUser user = event.getDiscordMessage().getAuthor(); + Component message = MinecraftSerializer.INSTANCE.serialize(event.getMessageContent()); + MinecraftComponent component = discordSRV.componentFactory() .enhancedBuilder(format) .addContext(event.getDiscordMessage(), user) diff --git a/common/src/main/java/com/discordsrv/common/listener/GameChatListener.java b/common/src/main/java/com/discordsrv/common/listener/GameChatListener.java index 2da7f02c..572fb83a 100644 --- a/common/src/main/java/com/discordsrv/common/listener/GameChatListener.java +++ b/common/src/main/java/com/discordsrv/common/listener/GameChatListener.java @@ -52,7 +52,6 @@ public class GameChatListener extends AbstractListener { } GameChannel gameChannel = event.getGameChannel(); - Component message = ComponentUtil.fromAPI(event.message()); OrDefault channelConfig = discordSRV.channelConfig().orDefault(gameChannel); OrDefault chatConfig = channelConfig.map(cfg -> cfg.minecraftToDiscord); @@ -62,9 +61,12 @@ public class GameChatListener extends AbstractListener { return; } + Component message = ComponentUtil.fromAPI(event.message()); + String serializedMessage = DiscordSerializer.INSTANCE.serialize(message); + SendableDiscordMessage discordMessage = discordSRV.discordAPI().format(builder) .addContext(event.getPlayer()) - .addReplacement("%message%", DiscordSerializer.INSTANCE.serialize(message)) + .addReplacement("%message%", serializedMessage) .build(); List channelIds = channelConfig.get(cfg -> cfg instanceof ChannelConfig ? ((ChannelConfig) cfg).channelIds : null); diff --git a/common/src/main/java/com/discordsrv/common/placeholder/converter/ComponentResultConverter.java b/common/src/main/java/com/discordsrv/common/placeholder/converter/ComponentResultConverter.java index bab50962..5bfc46dd 100644 --- a/common/src/main/java/com/discordsrv/common/placeholder/converter/ComponentResultConverter.java +++ b/common/src/main/java/com/discordsrv/common/placeholder/converter/ComponentResultConverter.java @@ -30,7 +30,7 @@ public class ComponentResultConverter implements PlaceholderResultConverter { private static final ThreadLocal PLAIN_CONTEXT = new ThreadLocal<>(); - public static void plain(Runnable runnable) { + public static void plainComponents(Runnable runnable) { PLAIN_CONTEXT.set(true); runnable.run(); PLAIN_CONTEXT.set(false); diff --git a/common/src/main/java/com/discordsrv/common/string/util/Placeholders.java b/common/src/main/java/com/discordsrv/common/string/util/Placeholders.java index 52dd2872..d45aa10b 100644 --- a/common/src/main/java/com/discordsrv/common/string/util/Placeholders.java +++ b/common/src/main/java/com/discordsrv/common/string/util/Placeholders.java @@ -76,7 +76,9 @@ public class Placeholders { Function replacement = entry.getValue(); Object value = replacement.apply(matcher); - input = matcher.replaceAll(String.valueOf(value)); + input = matcher.replaceAll( + Matcher.quoteReplacement( + String.valueOf(value))); } return input; }