Add escaping content for sending to Discord, adventure updated

This commit is contained in:
Vankka 2021-09-13 22:59:56 +03:00
parent 212a23b17e
commit 3b1fd58043
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
15 changed files with 225 additions and 16 deletions

View File

@ -28,6 +28,9 @@ import java.util.function.Supplier;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/**
* Minecraft equivalent for {@link com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage.Formatter}.
*/
public interface EnhancedTextBuilder { public interface EnhancedTextBuilder {
EnhancedTextBuilder addContext(Object... context); EnhancedTextBuilder addContext(Object... context);

View File

@ -192,8 +192,17 @@ public interface SendableDiscordMessage {
*/ */
@NotNull @NotNull
SendableDiscordMessage build(); 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 { interface Formatter {
Formatter addContext(Object... context); Formatter addContext(Object... context);

View File

@ -157,5 +157,17 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
public @NotNull SendableDiscordMessage build() { public @NotNull SendableDiscordMessage build() {
return new SendableDiscordMessageImpl(content, embeds, allowedMentions, webhookUsername, webhookAvatarUrl); 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;
}
} }
} }

View File

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

View File

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

View File

@ -49,4 +49,9 @@ public interface DiscordSRVPlayer {
@NotNull @NotNull
UUID uuid(); UUID uuid();
@Placeholder("totally_my_username") // TODO: remove
default String totallyMyUsername() {
return "*hi";
}
} }

View File

@ -20,7 +20,7 @@ ext {
// Configurate // Configurate
configurateVersion = '4.1.1' configurateVersion = '4.1.1'
// Adventure & Adventure Platform // Adventure & Adventure Platform
adventureVersion = '4.8.1' adventureVersion = '4.9.1'
adventurePlatformVersion = '4.0.0-SNAPSHOT' adventurePlatformVersion = '4.0.0-SNAPSHOT'
} }

View File

@ -23,7 +23,6 @@ dependencies {
// Adventure // Adventure
runtimeDownloadApi 'net.kyori:adventure-platform-bukkit:' + rootProject.adventurePlatformVersion runtimeDownloadApi 'net.kyori:adventure-platform-bukkit:' + rootProject.adventurePlatformVersion
runtimeDownloadApi 'net.kyori:adventure-text-serializer-craftbukkit:' + rootProject.adventurePlatformVersion
} }
shadowJar { shadowJar {

View File

@ -24,7 +24,7 @@ import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.common.channel.DefaultGlobalChannel; import com.discordsrv.common.channel.DefaultGlobalChannel;
import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.component.util.ComponentUtil;
import io.papermc.paper.event.player.AsyncChatEvent; 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.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;

View File

@ -23,8 +23,8 @@ import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.platform.bukkit.BukkitComponentSerializer;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.craftbukkit.BukkitComponentSerializer;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;

View File

@ -18,7 +18,10 @@
package com.discordsrv.common.discord.api.message; 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.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.api.placeholder.PlaceholderService;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.placeholder.converter.ComponentResultConverter; import com.discordsrv.common.placeholder.converter.ComponentResultConverter;
@ -39,7 +42,7 @@ public class SendableDiscordMessageFormatterImpl implements SendableDiscordMessa
public SendableDiscordMessageFormatterImpl(DiscordSRV discordSRV, SendableDiscordMessage.Builder builder) { public SendableDiscordMessageFormatterImpl(DiscordSRV discordSRV, SendableDiscordMessage.Builder builder) {
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
this.builder = builder; this.builder = builder.clone();
} }
@Override @Override
@ -50,7 +53,7 @@ public class SendableDiscordMessageFormatterImpl implements SendableDiscordMessa
@Override @Override
public SendableDiscordMessage.Formatter addReplacement(Pattern target, Function<Matcher, Object> replacement) { public SendableDiscordMessage.Formatter addReplacement(Pattern target, Function<Matcher, Object> replacement) {
this.replacements.put(target, replacement); this.replacements.put(target, wrapFunction(replacement));
return this; return this;
} }
@ -64,16 +67,90 @@ public class SendableDiscordMessageFormatterImpl implements SendableDiscordMessa
return new Placeholders(input) return new Placeholders(input)
.addAll(replacements) .addAll(replacements)
.replaceAll(PlaceholderService.PATTERN, .replaceAll(PlaceholderService.PATTERN,
matcher -> discordSRV.placeholderService().getResultAsString(matcher, context)) wrapFunction(
matcher -> discordSRV.placeholderService().getResultAsString(matcher, context)))
.get(); .get();
}; };
ComponentResultConverter.plain(() ->
builder.setWebhookUsername(placeholders.apply(builder.getWebhookUsername())));
builder.setContent(placeholders.apply(builder.getContent())); builder.setContent(placeholders.apply(builder.getContent()));
// TODO: rest of the content, escaping unwanted characters List<DiscordMessageEmbed> 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<DiscordMessageEmbed.Field> 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(); return builder.build();
} }
private Function<Matcher, Object> wrapFunction(Function<Matcher, Object> 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;
};
}
} }

View File

@ -56,7 +56,6 @@ public class DiscordChatListener extends AbstractListener {
} }
DiscordTextChannel channel = event.getChannel(); DiscordTextChannel channel = event.getChannel();
Component message = MinecraftSerializer.INSTANCE.serialize(event.getMessageContent());
OrDefault<Pair<GameChannel, BaseChannelConfig>> channelPair = discordSRV.channelConfig().orDefault(channel); OrDefault<Pair<GameChannel, BaseChannelConfig>> channelPair = discordSRV.channelConfig().orDefault(channel);
GameChannel gameChannel = channelPair.get(Pair::getKey); GameChannel gameChannel = channelPair.get(Pair::getKey);
@ -73,6 +72,8 @@ public class DiscordChatListener extends AbstractListener {
} }
DiscordUser user = event.getDiscordMessage().getAuthor(); DiscordUser user = event.getDiscordMessage().getAuthor();
Component message = MinecraftSerializer.INSTANCE.serialize(event.getMessageContent());
MinecraftComponent component = discordSRV.componentFactory() MinecraftComponent component = discordSRV.componentFactory()
.enhancedBuilder(format) .enhancedBuilder(format)
.addContext(event.getDiscordMessage(), user) .addContext(event.getDiscordMessage(), user)

View File

@ -52,7 +52,6 @@ public class GameChatListener extends AbstractListener {
} }
GameChannel gameChannel = event.getGameChannel(); GameChannel gameChannel = event.getGameChannel();
Component message = ComponentUtil.fromAPI(event.message());
OrDefault<BaseChannelConfig> channelConfig = discordSRV.channelConfig().orDefault(gameChannel); OrDefault<BaseChannelConfig> channelConfig = discordSRV.channelConfig().orDefault(gameChannel);
OrDefault<MinecraftToDiscordChatConfig> chatConfig = channelConfig.map(cfg -> cfg.minecraftToDiscord); OrDefault<MinecraftToDiscordChatConfig> chatConfig = channelConfig.map(cfg -> cfg.minecraftToDiscord);
@ -62,9 +61,12 @@ public class GameChatListener extends AbstractListener {
return; return;
} }
Component message = ComponentUtil.fromAPI(event.message());
String serializedMessage = DiscordSerializer.INSTANCE.serialize(message);
SendableDiscordMessage discordMessage = discordSRV.discordAPI().format(builder) SendableDiscordMessage discordMessage = discordSRV.discordAPI().format(builder)
.addContext(event.getPlayer()) .addContext(event.getPlayer())
.addReplacement("%message%", DiscordSerializer.INSTANCE.serialize(message)) .addReplacement("%message%", serializedMessage)
.build(); .build();
List<Long> channelIds = channelConfig.get(cfg -> cfg instanceof ChannelConfig ? ((ChannelConfig) cfg).channelIds : null); List<Long> channelIds = channelConfig.get(cfg -> cfg instanceof ChannelConfig ? ((ChannelConfig) cfg).channelIds : null);

View File

@ -30,7 +30,7 @@ public class ComponentResultConverter implements PlaceholderResultConverter {
private static final ThreadLocal<Boolean> PLAIN_CONTEXT = new ThreadLocal<>(); private static final ThreadLocal<Boolean> PLAIN_CONTEXT = new ThreadLocal<>();
public static void plain(Runnable runnable) { public static void plainComponents(Runnable runnable) {
PLAIN_CONTEXT.set(true); PLAIN_CONTEXT.set(true);
runnable.run(); runnable.run();
PLAIN_CONTEXT.set(false); PLAIN_CONTEXT.set(false);

View File

@ -76,7 +76,9 @@ public class Placeholders {
Function<Matcher, Object> replacement = entry.getValue(); Function<Matcher, Object> replacement = entry.getValue();
Object value = replacement.apply(matcher); Object value = replacement.apply(matcher);
input = matcher.replaceAll(String.valueOf(value)); input = matcher.replaceAll(
Matcher.quoteReplacement(
String.valueOf(value)));
} }
return input; return input;
} }