diff --git a/api/src/main/java/com/discordsrv/api/component/EnhancedTextBuilder.java b/api/src/main/java/com/discordsrv/api/component/EnhancedTextBuilder.java new file mode 100644 index 00000000..8613c434 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/component/EnhancedTextBuilder.java @@ -0,0 +1,58 @@ +/* + * 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.component; + +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public interface EnhancedTextBuilder { + + EnhancedTextBuilder addContext(Object... context); + + default EnhancedTextBuilder addReplacement(String target, Object replacement) { + return addReplacement(Pattern.compile(target, Pattern.LITERAL), replacement); + } + + default EnhancedTextBuilder addReplacement(Pattern target, Object replacement) { + return addReplacement(target, matcher -> replacement); + } + + default EnhancedTextBuilder addReplacement(String target, Supplier replacement) { + return addReplacement(Pattern.compile(target, Pattern.LITERAL), replacement); + } + + default EnhancedTextBuilder addReplacement(Pattern target, Supplier replacement) { + return addReplacement(target, matcher -> replacement.get()); + } + + default EnhancedTextBuilder addReplacement(String target, Function replacement) { + return addReplacement(Pattern.compile(target, Pattern.LITERAL), replacement); + } + + EnhancedTextBuilder addReplacement(Pattern target, Function replacement); + + MinecraftComponent build(); +} diff --git a/api/src/main/java/com/discordsrv/api/component/MinecraftComponent.java b/api/src/main/java/com/discordsrv/api/component/MinecraftComponent.java index 29397649..70e5d255 100644 --- a/api/src/main/java/com/discordsrv/api/component/MinecraftComponent.java +++ b/api/src/main/java/com/discordsrv/api/component/MinecraftComponent.java @@ -66,7 +66,7 @@ public interface MinecraftComponent { * * @param gsonSerializerClass the gson serializer class * @return a adapter that will convert to/from relocated or unrelocated adventure classes to/from json - * @throws IllegalArgumentException if the provided class is not a Adventure GsonComponentSerializer + * @throws IllegalArgumentException if the provided class is not an Adventure GsonComponentSerializer */ @NotNull Adapter adventureAdapter(@NotNull Class gsonSerializerClass); @@ -96,7 +96,7 @@ public interface MinecraftComponent { } /** - * A Adventure adapter, converts from/to given adventure components from/to json. + * An Adventure adapter, converts from/to given adventure components from/to json. */ interface Adapter { diff --git a/api/src/main/java/com/discordsrv/api/component/MinecraftComponentFactory.java b/api/src/main/java/com/discordsrv/api/component/MinecraftComponentFactory.java index 7826ffc8..5d69a5e5 100644 --- a/api/src/main/java/com/discordsrv/api/component/MinecraftComponentFactory.java +++ b/api/src/main/java/com/discordsrv/api/component/MinecraftComponentFactory.java @@ -39,4 +39,6 @@ public interface MinecraftComponentFactory { */ @NotNull MinecraftComponent empty(); + + EnhancedTextBuilder enhancedBuilder(String content); } diff --git a/api/src/main/java/com/discordsrv/api/discord/api/DiscordAPI.java b/api/src/main/java/com/discordsrv/api/discord/api/DiscordAPI.java index 6f95188e..776631e3 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/DiscordAPI.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/DiscordAPI.java @@ -27,6 +27,7 @@ import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; +import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; import com.discordsrv.api.discord.api.entity.user.DiscordUser; import org.jetbrains.annotations.NotNull; @@ -37,6 +38,8 @@ import java.util.Optional; */ public interface DiscordAPI { + SendableDiscordMessage.Formatter format(SendableDiscordMessage.Builder message); + /** * Gets a Discord message channel by id, the provided entity can be cached and will not update if it changes on Discord. * @param id the id for the message channel 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 f5c06d42..679832a6 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 @@ -29,6 +29,10 @@ import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * A message that can be sent to Discord. @@ -190,5 +194,33 @@ public interface SendableDiscordMessage { SendableDiscordMessage build(); } + interface Formatter { + + Formatter addContext(Object... context); + + default Formatter addReplacement(String target, Object replacement) { + return addReplacement(Pattern.compile(target, Pattern.LITERAL), replacement); + } + + default Formatter addReplacement(Pattern target, Object replacement) { + return addReplacement(target, matcher -> replacement); + } + + default Formatter addReplacement(String target, Supplier replacement) { + return addReplacement(Pattern.compile(target, Pattern.LITERAL), replacement); + } + + default Formatter addReplacement(Pattern target, Supplier replacement) { + return addReplacement(target, matcher -> replacement.get()); + } + + default Formatter addReplacement(String target, Function replacement) { + return addReplacement(Pattern.compile(target, Pattern.LITERAL), replacement); + } + + Formatter addReplacement(Pattern target, Function replacement); + + SendableDiscordMessage build(); + } } 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 82497a7c..815b8fef 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 @@ -42,10 +42,10 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage { private final String webhookAvatarUrl; protected SendableDiscordMessageImpl(String content, - List embeds, - Set allowedMentions, - String webhookUsername, - String webhookAvatarUrl) { + List embeds, + Set allowedMentions, + String webhookUsername, + String webhookAvatarUrl) { this.content = content; this.embeds = embeds; this.allowedMentions = allowedMentions; @@ -78,7 +78,7 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage { return webhookAvatarUrl; } - public static class BuilderImpl implements Builder { + public static class BuilderImpl implements SendableDiscordMessage.Builder { private String content; private final List embeds = new ArrayList<>(); diff --git a/api/src/main/java/com/discordsrv/api/event/events/message/send/game/AbstractGameMessageSendEvent.java b/api/src/main/java/com/discordsrv/api/event/events/message/send/game/AbstractGameMessageSendEvent.java index a9a9c51c..65405d5b 100644 --- a/api/src/main/java/com/discordsrv/api/event/events/message/send/game/AbstractGameMessageSendEvent.java +++ b/api/src/main/java/com/discordsrv/api/event/events/message/send/game/AbstractGameMessageSendEvent.java @@ -24,43 +24,32 @@ package com.discordsrv.api.event.events.message.send.game; import com.discordsrv.api.channel.GameChannel; +import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; import com.discordsrv.api.event.events.Cancellable; import com.discordsrv.api.event.events.Processable; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; public abstract class AbstractGameMessageSendEvent implements Cancellable, Processable { - private String discordMessage; - private String discordUsername; + private SendableDiscordMessage discordMessage; private GameChannel targetChannel; private boolean cancelled; private boolean processed; - public AbstractGameMessageSendEvent(@NotNull String discordMessage, @Nullable String discordUsername, @NotNull GameChannel targetChannel) { + public AbstractGameMessageSendEvent(@NotNull SendableDiscordMessage discordMessage, @NotNull GameChannel targetChannel) { this.discordMessage = discordMessage; - this.discordUsername = discordUsername; this.targetChannel = targetChannel; } @NotNull - public String getDiscordMessage() { + public SendableDiscordMessage getDiscordMessage() { return discordMessage; } - public void setDiscordMessage(@NotNull String discordMessage) { + public void setDiscordMessage(@NotNull SendableDiscordMessage discordMessage) { this.discordMessage = discordMessage; } - @Nullable - public String getDiscordUsername() { - return discordUsername; - } - - public void setDiscordUsername(@Nullable String discordUsername) { - this.discordUsername = discordUsername; - } - @NotNull public GameChannel getTargetChannel() { return targetChannel; diff --git a/api/src/main/java/com/discordsrv/api/event/events/message/send/game/ChatMessageSendEvent.java b/api/src/main/java/com/discordsrv/api/event/events/message/send/game/ChatMessageSendEvent.java index c609b941..6f6604d6 100644 --- a/api/src/main/java/com/discordsrv/api/event/events/message/send/game/ChatMessageSendEvent.java +++ b/api/src/main/java/com/discordsrv/api/event/events/message/send/game/ChatMessageSendEvent.java @@ -24,12 +24,12 @@ package com.discordsrv.api.event.events.message.send.game; import com.discordsrv.api.channel.GameChannel; +import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; public class ChatMessageSendEvent extends AbstractGameMessageSendEvent { - public ChatMessageSendEvent(@NotNull String discordMessage, @Nullable String discordUsername, @NotNull GameChannel targetChannel) { - super(discordMessage, discordUsername, targetChannel); + public ChatMessageSendEvent(@NotNull SendableDiscordMessage discordMessage, @NotNull GameChannel targetChannel) { + super(discordMessage, targetChannel); } } diff --git a/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderLookupResult.java b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderLookupResult.java index c31cf5a7..04b11e88 100644 --- a/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderLookupResult.java +++ b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderLookupResult.java @@ -32,7 +32,7 @@ public class PlaceholderLookupResult { public static final PlaceholderLookupResult UNKNOWN_PLACEHOLDER = new PlaceholderLookupResult(Type.UNKNOWN_PLACEHOLDER); public static PlaceholderLookupResult success(Object result) { - return new PlaceholderLookupResult(String.valueOf(result)); + return new PlaceholderLookupResult(result); } public static PlaceholderLookupResult newLookup(String placeholder, Set extras) { @@ -40,7 +40,7 @@ public class PlaceholderLookupResult { } private final Type type; - private final String value; + private final Object value; private final Set extras; protected PlaceholderLookupResult(Type type) { @@ -49,7 +49,7 @@ public class PlaceholderLookupResult { this.extras = null; } - protected PlaceholderLookupResult(String value) { + protected PlaceholderLookupResult(Object value) { this.type = Type.SUCCESS; this.value = value; this.extras = null; @@ -65,7 +65,7 @@ public class PlaceholderLookupResult { return type; } - public String getValue() { + public Object getValue() { return value; } diff --git a/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderResultConverter.java b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderResultConverter.java new file mode 100644 index 00000000..76f30633 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderResultConverter.java @@ -0,0 +1,37 @@ +/* + * 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; + +import org.jetbrains.annotations.NotNull; + +@FunctionalInterface +public interface PlaceholderResultConverter { + + /** + * Converts a successful placeholder lookup result into a {@link String}. + * @param result the result + * @return the result in {@link String} form + */ + String convertPlaceholderResult(@NotNull Object result); +} diff --git a/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java index 5769e2e8..8f6c600d 100644 --- a/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java +++ b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java @@ -23,7 +23,10 @@ package com.discordsrv.api.placeholder; +import org.jetbrains.annotations.NotNull; + import java.util.Set; +import java.util.regex.Matcher; import java.util.regex.Pattern; public interface PlaceholderService { @@ -38,9 +41,16 @@ public interface PlaceholderService { */ Pattern RECURSIVE_PATTERN = Pattern.compile("(\\{)(.+)(})"); - PlaceholderLookupResult lookupPlaceholder(String placeholder, Set context); - PlaceholderLookupResult lookupPlaceholder(String placeholder, Object... context); + void addResultConverter(@NotNull PlaceholderResultConverter resultConverter); + void removeResultConverter(@NotNull PlaceholderResultConverter resultConverter); + + String replacePlaceholders(@NotNull String placeholder, @NotNull Set context); + String replacePlaceholders(@NotNull String placeholder, @NotNull Object... context); + + PlaceholderLookupResult lookupPlaceholder(@NotNull String placeholder, @NotNull Set context); + PlaceholderLookupResult lookupPlaceholder(@NotNull String placeholder, @NotNull Object... context); + + Object getResult(@NotNull Matcher matcher, @NotNull Set context); + String getResultAsString(@NotNull Matcher matcher, @NotNull Set context); - String replacePlaceholders(String placeholder, Set context); - String replacePlaceholders(String placeholder, Object... context); } 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 0ed79213..cca6529e 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java @@ -26,9 +26,11 @@ import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.craftbukkit.BukkitComponentSerializer; import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; import java.lang.reflect.Method; +@SuppressWarnings("NullableProblems") // BukkitOfflinePlayer nullability public class BukkitPlayer extends BukkitOfflinePlayer implements IPlayer { private static final Method DISPLAY_NAME_METHOD; // Paper 1.16+ @@ -72,12 +74,13 @@ public class BukkitPlayer extends BukkitOfflinePlayer implements IPlayer { @SuppressWarnings("deprecation") // Paper @Override - public Component displayName() { + public @NotNull Component displayName() { if (DISPLAY_NAME_METHOD != null) { try { return ComponentUtil.fromUnrelocated(DISPLAY_NAME_METHOD.invoke(player)); } catch (Throwable ignored) {} } + // Use the legacy method return BukkitComponentSerializer.legacy().deserialize(player.getDisplayName()); } diff --git a/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java b/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java index 4df9fc94..bcfe2e1e 100644 --- a/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java +++ b/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java @@ -67,7 +67,7 @@ public class BungeePlayer implements IPlayer { } @Override - public Component displayName() { + public @NotNull Component displayName() { return BungeeComponentUtil.fromLegacy(player.getDisplayName()); } } diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index f42a0832..cbb8f2c3 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -41,6 +41,7 @@ import com.discordsrv.common.listener.DefaultChatListener; import com.discordsrv.common.logging.DependencyLoggingFilter; import com.discordsrv.common.logging.logger.backend.LoggingBackend; import com.discordsrv.common.placeholder.PlaceholderServiceImpl; +import com.discordsrv.common.placeholder.converter.ComponentResultConverter; import net.dv8tion.jda.api.JDA; import org.jetbrains.annotations.NotNull; @@ -84,7 +85,7 @@ public abstract class AbstractDiscordSRV channels() { + private Map channels() { return discordSRV.config().channels; } @@ -64,12 +63,12 @@ public class ChannelConfig { } public OrDefault orDefault(String ownerName, String channelName) { - ChannelConfigHolder defaultConfig = channels().computeIfAbsent( - "default", key -> new ChannelConfigHolder(new BaseChannelConfig())); + BaseChannelConfig defaultConfig = channels().computeIfAbsent( + "default", key -> new BaseChannelConfig()); return new OrDefault<>( get(ownerName, channelName), - defaultConfig.get() + defaultConfig ); } @@ -79,15 +78,15 @@ public class ChannelConfig { public BaseChannelConfig get(String ownerName, String channelName) { if (ownerName != null) { - ChannelConfigHolder config = channels().get(ownerName + ":" + channelName); + BaseChannelConfig config = channels().get(ownerName + ":" + channelName); if (config != null) { - return config.get(); + return config; } GameChannel gameChannel = channels.get(channelName); if (gameChannel != null && gameChannel.getOwnerName().equals(ownerName)) { config = channels().get(channelName); - return config != null ? config.get() : null; + return config; } return null; } diff --git a/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java b/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java index b5a48712..74fed782 100644 --- a/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java +++ b/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java @@ -18,14 +18,27 @@ package com.discordsrv.common.component; +import com.discordsrv.api.component.EnhancedTextBuilder; import com.discordsrv.api.component.MinecraftComponent; import com.discordsrv.api.component.MinecraftComponentFactory; +import com.discordsrv.common.DiscordSRV; import org.jetbrains.annotations.NotNull; public class ComponentFactory implements MinecraftComponentFactory { + private final DiscordSRV discordSRV; + + public ComponentFactory(DiscordSRV discordSRV) { + this.discordSRV = discordSRV; + } + @Override public @NotNull MinecraftComponent empty() { return MinecraftComponentImpl.empty(); } + + @Override + public EnhancedTextBuilder enhancedBuilder(String content) { + return new EnhancedTextBuilderImpl(discordSRV, content); + } } diff --git a/common/src/main/java/com/discordsrv/common/component/EnhancedTextBuilderImpl.java b/common/src/main/java/com/discordsrv/common/component/EnhancedTextBuilderImpl.java new file mode 100644 index 00000000..d5b42314 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/component/EnhancedTextBuilderImpl.java @@ -0,0 +1,72 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2021 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 . + */ + +package com.discordsrv.common.component; + +import com.discordsrv.api.component.EnhancedTextBuilder; +import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.placeholder.PlaceholderService; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.component.util.ComponentUtil; +import dev.vankka.enhancedlegacytext.EnhancedComponentBuilder; +import dev.vankka.enhancedlegacytext.EnhancedLegacyText; +import net.kyori.adventure.text.Component; + +import java.util.*; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class EnhancedTextBuilderImpl implements EnhancedTextBuilder { + + private final Set context = new HashSet<>(); + private final Map> replacements = new HashMap<>(); + + private final DiscordSRV discordSRV; + private final String enhancedFormat; + + public EnhancedTextBuilderImpl(DiscordSRV discordSRV, String enhancedFormat) { + this.discordSRV = discordSRV; + this.enhancedFormat = enhancedFormat; + } + + @Override + public EnhancedTextBuilder addContext(Object... context) { + this.context.addAll(Arrays.asList(context)); + return this; + } + + @Override + public EnhancedTextBuilder addReplacement(Pattern target, Function replacement) { + this.replacements.put(target, replacement); + return this; + } + + @Override + public MinecraftComponent build() { + EnhancedComponentBuilder builder = EnhancedLegacyText.get() + .buildComponent(enhancedFormat); + + replacements.forEach(builder::replaceAll); + builder.replaceAll(PlaceholderService.PATTERN, + matcher -> discordSRV.placeholderService().getResult(matcher, context)); + + Component component = builder.build(); + return ComponentUtil.toAPI(component); + } +} diff --git a/common/src/main/java/com/discordsrv/common/config/connection/ConnectionConfig.java b/common/src/main/java/com/discordsrv/common/config/connection/ConnectionConfig.java index 1fc80828..4cd83965 100644 --- a/common/src/main/java/com/discordsrv/common/config/connection/ConnectionConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/connection/ConnectionConfig.java @@ -26,6 +26,8 @@ import org.spongepowered.configurate.objectmapping.meta.Comment; public class ConnectionConfig implements Config { public static final String FILE_NAME = "connections.yaml"; + + @Override public final String getFileName() { return FILE_NAME; } @@ -37,5 +39,6 @@ public class ConnectionConfig implements Config { @Comment("Don't know what this is? Neither do I") public String token = "Token here"; + } } diff --git a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java index a9992443..9a92d3d9 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java @@ -22,7 +22,6 @@ import com.discordsrv.common.config.Config; import com.discordsrv.common.config.annotation.DefaultOnly; import com.discordsrv.common.config.main.channels.BaseChannelConfig; import com.discordsrv.common.config.main.channels.ChannelConfig; -import com.discordsrv.common.config.main.channels.ChannelConfigHolder; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import java.util.HashMap; @@ -32,13 +31,15 @@ import java.util.Map; public class MainConfig implements Config { public static final String FILE_NAME = "config.yaml"; + + @Override public final String getFileName() { return FILE_NAME; } @DefaultOnly("default") - public Map channels = new HashMap() {{ - put("default", new ChannelConfigHolder(new BaseChannelConfig())); - put("global", new ChannelConfigHolder(new ChannelConfig())); + public Map channels = new HashMap() {{ + put("default", new BaseChannelConfig()); + put("global", new ChannelConfig()); }}; } diff --git a/common/src/main/java/com/discordsrv/common/config/main/channels/BaseChannelConfig.java b/common/src/main/java/com/discordsrv/common/config/main/channels/BaseChannelConfig.java index d572b885..684db393 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/channels/BaseChannelConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/channels/BaseChannelConfig.java @@ -19,10 +19,50 @@ package com.discordsrv.common.config.main.channels; import com.discordsrv.common.config.main.channels.minecraftodiscord.MinecraftToDiscordChatConfig; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.ObjectMapper; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.lang.reflect.Type; @ConfigSerializable public class BaseChannelConfig { public MinecraftToDiscordChatConfig minecraftToDiscord = new MinecraftToDiscordChatConfig(); + + + public static class Serializer implements TypeSerializer { + + private final ObjectMapper.Factory mapperFactory; + + public Serializer(ObjectMapper.Factory mapperFactory) { + this.mapperFactory = mapperFactory; + } + + @Override + public BaseChannelConfig deserialize(Type type, ConfigurationNode node) throws SerializationException { + return (BaseChannelConfig) mapperFactory.asTypeSerializer() + .deserialize( + "default".equals(node.key()) ? BaseChannelConfig.class : ChannelConfig.class, + node + ); + } + + @Override + public void serialize(Type type, @Nullable BaseChannelConfig obj, ConfigurationNode node) throws SerializationException { + if (obj == null) { + node.set(null); + return; + } + + mapperFactory.asTypeSerializer().serialize( + "default".equals(node.key()) ? BaseChannelConfig.class : ChannelConfig.class, + obj, + node + ); + } + } } diff --git a/common/src/main/java/com/discordsrv/common/config/main/channels/ChannelConfig.java b/common/src/main/java/com/discordsrv/common/config/main/channels/ChannelConfig.java index edcee5c0..f3dd4149 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/channels/ChannelConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/channels/ChannelConfig.java @@ -20,7 +20,6 @@ package com.discordsrv.common.config.main.channels; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; -import org.spongepowered.configurate.objectmapping.meta.Setting; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -31,8 +30,6 @@ import java.util.List; @ConfigSerializable public class ChannelConfig extends BaseChannelConfig { - protected static final String CHANNEL_IDS_OPTION_NAME = "ChannelIds"; - public ChannelConfig() { // Clear everything besides channelIds by default (these will be filled back in by Configurate if they are in the config itself) for (Field field : getClass().getFields()) { @@ -50,7 +47,6 @@ public class ChannelConfig extends BaseChannelConfig { } } - @Setting(CHANNEL_IDS_OPTION_NAME) @Comment("The channels this in-game channel will forward to in Discord") public List channelIds = new ArrayList<>(Collections.singletonList("channel-id-here")); diff --git a/common/src/main/java/com/discordsrv/common/config/main/channels/ChannelConfigHolder.java b/common/src/main/java/com/discordsrv/common/config/main/channels/ChannelConfigHolder.java deleted file mode 100644 index b85cfbac..00000000 --- a/common/src/main/java/com/discordsrv/common/config/main/channels/ChannelConfigHolder.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * This file is part of DiscordSRV, licensed under the GPLv3 License - * Copyright (c) 2016-2021 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 . - */ - -package com.discordsrv.common.config.main.channels; - -import org.checkerframework.checker.nullness.qual.Nullable; -import org.spongepowered.configurate.ConfigurationNode; -import org.spongepowered.configurate.objectmapping.ObjectMapper; -import org.spongepowered.configurate.serialize.SerializationException; -import org.spongepowered.configurate.serialize.TypeSerializer; - -import java.lang.reflect.Type; - -/** - * A bit of a trick to have two different types in the same map with Configurate. - */ -public class ChannelConfigHolder { - - private final BaseChannelConfig obj; - - public ChannelConfigHolder(BaseChannelConfig obj) { - this.obj = obj; - } - - public BaseChannelConfig get() { - return obj; - } - - public static class Serializer implements TypeSerializer { - - private final ObjectMapper.Factory mapperFactory; - - public Serializer(ObjectMapper.Factory mapperFactory) { - this.mapperFactory = mapperFactory; - } - - @Override - public ChannelConfigHolder deserialize(Type type, ConfigurationNode node) throws SerializationException { - boolean channelsNotPresent = node.node(ChannelConfig.CHANNEL_IDS_OPTION_NAME).empty(); - BaseChannelConfig channelConfig = (BaseChannelConfig) mapperFactory - .get(channelsNotPresent ? BaseChannelConfig.class : ChannelConfig.class) - .load(node); - return new ChannelConfigHolder(channelConfig); - } - - @Override - public void serialize(Type type, @Nullable ChannelConfigHolder obj, ConfigurationNode node) throws SerializationException { - map(obj != null ? obj.get() : null, node); - } - - @SuppressWarnings("unchecked") - private void map(BaseChannelConfig obj, ConfigurationNode node) throws SerializationException { - mapperFactory - .get((Class) (obj instanceof ChannelConfig ? ChannelConfig.class : BaseChannelConfig.class)) - .save((T) obj, node); - } - - } -} diff --git a/common/src/main/java/com/discordsrv/common/config/main/channels/minecraftodiscord/MinecraftToDiscordChatConfig.java b/common/src/main/java/com/discordsrv/common/config/main/channels/minecraftodiscord/MinecraftToDiscordChatConfig.java index ee97401e..cdf4f7fb 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/channels/minecraftodiscord/MinecraftToDiscordChatConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/channels/minecraftodiscord/MinecraftToDiscordChatConfig.java @@ -28,8 +28,6 @@ public class MinecraftToDiscordChatConfig { @Setting("Format") public SendableDiscordMessage.Builder messageFormat = SendableDiscordMessage.builder() .setWebhookUsername("%player_display_name%") - .setContent("%player_message%");// TODO - - @Setting("UseWebhooks") - public boolean useWebhooks = false; + .setContent("%message%");// TODO + } diff --git a/common/src/main/java/com/discordsrv/common/config/manager/ConnectionConfigManager.java b/common/src/main/java/com/discordsrv/common/config/manager/ConnectionConfigManager.java index 4b7e2525..6f11c670 100644 --- a/common/src/main/java/com/discordsrv/common/config/manager/ConnectionConfigManager.java +++ b/common/src/main/java/com/discordsrv/common/config/manager/ConnectionConfigManager.java @@ -24,7 +24,9 @@ import com.discordsrv.common.config.manager.loader.YamlConfigLoaderProvider; import com.discordsrv.common.config.manager.manager.TranslatedConfigManager; import org.spongepowered.configurate.yaml.YamlConfigurationLoader; -public abstract class ConnectionConfigManager extends TranslatedConfigManager implements YamlConfigLoaderProvider { +public abstract class ConnectionConfigManager + extends TranslatedConfigManager + implements YamlConfigLoaderProvider { public ConnectionConfigManager(DiscordSRV discordSRV) { super(discordSRV); diff --git a/common/src/main/java/com/discordsrv/common/config/manager/MainConfigManager.java b/common/src/main/java/com/discordsrv/common/config/manager/MainConfigManager.java index 84642a3c..8f7d8604 100644 --- a/common/src/main/java/com/discordsrv/common/config/manager/MainConfigManager.java +++ b/common/src/main/java/com/discordsrv/common/config/manager/MainConfigManager.java @@ -18,22 +18,12 @@ package com.discordsrv.common.config.manager; -import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed; -import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.main.MainConfig; -import com.discordsrv.common.config.main.channels.ChannelConfigHolder; import com.discordsrv.common.config.manager.loader.YamlConfigLoaderProvider; import com.discordsrv.common.config.manager.manager.TranslatedConfigManager; -import com.discordsrv.common.config.serializer.ColorSerializer; -import com.discordsrv.common.config.serializer.DiscordMessageEmbedSerializer; -import com.discordsrv.common.config.serializer.SendableDiscordMessageSerializer; -import org.spongepowered.configurate.ConfigurationOptions; -import org.spongepowered.configurate.objectmapping.ObjectMapper; import org.spongepowered.configurate.yaml.YamlConfigurationLoader; -import java.awt.Color; - public abstract class MainConfigManager extends TranslatedConfigManager implements YamlConfigLoaderProvider { @@ -46,17 +36,4 @@ public abstract class MainConfigManager protected String fileName() { return MainConfig.FILE_NAME; } - - @Override - public ConfigurationOptions defaultOptions() { - return super.defaultOptions() - .serializers(builder -> { - ObjectMapper.Factory objectMapper = defaultObjectMapper(); - builder.register(Color.class, new ColorSerializer()); - builder.register(ChannelConfigHolder.class, new ChannelConfigHolder.Serializer(objectMapper)); - builder.register(DiscordMessageEmbed.Builder.class, new DiscordMessageEmbedSerializer()); - builder.register(DiscordMessageEmbed.Field.class, new DiscordMessageEmbedSerializer.FieldSerializer()); - builder.register(SendableDiscordMessage.Builder.class, new SendableDiscordMessageSerializer()); - }); - } } diff --git a/common/src/main/java/com/discordsrv/common/config/manager/manager/ConfigurateConfigManager.java b/common/src/main/java/com/discordsrv/common/config/manager/manager/ConfigurateConfigManager.java index c5c1147a..27a3294e 100644 --- a/common/src/main/java/com/discordsrv/common/config/manager/manager/ConfigurateConfigManager.java +++ b/common/src/main/java/com/discordsrv/common/config/manager/manager/ConfigurateConfigManager.java @@ -18,9 +18,15 @@ package com.discordsrv.common.config.manager.manager; +import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed; +import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.annotation.DefaultOnly; +import com.discordsrv.common.config.main.channels.BaseChannelConfig; import com.discordsrv.common.config.manager.loader.ConfigLoaderProvider; +import com.discordsrv.common.config.serializer.ColorSerializer; +import com.discordsrv.common.config.serializer.DiscordMessageEmbedSerializer; +import com.discordsrv.common.config.serializer.SendableDiscordMessageSerializer; import com.discordsrv.common.exception.ConfigException; import org.jetbrains.annotations.Nullable; import org.spongepowered.configurate.CommentedConfigurationNode; @@ -31,7 +37,9 @@ import org.spongepowered.configurate.loader.AbstractConfigurationLoader; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.ObjectMapper; import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.util.NamingSchemes; +import java.awt.Color; import java.nio.file.Path; import java.util.Arrays; import java.util.List; @@ -73,31 +81,40 @@ public abstract class ConfigurateConfigManager { + ObjectMapper.Factory objectMapper = configObjectMapper(); + builder.register(Color.class, new ColorSerializer()); + builder.register(BaseChannelConfig.class, new BaseChannelConfig.Serializer(objectMapper)); + builder.register(DiscordMessageEmbed.Builder.class, new DiscordMessageEmbedSerializer()); + builder.register(DiscordMessageEmbed.Field.class, new DiscordMessageEmbedSerializer.FieldSerializer()); + builder.register(SendableDiscordMessage.Builder.class, new SendableDiscordMessageSerializer()); + }); } public ConfigurationOptions configNodeOptions() { return defaultOptions(); } - protected ObjectMapper.Factory.Builder configObjectMapperBuilder() { - return objectMapperBuilder(); - } - - public ObjectMapper.Factory configObjectMapper() { - return configObjectMapper; - } - public ConfigurationOptions defaultNodeOptions() { return defaultOptions(); } + protected ObjectMapper.Factory.Builder objectMapperBuilder() { + return ObjectMapper.factoryBuilder() + .defaultNamingScheme(input -> { + String camelCase = NamingSchemes.CAMEL_CASE.coerce(input); // Gets rid of underscores and dashes + return Character.toUpperCase(camelCase.charAt(0)) + camelCase.substring(1); + }); + } + + protected ObjectMapper.Factory.Builder configObjectMapperBuilder() { + return objectMapperBuilder(); + } + protected ObjectMapper.Factory.Builder defaultObjectMapperBuilder() { - return configObjectMapperBuilder() + return objectMapperBuilder() .addProcessor(DefaultOnly.class, (data, value) -> (value1, destination) -> { String[] children = data.value(); boolean whitelist = data.whitelist(); @@ -131,6 +148,10 @@ public abstract class ConfigurateConfigManager) configuration.getClass(), node); loader.save(node); } catch (ConfigurateException e) { throw new ConfigException("Failed to load configuration", e); } } + + protected void save(T config, Class clazz, CommentedConfigurationNode node) throws SerializationException { + configObjectMapper().get(clazz).save(config, node); + } } diff --git a/common/src/main/java/com/discordsrv/common/config/manager/manager/TranslatedConfigManager.java b/common/src/main/java/com/discordsrv/common/config/manager/manager/TranslatedConfigManager.java index 5bd0c4be..4525738f 100644 --- a/common/src/main/java/com/discordsrv/common/config/manager/manager/TranslatedConfigManager.java +++ b/common/src/main/java/com/discordsrv/common/config/manager/manager/TranslatedConfigManager.java @@ -31,7 +31,8 @@ import org.spongepowered.configurate.yaml.YamlConfigurationLoader; import java.net.URL; -public abstract class TranslatedConfigManager> extends ConfigurateConfigManager { +public abstract class TranslatedConfigManager> + extends ConfigurateConfigManager { public TranslatedConfigManager(DiscordSRV discordSRV) { super(discordSRV); @@ -55,6 +56,7 @@ public abstract class TranslatedConfigManager) config.getClass(), node); + //node.set(config); translateNode(node, translation, translation.node("_comments")); } catch (ConfigurateException e) { throw new ConfigException(e); diff --git a/common/src/main/java/com/discordsrv/common/config/serializer/SendableDiscordMessageSerializer.java b/common/src/main/java/com/discordsrv/common/config/serializer/SendableDiscordMessageSerializer.java index 72e6d436..adc2ec93 100644 --- a/common/src/main/java/com/discordsrv/common/config/serializer/SendableDiscordMessageSerializer.java +++ b/common/src/main/java/com/discordsrv/common/config/serializer/SendableDiscordMessageSerializer.java @@ -45,7 +45,9 @@ public class SendableDiscordMessageSerializer implements TypeSerializer embedBuilders = new ArrayList<>(); obj.getEmbeds().forEach(embed -> embedBuilders.add(embed.toBuilder())); - node.setList(DiscordMessageEmbed.Builder.class, embedBuilders); + if (!embedBuilders.isEmpty()) { + node.node("Embeds").setList(DiscordMessageEmbed.Builder.class, embedBuilders); + } node.node("Content").set(obj.getContent()); } 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 b37924d5..6310c3f4 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 @@ -25,18 +25,22 @@ import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; +import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; import com.discordsrv.api.discord.api.entity.user.DiscordUser; import com.discordsrv.api.discord.api.exception.NotReadyException; import com.discordsrv.api.discord.api.exception.UnknownChannelException; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.main.channels.BaseChannelConfig; import com.discordsrv.common.config.main.channels.ChannelConfig; -import com.discordsrv.common.config.main.channels.ChannelConfigHolder; import com.discordsrv.common.discord.api.channel.DiscordDMChannelImpl; import com.discordsrv.common.discord.api.channel.DiscordTextChannelImpl; import com.discordsrv.common.discord.api.guild.DiscordGuildImpl; +import com.discordsrv.common.discord.api.message.SendableDiscordMessageFormatterImpl; import com.discordsrv.common.discord.api.user.DiscordUserImpl; -import com.github.benmanes.caffeine.cache.*; +import com.github.benmanes.caffeine.cache.AsyncCacheLoader; +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import com.github.benmanes.caffeine.cache.Expiry; +import com.github.benmanes.caffeine.cache.RemovalListener; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; @@ -72,6 +76,11 @@ public class DiscordAPIImpl implements DiscordAPI { return cachedClients.get(channelId); } + @Override + public SendableDiscordMessage.Formatter format(SendableDiscordMessage.Builder message) { + return new SendableDiscordMessageFormatterImpl(discordSRV, message); + } + @Override public @NotNull Optional getMessageChannelById(@NotNull String id) { Optional textChannel = getTextChannelById(id); @@ -163,8 +172,7 @@ public class DiscordAPIImpl implements DiscordAPI { private class WebhookCacheExpiry implements Expiry { private boolean isConfiguredChannel(String channelId) { - for (ChannelConfigHolder value : discordSRV.config().channels.values()) { - BaseChannelConfig config = value.get(); + for (BaseChannelConfig config : discordSRV.config().channels.values()) { if (config instanceof ChannelConfig && ((ChannelConfig) config).channelIds.contains(channelId)) { return true; diff --git a/common/src/main/java/com/discordsrv/common/discord/api/message/ReceivedDiscordMessageImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/message/ReceivedDiscordMessageImpl.java index 3615001b..f88203a9 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/message/ReceivedDiscordMessageImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/message/ReceivedDiscordMessageImpl.java @@ -23,11 +23,11 @@ import club.minnced.discord.webhook.receive.ReadonlyMessage; import club.minnced.discord.webhook.receive.ReadonlyUser; import club.minnced.discord.webhook.send.WebhookEmbed; import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; -import com.discordsrv.api.discord.api.exception.UnknownChannelException; import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; import com.discordsrv.api.discord.api.entity.message.impl.SendableDiscordMessageImpl; +import com.discordsrv.api.discord.api.exception.UnknownChannelException; import com.discordsrv.common.DiscordSRV; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; 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 new file mode 100644 index 00000000..2110e603 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/discord/api/message/SendableDiscordMessageFormatterImpl.java @@ -0,0 +1,79 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2021 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 . + */ + +package com.discordsrv.common.discord.api.message; + +import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; +import com.discordsrv.api.placeholder.PlaceholderService; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.placeholder.converter.ComponentResultConverter; +import com.discordsrv.common.string.util.Placeholders; + +import java.util.*; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SendableDiscordMessageFormatterImpl implements SendableDiscordMessage.Formatter { + + private final Set context = new HashSet<>(); + private final Map> replacements = new HashMap<>(); + + private final DiscordSRV discordSRV; + private final SendableDiscordMessage.Builder builder; + + public SendableDiscordMessageFormatterImpl(DiscordSRV discordSRV, SendableDiscordMessage.Builder builder) { + this.discordSRV = discordSRV; + this.builder = builder; + } + + @Override + public SendableDiscordMessage.Formatter addContext(Object... context) { + this.context.addAll(Arrays.asList(context)); + return this; + } + + @Override + public SendableDiscordMessage.Formatter addReplacement(Pattern target, Function replacement) { + this.replacements.put(target, replacement); + return this; + } + + @Override + public SendableDiscordMessage build() { + Function placeholders = input -> { + if (input == null) { + return null; + } + + return new Placeholders(input) + .addAll(replacements) + .replaceAll(PlaceholderService.PATTERN, + 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 + + return builder.build(); + } +} diff --git a/common/src/main/java/com/discordsrv/common/listener/DefaultChatListener.java b/common/src/main/java/com/discordsrv/common/listener/DefaultChatListener.java index 8c3ae565..1e71af57 100644 --- a/common/src/main/java/com/discordsrv/common/listener/DefaultChatListener.java +++ b/common/src/main/java/com/discordsrv/common/listener/DefaultChatListener.java @@ -28,8 +28,9 @@ import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.config.main.channels.BaseChannelConfig; import com.discordsrv.common.config.main.channels.ChannelConfig; +import com.discordsrv.common.config.main.channels.minecraftodiscord.MinecraftToDiscordChatConfig; import com.discordsrv.common.function.OrDefault; -import com.discordsrv.common.player.util.PlayerUtil; +import dev.vankka.mcdiscordreserializer.discord.DiscordSerializer; import net.kyori.adventure.text.Component; import java.util.Collections; @@ -49,23 +50,23 @@ public class DefaultChatListener extends AbstractListener { GameChannel gameChannel = event.getGameChannel(); Component message = ComponentUtil.fromAPI(event.message()); - Component displayName = PlayerUtil.displayName(event.getPlayer()); OrDefault channelConfig = discordSRV.channelConfig().orDefault(gameChannel); + OrDefault chatConfig = channelConfig.map(cfg -> cfg.minecraftToDiscord); -// Component discordMessage = EnhancedLegacyText.get().buildComponent(channelConfig.map(cfg -> cfg.minecraftToDiscord).get(cfg -> cfg.messageFormat)) -// .replace("%message%", message) -// .replace("%player_display_name%", displayName) -// .build(); -// -// String username = new Placeholders(channelConfig.map(cfg -> cfg.minecraftToDiscord).get(cfg -> cfg.usernameFormat)) -// .replace("%player_display_name%", () -> PlainTextComponentSerializer.plainText().serialize(displayName)) -// .get(); + SendableDiscordMessage.Builder builder = chatConfig.get(cfg -> cfg.messageFormat); + if (builder == null) { + return; + } + + SendableDiscordMessage discordMessage = discordSRV.discordAPI().format(builder) + .addContext(event.getPlayer()) + .addReplacement("%message%", DiscordSerializer.INSTANCE.serialize(message)) + .build(); discordSRV.eventBus().publish( new ChatMessageSendEvent( - null, - null, + discordMessage, gameChannel ) ); @@ -86,13 +87,7 @@ public class DefaultChatListener extends AbstractListener { for (String channelId : channelIds) { discordSRV.discordAPI().getTextChannelById(channelId).ifPresent(textChannel -> - textChannel.sendMessage( - SendableDiscordMessage.builder() - .setWebhookUsername(event.getDiscordUsername()) - .setContent(event.getDiscordMessage()) - .build() - ) - ); + textChannel.sendMessage(event.getDiscordMessage())); } } } diff --git a/common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java b/common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java index 73e77e35..30ced20d 100644 --- a/common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java +++ b/common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java @@ -21,6 +21,7 @@ package com.discordsrv.common.placeholder; import com.discordsrv.api.event.events.placeholder.PlaceholderLookupEvent; import com.discordsrv.api.placeholder.Placeholder; import com.discordsrv.api.placeholder.PlaceholderLookupResult; +import com.discordsrv.api.placeholder.PlaceholderResultConverter; import com.discordsrv.api.placeholder.PlaceholderService; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.placeholder.provider.AnnotationPlaceholderProvider; @@ -29,11 +30,13 @@ import com.github.benmanes.caffeine.cache.CacheLoader; import com.github.benmanes.caffeine.cache.LoadingCache; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -42,6 +45,7 @@ public class PlaceholderServiceImpl implements PlaceholderService { private final DiscordSRV discordSRV; private final LoadingCache, Set> classProviders; + private final Set converters = new CopyOnWriteArraySet<>(); public PlaceholderServiceImpl(DiscordSRV discordSRV) { this.discordSRV = discordSRV; @@ -58,13 +62,20 @@ public class PlaceholderServiceImpl implements PlaceholderService { } @Override - public PlaceholderLookupResult lookupPlaceholder(String placeholder, Object... context) { + public PlaceholderLookupResult lookupPlaceholder(@NotNull String placeholder, Object... context) { return lookupPlaceholder(placeholder, getArrayAsSet(context)); } @Override - public PlaceholderLookupResult lookupPlaceholder(String placeholder, Set context) { + public PlaceholderLookupResult lookupPlaceholder(@NotNull String placeholder, @NotNull Set context) { for (Object o : context) { + if (o instanceof PlaceholderProvider) { + PlaceholderLookupResult result = ((PlaceholderProvider) o).lookup(placeholder, context); + if (result.getType() != PlaceholderLookupResult.Type.UNKNOWN_PLACEHOLDER) { + return result; + } + } + Set providers = classProviders.get(o.getClass()); if (providers == null) { continue; @@ -88,35 +99,108 @@ public class PlaceholderServiceImpl implements PlaceholderService { } @Override - public String replacePlaceholders(String input, Object... context) { + public String replacePlaceholders(@NotNull String input, Object... context) { return replacePlaceholders(input, getArrayAsSet(context)); } @Override - public String replacePlaceholders(String input, Set context) { - return processReplacement(PATTERN, input, context); + public void addResultConverter(@NotNull PlaceholderResultConverter resultConverter) { + converters.add(resultConverter); } - private String processReplacement(Pattern pattern, String input, Set context) { + @Override + public void removeResultConverter(@NotNull PlaceholderResultConverter resultConverter) { + converters.remove(resultConverter); + } + + @Override + public String replacePlaceholders(@NotNull String input, @NotNull Set context) { + return getReplacement(PATTERN, input, context); + } + + private String getReplacement(Pattern pattern, String input, Set context) { Matcher matcher = pattern.matcher(input); String output = input; while (matcher.find()) { String placeholder = matcher.group(2); - String originalPlaceholder = placeholder; - - // Recursive - placeholder = processReplacement(RECURSIVE_PATTERN, placeholder, context); - - PlaceholderLookupResult result = lookupPlaceholder(placeholder, context); - output = updateBasedOnResult(result, input, originalPlaceholder, matcher); + PlaceholderLookupResult result = resolve(placeholder, context); + output = updateContent(result, placeholder, matcher, output); } return output; } - private String updateBasedOnResult( - PlaceholderLookupResult result, String input, String originalPlaceholder, Matcher matcher) { - String output = input; + @Override + public Object getResult(@NotNull Matcher matcher, @NotNull Set context) { + if (matcher.groupCount() < 3) { + throw new IllegalStateException("Matcher must have atleast 3 groups"); + } + String placeholder = matcher.group(2); + PlaceholderLookupResult result = resolve(matcher, context); + return getResultRepresentation(result, placeholder, matcher); + } + + @Override + public String getResultAsString(@NotNull Matcher matcher, @NotNull Set context) { + Object result = getResult(matcher, context); + return getResultAsString(result); + } + + private String getResultAsString(Object result) { + if (result == null) { + return "null"; + } else if (result instanceof String) { + return (String) result; + } + + String output = null; + for (PlaceholderResultConverter converter : converters) { + output = converter.convertPlaceholderResult(result); + if (output != null) { + break; + } + } + if (output == null) { + output = String.valueOf(result); + } + return output; + } + + public PlaceholderLookupResult resolve(Matcher matcher, Set context) { + return resolve(matcher.group(2), context); + } + + private PlaceholderLookupResult resolve(String placeholder, Set context) { + // Recursive + placeholder = getReplacement(RECURSIVE_PATTERN, placeholder, context); + + return lookupPlaceholder(placeholder, context); + } + + private String updateContent( + PlaceholderLookupResult result, String placeholder, Matcher matcher, String input) { + Object representation = getResultRepresentation(result, placeholder, matcher); + + String output = getResultAsString(representation); + for (PlaceholderResultConverter converter : converters) { + output = converter.convertPlaceholderResult(representation); + if (output != null) { + break; + } + } + if (output == null) { + output = String.valueOf(representation); + } + + return Pattern.compile( + matcher.group(1) + placeholder + matcher.group(3), + Pattern.LITERAL + ) + .matcher(input) + .replaceFirst(output); + } + + private Object getResultRepresentation(PlaceholderLookupResult result, String placeholder, Matcher matcher) { while (result != null) { PlaceholderLookupResult.Type type = result.getType(); if (type == PlaceholderLookupResult.Type.UNKNOWN_PLACEHOLDER) { @@ -124,7 +208,7 @@ public class PlaceholderServiceImpl implements PlaceholderService { } boolean newLookup = false; - String replacement = null; + Object replacement = null; switch (type) { case SUCCESS: replacement = result.getValue(); @@ -136,27 +220,18 @@ public class PlaceholderServiceImpl implements PlaceholderService { replacement = "Error"; break; case NEW_LOOKUP: - // prevent infinite recursion - if (result.getValue().equals(originalPlaceholder)) { - break; - } - result = lookupPlaceholder(result.getValue(), result.getExtras()); + result = lookupPlaceholder((String) result.getValue(), result.getExtras()); newLookup = true; break; } if (replacement != null) { - output = Pattern.compile( - matcher.group(1) - + originalPlaceholder - + matcher.group(3), - Pattern.LITERAL - ).matcher(output).replaceFirst(replacement); + return replacement; } if (!newLookup) { break; } } - return output; + return matcher.group(1) + placeholder + matcher.group(3); } private static class ClassProviderLoader implements CacheLoader, Set> { 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 new file mode 100644 index 00000000..bab50962 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/placeholder/converter/ComponentResultConverter.java @@ -0,0 +1,56 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2021 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 . + */ + +package com.discordsrv.common.placeholder.converter; + +import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.placeholder.PlaceholderResultConverter; +import com.discordsrv.common.component.util.ComponentUtil; +import dev.vankka.mcdiscordreserializer.discord.DiscordSerializer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.jetbrains.annotations.NotNull; + +public class ComponentResultConverter implements PlaceholderResultConverter { + + private static final ThreadLocal PLAIN_CONTEXT = new ThreadLocal<>(); + + public static void plain(Runnable runnable) { + PLAIN_CONTEXT.set(true); + runnable.run(); + PLAIN_CONTEXT.set(false); + } + + @Override + public String convertPlaceholderResult(@NotNull Object result) { + if (result instanceof MinecraftComponent) { + result = ComponentUtil.fromAPI((MinecraftComponent) result); + } + if (result instanceof Component) { + Component component = (Component) result; + if (PLAIN_CONTEXT.get()) { + return PlainTextComponentSerializer.plainText() + .serialize(component); + } else { + return DiscordSerializer.INSTANCE + .serialize(component); + } + } + return null; + } +} diff --git a/common/src/main/java/com/discordsrv/common/player/IPlayer.java b/common/src/main/java/com/discordsrv/common/player/IPlayer.java index b6fd5dbb..6391dea5 100644 --- a/common/src/main/java/com/discordsrv/common/player/IPlayer.java +++ b/common/src/main/java/com/discordsrv/common/player/IPlayer.java @@ -22,7 +22,6 @@ import com.discordsrv.api.placeholder.Placeholder; import com.discordsrv.api.player.DiscordSRVPlayer; import com.discordsrv.common.command.game.sender.ICommandSender; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -40,12 +39,8 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende return identity().uuid(); } + @NotNull + @Placeholder("player_display_name") Component displayName(); - @ApiStatus.NonExtendable - @Placeholder("player_display_name") - default String plainDisplayName() { - return PlainTextComponentSerializer.plainText() - .serialize(displayName()); - } } diff --git a/common/src/main/java/com/discordsrv/common/player/util/PlayerUtil.java b/common/src/main/java/com/discordsrv/common/player/util/PlayerUtil.java deleted file mode 100644 index 2a330cd1..00000000 --- a/common/src/main/java/com/discordsrv/common/player/util/PlayerUtil.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This file is part of DiscordSRV, licensed under the GPLv3 License - * Copyright (c) 2016-2021 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 . - */ - -package com.discordsrv.common.player.util; - -import com.discordsrv.api.player.DiscordSRVPlayer; -import com.discordsrv.common.player.IPlayer; -import net.kyori.adventure.text.Component; - -public final class PlayerUtil { - - private PlayerUtil() {} - - public static Component displayName(DiscordSRVPlayer player) { - if (player instanceof IPlayer) { - return ((IPlayer) player).displayName(); - } else { - return Component.text(player.getUsername()); - } - } -} 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 c34987d9..52dd2872 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 @@ -18,9 +18,9 @@ package com.discordsrv.common.string.util; -import javax.annotation.CheckReturnValue; import java.util.HashMap; import java.util.Map; +import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -28,54 +28,55 @@ import java.util.regex.Pattern; public class Placeholders { private final String inputText; - private final Map> replacements = new HashMap<>(); + private final Map> replacements = new HashMap<>(); public Placeholders(String inputText) { this.inputText = inputText; } - @CheckReturnValue - public Placeholders replace(String replace, String replacement) { - return replace(replace, () -> replacement); + public Placeholders addAll(Map> replacements) { + replacements.forEach(this.replacements::put); + return this; } - @CheckReturnValue - public Placeholders replaceAll(String regex, String replacement) { - return replaceAll(regex, () -> replacement); + public Placeholders replace(String target, Object replacement) { + return replace(target, matcher -> replacement); } - @CheckReturnValue - public Placeholders replaceAll(Pattern pattern, String replacement) { - return replaceAll(pattern, () -> replacement); + public Placeholders replaceAll(Pattern pattern, Object replacement) { + return replaceAll(pattern, matcher -> replacement); } - @CheckReturnValue - public Placeholders replace(String replace, Supplier replacement) { - return replaceAll(Pattern.compile(replace, Pattern.LITERAL), replacement); + public Placeholders replace(String target, Supplier replacement) { + return replaceAll(Pattern.compile(target, Pattern.LITERAL), matcher -> replacement); } - @CheckReturnValue - public Placeholders replaceAll(String regex, Supplier replacement) { - return replaceAll(Pattern.compile(regex), replacement); + public Placeholders replaceAll(Pattern pattern, Supplier replacement) { + return replaceAll(pattern, matcher -> replacement); } - @CheckReturnValue - public Placeholders replaceAll(Pattern pattern, Supplier replacement) { - replacements.put(pattern, replacement); + public Placeholders replace(String target, Function replacement) { + return replaceAll(Pattern.compile(target, Pattern.LITERAL), replacement); + } + + public Placeholders replaceAll(Pattern pattern, Function replacement) { + this.replacements.put(pattern, replacement); return this; } public String get() { String input = inputText; - for (Map.Entry> entry : replacements.entrySet()) { + for (Map.Entry> entry : replacements.entrySet()) { Pattern pattern = entry.getKey(); Matcher matcher = pattern.matcher(input); if (!matcher.find()) { continue; } - Supplier replacement = entry.getValue(); - input = matcher.replaceAll(replacement.get()); + Function replacement = entry.getValue(); + Object value = replacement.apply(matcher); + + input = matcher.replaceAll(String.valueOf(value)); } return input; } diff --git a/i18n/src/main/java/com/discordsrv/config/DiscordSRVTranslation.java b/i18n/src/main/java/com/discordsrv/config/DiscordSRVTranslation.java index 6d281a07..46a250db 100644 --- a/i18n/src/main/java/com/discordsrv/config/DiscordSRVTranslation.java +++ b/i18n/src/main/java/com/discordsrv/config/DiscordSRVTranslation.java @@ -22,7 +22,7 @@ import com.discordsrv.bukkit.config.connection.BukkitConnectionConfig; import com.discordsrv.bukkit.config.main.BukkitConfig; import com.discordsrv.common.config.Config; import com.discordsrv.common.config.annotation.Untranslated; -import com.discordsrv.common.config.main.channels.ChannelConfigHolder; +import com.discordsrv.common.config.main.channels.BaseChannelConfig; import org.spongepowered.configurate.CommentedConfigurationNode; import org.spongepowered.configurate.ConfigurateException; import org.spongepowered.configurate.ConfigurationNode; @@ -75,9 +75,9 @@ public final class DiscordSRVTranslation { .build(); - ChannelConfigHolder.Serializer channelSerializer = new ChannelConfigHolder.Serializer(objectMapper); + BaseChannelConfig.Serializer channelSerializer = new BaseChannelConfig.Serializer(objectMapper); CommentedConfigurationNode node = CommentedConfigurationNode.root(ConfigurationOptions.defaults() - .serializers(builder -> builder.register(ChannelConfigHolder.class, channelSerializer))); + .serializers(builder -> builder.register(BaseChannelConfig.class, channelSerializer))); for (Config config : CONFIG_INSTANCES) { ConfigurationNode section = node.node(config.getFileName()); ConfigurationNode configSection = section.copy(); diff --git a/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java b/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java index 7ba4f383..41e5734d 100644 --- a/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java +++ b/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java @@ -22,6 +22,7 @@ import com.discordsrv.common.player.IPlayer; import com.discordsrv.sponge.SpongeDiscordSRV; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; import org.spongepowered.api.command.exception.CommandException; import org.spongepowered.api.entity.living.player.server.ServerPlayer; @@ -54,7 +55,7 @@ public class SpongePlayer extends SpongeOfflinePlayer implements IPlayer { } @Override - public Component displayName() { + public @NotNull Component displayName() { return player.displayName().get(); } } diff --git a/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java b/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java index 0c4de623..e2ae6ece 100644 --- a/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java +++ b/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java @@ -61,7 +61,7 @@ public class VelocityPlayer implements IPlayer { } @Override - public Component displayName() { + public @NotNull Component displayName() { // Use Adventure's Pointer, otherwise username return player.getOrDefaultFrom( Identity.DISPLAY_NAME,