Automatically disable escaping markdown when inside a code string or block

This commit is contained in:
Vankka 2023-01-23 00:24:31 +02:00
parent 6fd03af3a8
commit 88a267bc0a
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
9 changed files with 235 additions and 92 deletions

View File

@ -27,6 +27,7 @@ import com.discordsrv.api.component.MinecraftComponentFactory;
import com.discordsrv.api.discord.DiscordAPI;
import com.discordsrv.api.discord.connection.details.DiscordConnectionDetails;
import com.discordsrv.api.event.bus.EventBus;
import com.discordsrv.api.placeholder.DiscordPlaceholders;
import com.discordsrv.api.placeholder.PlaceholderService;
import com.discordsrv.api.player.DiscordSRVPlayer;
import com.discordsrv.api.player.IPlayerProvider;
@ -104,6 +105,13 @@ public interface DiscordSRVApi {
@NotNull
PlaceholderService placeholderService();
/**
* Provides the {@link DiscordPlaceholders} instance.
* @return the {@link DiscordPlaceholders} instance
*/
@NotNull
DiscordPlaceholders discordPlaceholders();
/**
* A provider for {@link com.discordsrv.api.component.MinecraftComponent}s.
* @return the {@link com.discordsrv.api.component.MinecraftComponentFactory} instance.

View File

@ -29,9 +29,9 @@ import com.discordsrv.api.discord.entity.message.AllowedMention;
import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.api.discord.util.DiscordFormattingUtil;
import com.discordsrv.api.placeholder.DiscordPlaceholders;
import com.discordsrv.api.placeholder.FormattedText;
import com.discordsrv.api.placeholder.PlaceholderService;
import com.discordsrv.api.placeholder.mapper.ResultMappers;
import com.discordsrv.api.placeholder.util.Placeholders;
import net.dv8tion.jda.api.entities.MessageEmbed;
import org.jetbrains.annotations.NotNull;
@ -265,7 +265,7 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
private Function<Matcher, Object> wrapFunction(Function<Matcher, Object> function) {
return matcher -> {
Object result = function.apply(matcher);
if (result instanceof FormattedText || ResultMappers.isPlainContext()) {
if (result instanceof FormattedText || DiscordPlaceholders.MAPPING_STATE.get() != DiscordPlaceholders.MappingState.NORMAL) {
// Process as regular text
return result.toString();
} else if (result instanceof CharSequence) {
@ -281,6 +281,11 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
@Override
public @NotNull SendableDiscordMessage build() {
DiscordSRVApi api = DiscordSRVApi.get();
if (api == null) {
throw new IllegalStateException("DiscordSRVApi not available");
}
Function<String, String> placeholders = input -> {
if (input == null) {
return null;
@ -289,11 +294,25 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
Placeholders placeholderUtil = new Placeholders(input)
.addAll(replacements);
// Empty string -> null
// Empty string -> null (so we don't provide empty strings to random fields)
String output = placeholderUtil.toString();
return output.isEmpty() ? null : output;
};
builder.setContent(placeholders.apply(builder.getContent()));
Function<String, String> discordPlaceholders = input -> {
if (input == null) {
return null;
}
// Empty string -> null (so we don't provide empty strings to random fields)
String output = api.discordPlaceholders().map(input, in -> {
// Since this will be processed in parts, we don't want parts to return null, only the full output
String out = placeholders.apply(in);
return out == null ? "" : out;
});
return output.isEmpty() ? null : output;
};
builder.setContent(discordPlaceholders.apply(builder.getContent()));
List<DiscordMessageEmbed> embeds = new ArrayList<>(builder.getEmbeds());
embeds.forEach(builder::removeEmbed);
@ -302,7 +321,7 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
DiscordMessageEmbed.Builder embedBuilder = embed.toBuilder();
// TODO: check which parts allow formatting more thoroughly
ResultMappers.runInPlainContext(() -> {
DiscordPlaceholders.with(DiscordPlaceholders.MappingState.PLAIN, () -> {
embedBuilder.setAuthor(
cutToLength(
placeholders.apply(embedBuilder.getAuthorName()),
@ -338,7 +357,7 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
embedBuilder.setDescription(
cutToLength(
placeholders.apply(embedBuilder.getDescription()),
discordPlaceholders.apply(embedBuilder.getDescription()),
MessageEmbed.DESCRIPTION_MAX_LENGTH
)
);
@ -361,7 +380,7 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
builder.addEmbed(embedBuilder.build());
}
ResultMappers.runInPlainContext(() -> {
DiscordPlaceholders.with(DiscordPlaceholders.MappingState.PLAIN, () -> {
builder.setWebhookUsername(placeholders.apply(builder.getWebhookUsername()));
builder.setWebhookAvatarUrl(placeholders.apply(builder.getWebhookAvatarUrl()));
});

View File

@ -0,0 +1,49 @@
/*
* This file is part of the DiscordSRV API, licensed under the MIT License
* Copyright (c) 2016-2023 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;
import java.util.function.Function;
/**
* A helper class to handle replacing placeholders with Discord code blocks.
*/
public interface DiscordPlaceholders {
ThreadLocal<MappingState> MAPPING_STATE = ThreadLocal.withInitial(() -> MappingState.NORMAL);
static void with(MappingState mappingState, Runnable runnable) {
MappingState before = MAPPING_STATE.get();
MAPPING_STATE.set(mappingState);
runnable.run();
MAPPING_STATE.set(before);
}
enum MappingState {
NORMAL,
PLAIN,
ANSI
}
String map(String input, Function<String, String> placeholders);
}

View File

@ -1,64 +0,0 @@
/*
* This file is part of the DiscordSRV API, licensed under the MIT License
* Copyright (c) 2016-2023 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.mapper;
import com.discordsrv.api.placeholder.PlaceholderService;
import java.util.function.Supplier;
public final class ResultMappers {
private static final ThreadLocal<Boolean> PLAIN = ThreadLocal.withInitial(() -> false);
private ResultMappers() {}
public static boolean isPlainContext() {
return PLAIN.get();
}
/**
* Utility method to run the provided {@link Runnable} where {@link PlaceholderService}s
* will use plain text without Discord formatting (instead of converting to Discord formatting).
* @param runnable a task that will be executed immediately
*/
public static void runInPlainContext(Runnable runnable) {
getInPlainContext(() -> {
runnable.run();
return null;
});
}
/**
* Utility method to run the provided {@link Runnable} where {@link PlaceholderService}s
* will use plain text without Discord formatting (instead of converting to Discord formatting).
* @param supplier a supplier that will be executed immediately
* @return the output of the supplier provided as parameter
*/
public static <T> T getInPlainContext(Supplier<T> supplier) {
PLAIN.set(true);
T output = supplier.get();
PLAIN.set(false);
return output;
}
}

View File

@ -66,6 +66,7 @@ import com.discordsrv.common.messageforwarding.game.StopMessageModule;
import com.discordsrv.common.messageforwarding.game.minecrafttodiscord.MentionCachingModule;
import com.discordsrv.common.module.ModuleManager;
import com.discordsrv.common.module.type.AbstractModule;
import com.discordsrv.common.placeholder.DiscordPlaceholdersImpl;
import com.discordsrv.common.placeholder.PlaceholderServiceImpl;
import com.discordsrv.common.placeholder.context.GlobalTextHandlingContext;
import com.discordsrv.common.placeholder.result.ComponentResultStringifier;
@ -116,6 +117,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
private EventBusImpl eventBus;
private ProfileManager profileManager;
private PlaceholderServiceImpl placeholderService;
private DiscordPlaceholdersImpl discordPlaceholders;
private ComponentFactory componentFactory;
private DiscordAPIImpl discordAPI;
private DiscordConnectionDetailsImpl discordConnectionDetails;
@ -162,6 +164,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
this.moduleManager = new ModuleManager(this);
this.profileManager = new ProfileManager(this);
this.placeholderService = new PlaceholderServiceImpl(this);
this.discordPlaceholders = new DiscordPlaceholdersImpl();
this.componentFactory = new ComponentFactory(this);
this.discordAPI = new DiscordAPIImpl(this);
this.discordConnectionDetails = new DiscordConnectionDetailsImpl(this);
@ -262,6 +265,11 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
return placeholderService;
}
@Override
public final @NotNull DiscordPlaceholdersImpl discordPlaceholders() {
return discordPlaceholders;
}
@Override
public final @NotNull ComponentFactory componentFactory() {
return componentFactory;

View File

@ -20,6 +20,7 @@ package com.discordsrv.common;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.module.type.Module;
import com.discordsrv.api.placeholder.DiscordPlaceholders;
import com.discordsrv.common.bootstrap.IBootstrap;
import com.discordsrv.common.channel.ChannelConfigHelper;
import com.discordsrv.common.command.game.handler.ICommandHandler;
@ -87,6 +88,10 @@ public interface DiscordSRV extends DiscordSRVApi {
@NotNull
PlaceholderServiceImpl placeholderService();
@Override
@NotNull
DiscordPlaceholders discordPlaceholders();
@Override
@NotNull
DiscordAPIImpl discordAPI();

View File

@ -31,6 +31,7 @@ import com.discordsrv.api.event.bus.EventPriority;
import com.discordsrv.api.event.bus.Subscribe;
import com.discordsrv.api.event.events.message.forward.game.GameChatMessageForwardedEvent;
import com.discordsrv.api.event.events.message.receive.game.GameChatMessageReceiveEvent;
import com.discordsrv.api.placeholder.DiscordPlaceholders;
import com.discordsrv.api.placeholder.FormattedText;
import com.discordsrv.api.placeholder.util.Placeholders;
import com.discordsrv.common.DiscordSRV;
@ -50,8 +51,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MinecraftToDiscordChatModule
extends AbstractGameMessageModule<MinecraftToDiscordChatConfig, GameChatMessageReceiveEvent> {
public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<MinecraftToDiscordChatConfig, GameChatMessageReceiveEvent> {
public MinecraftToDiscordChatModule(DiscordSRV discordSRV) {
super(discordSRV, "MINECRAFT_TO_DISCORD");
@ -203,10 +203,6 @@ public class MinecraftToDiscordChatModule
.sorted(Comparator.comparingInt(mention -> ((MentionCachingModule.CachedMention) mention).searchLength()).reversed())
.forEachOrdered(mention -> channelMessagePlaceholders.replaceAll(mention.search(), mention.mention()));
String formattedMessage = DiscordFormattingUtil.escapeFormatting(
DiscordFormattingUtil.escapeQuotes(
channelMessagePlaceholders.toString()));
List<AllowedMention> allowedMentions = new ArrayList<>();
if (mentionConfig.get(cfg -> cfg.users, false) && player.hasPermission("discordsrv.mention.user")) {
allowedMentions.add(AllowedMention.ALL_USERS);
@ -224,23 +220,41 @@ public class MinecraftToDiscordChatModule
}
}
if (mentionConfig.get(cfg -> cfg.everyone, false) && player.hasPermission("discordsrv.mention.everyone")) {
boolean everyone = mentionConfig.get(cfg -> cfg.everyone, false) && player.hasPermission("discordsrv.mention.everyone");
if (everyone) {
allowedMentions.add(AllowedMention.EVERYONE);
} else {
formattedMessage = formattedMessage
.replace("@everyone", "\\@\u200Beveryone") // zero-width-space
.replace("@here", "\\@\u200Bhere"); // zero-width-space
if (formattedMessage.contains("@everyone") || formattedMessage.contains("@here")) {
throw new IllegalStateException("@everyone or @here blocking unsuccessful");
}
}
return format.setAllowedMentions(allowedMentions)
.toFormatter()
.addContext(context)
.addReplacement("%message%", new FormattedText(formattedMessage))
.addReplacement("%message%", () -> {
String finalMessage = channelMessagePlaceholders.toString();
if (DiscordPlaceholders.MAPPING_STATE.get() != DiscordPlaceholders.MappingState.NORMAL) {
return preventEveryoneMentions(everyone, finalMessage, false);
} else {
String formattedMessage = DiscordFormattingUtil.escapeFormatting(
DiscordFormattingUtil.escapeQuotes(finalMessage));
return new FormattedText(preventEveryoneMentions(everyone, formattedMessage, true));
}
})
.applyPlaceholderService()
.build();
}
private String preventEveryoneMentions(boolean everyoneAllowed, String message, boolean permitBackslash) {
if (everyoneAllowed) {
// Nothing to do
return message;
}
message = message
.replace("@everyone", (permitBackslash ? "\\" : "") + "@\u200Beveryone") // zero-width-space
.replace("@here", (permitBackslash ? "\\" : "") + "@\u200Bhere"); // zero-width-space
if (message.contains("@everyone") || message.contains("@here")) {
throw new IllegalStateException("@everyone or @here blocking unsuccessful");
}
return message;
}
}

View File

@ -0,0 +1,100 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2023 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.placeholder;
import com.discordsrv.api.placeholder.DiscordPlaceholders;
import dev.vankka.mcdiscordreserializer.rules.DiscordMarkdownRules;
import dev.vankka.simpleast.core.TextStyle;
import dev.vankka.simpleast.core.node.Node;
import dev.vankka.simpleast.core.node.StyleNode;
import dev.vankka.simpleast.core.node.TextNode;
import dev.vankka.simpleast.core.parser.Parser;
import dev.vankka.simpleast.core.parser.Rule;
import dev.vankka.simpleast.core.simple.SimpleMarkdownRules;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class DiscordPlaceholdersImpl implements DiscordPlaceholders {
private final Parser<Object, Node<Object>, Object> parser;
public DiscordPlaceholdersImpl() {
List<Rule<Object, Node<Object>, Object>> rules = new ArrayList<>();
rules.add(SimpleMarkdownRules.createEscapeRule());
rules.add(SimpleMarkdownRules.createNewlineRule());
rules.add(DiscordMarkdownRules.createCodeBlockRule());
rules.add(DiscordMarkdownRules.createCodeStringRule());
rules.add(DiscordMarkdownRules.createSpecialTextRule());
this.parser = new Parser<>().addRules(rules);
}
@Override
public String map(String input, Function<String, String> placeholders) {
List<Node<Object>> nodes = parser.parse(input);
StringBuilder finalText = new StringBuilder();
StringBuilder text = new StringBuilder();
for (Node<Object> node : nodes) {
if (node instanceof TextNode) {
text.append(((TextNode<Object>) node).getContent());
} else if (node instanceof StyleNode) {
String content = text.toString();
text.setLength(0);
finalText.append(placeholders.apply(content));
for (Object style : ((StyleNode<?, ?>) node).getStyles()) {
if (!(style instanceof TextStyle)) {
continue;
}
TextStyle textStyle = (TextStyle) style;
String childText = ((TextNode<?>) node.getChildren().get(0)).getContent();
if (textStyle.getType() == TextStyle.Type.CODE_STRING) {
DiscordPlaceholders.with(MappingState.PLAIN, () -> finalText.append("`").append(placeholders.apply(childText)).append("`"));
} else if (textStyle.getType() == TextStyle.Type.CODE_BLOCK) {
String language = textStyle.getExtra().get("language");
if (language != null && language.equals("ansi")) {
DiscordPlaceholders.with(MappingState.ANSI, () -> finalText
.append("```ansi\n")
.append(placeholders.apply(childText))
.append("```")
);
} else {
DiscordPlaceholders.with(MappingState.PLAIN, () -> finalText
.append("```")
.append(language != null ? language : "")
.append("\n")
.append(placeholders.apply(childText))
.append("```")
);
}
}
}
}
}
finalText.append(placeholders.apply(text.toString()));
return finalText.toString();
}
}

View File

@ -19,9 +19,9 @@
package com.discordsrv.common.placeholder.result;
import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.api.placeholder.DiscordPlaceholders;
import com.discordsrv.api.placeholder.FormattedText;
import com.discordsrv.api.placeholder.mapper.PlaceholderResultMapper;
import com.discordsrv.api.placeholder.mapper.ResultMappers;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil;
import net.kyori.adventure.text.Component;
@ -43,10 +43,14 @@ public class ComponentResultStringifier implements PlaceholderResultMapper {
}
if (result instanceof Component) {
Component component = (Component) result;
if (ResultMappers.isPlainContext()) {
return PlainTextComponentSerializer.plainText().serialize(component);
} else {
return new FormattedText(discordSRV.componentFactory().discordSerializer().serialize(component));
DiscordPlaceholders.MappingState mappingState = DiscordPlaceholders.MAPPING_STATE.get();
switch (mappingState) {
case ANSI: // TODO: ansi serializer (?)
case PLAIN:
return PlainTextComponentSerializer.plainText().serialize(component);
default:
case NORMAL:
return new FormattedText(discordSRV.componentFactory().discordSerializer().serialize(component));
}
}
return null;