From 7d14a92158c03b6b76caed0addbaf3cacc35d610 Mon Sep 17 00:00:00 2001 From: Vankka Date: Wed, 4 Aug 2021 22:58:50 +0300 Subject: [PATCH] Placeholder service, misc bug fixes & improvements --- .../com/discordsrv/api/DiscordSRVApi.java | 7 + .../channel/GameChannelLookupEvent.java | 13 +- .../placeholder/PlaceholderLookupEvent.java | 95 +++++++++ .../api/placeholder/Placeholder.java | 43 ++++ .../placeholder/PlaceholderLookupResult.java | 85 ++++++++ .../api/placeholder/PlaceholderService.java | 46 ++++ .../api/player/DiscordSRVPlayer.java | 3 + .../bukkit/player/BukkitOfflinePlayer.java | 4 +- .../discordsrv/common/AbstractDiscordSRV.java | 15 +- .../com/discordsrv/common/DiscordSRV.java | 13 +- .../common/channel/ChannelConfig.java | 34 +-- .../common/config/annotation/DefaultOnly.java | 2 +- .../config/annotation/Untranslated.java | 2 +- .../MinecraftToDiscordChatConfig.java | 10 +- .../config/manager/MainConfigManager.java | 7 +- .../manager/ConfigurateConfigManager.java | 4 +- .../config/serializer/ColorSerializer.java | 63 ++++++ .../DiscordMessageEmbedSerializer.java | 131 +++++++++++- .../SendableDiscordMessageSerializer.java | 47 +++- .../dependency/InitialDependencyLoader.java | 3 + .../common/discord/api/DiscordAPIImpl.java | 17 +- .../connection/jda/JDAConnectionManager.java | 39 ++++ .../common/listener/DefaultChatListener.java | 24 +-- .../placeholder/PlaceholderServiceImpl.java | 200 ++++++++++++++++++ .../AnnotationPlaceholderProvider.java | 88 ++++++++ .../provider/PlaceholderProvider.java | 34 +++ .../provider/util/PlaceholderMethodUtil.java | 60 ++++++ .../common/player/IOfflinePlayer.java | 3 + .../com/discordsrv/common/player/IPlayer.java | 10 +- 29 files changed, 1026 insertions(+), 76 deletions(-) create mode 100644 api/src/main/java/com/discordsrv/api/event/events/placeholder/PlaceholderLookupEvent.java create mode 100644 api/src/main/java/com/discordsrv/api/placeholder/Placeholder.java create mode 100644 api/src/main/java/com/discordsrv/api/placeholder/PlaceholderLookupResult.java create mode 100644 api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java create mode 100644 common/src/main/java/com/discordsrv/common/config/serializer/ColorSerializer.java create mode 100644 common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java create mode 100644 common/src/main/java/com/discordsrv/common/placeholder/provider/AnnotationPlaceholderProvider.java create mode 100644 common/src/main/java/com/discordsrv/common/placeholder/provider/PlaceholderProvider.java create mode 100644 common/src/main/java/com/discordsrv/common/placeholder/provider/util/PlaceholderMethodUtil.java diff --git a/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java b/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java index 60321ba2..77ac61b7 100644 --- a/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java +++ b/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java @@ -27,6 +27,7 @@ import com.discordsrv.api.component.MinecraftComponentFactory; import com.discordsrv.api.discord.api.DiscordAPI; import com.discordsrv.api.discord.connection.DiscordConnectionDetails; import com.discordsrv.api.event.bus.EventBus; +import com.discordsrv.api.placeholder.PlaceholderService; import com.discordsrv.api.player.DiscordSRVPlayer; import com.discordsrv.api.player.IPlayerProvider; import net.dv8tion.jda.api.JDA; @@ -57,6 +58,12 @@ public interface DiscordSRVApi { @NotNull EventBus eventBus(); + /** + * DiscordSRV's own placeholder service. + * @return the {@link PlaceholderService} instance. + */ + PlaceholderService placeholderService(); + /** * A provider for {@link com.discordsrv.api.component.MinecraftComponent}s. * @return the {@link com.discordsrv.api.component.MinecraftComponentFactory} instance. diff --git a/api/src/main/java/com/discordsrv/api/event/events/channel/GameChannelLookupEvent.java b/api/src/main/java/com/discordsrv/api/event/events/channel/GameChannelLookupEvent.java index 37f2ed6e..5440a848 100644 --- a/api/src/main/java/com/discordsrv/api/event/events/channel/GameChannelLookupEvent.java +++ b/api/src/main/java/com/discordsrv/api/event/events/channel/GameChannelLookupEvent.java @@ -28,10 +28,10 @@ import com.discordsrv.api.event.events.Processable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Objects; - /** - * + * This event is used to lookup {@link GameChannel}s by their name (and optionally plugin name). + * This is also used to determine which plugin's channel should take priority when multiple plugins + * define channels with the same name ({@link com.discordsrv.api.event.bus.EventPriority}). */ public class GameChannelLookupEvent implements Processable { @@ -76,9 +76,12 @@ public class GameChannelLookupEvent implements Processable { /** * Provides a {@link GameChannel} for the provided channel name ({@link #getChannelName()}). * @param channel the channel + * @throws IllegalStateException if the event is already processed */ - public void process(GameChannel channel) { - Objects.requireNonNull(channel, "channel"); + public void process(@NotNull GameChannel channel) { + if (processed) { + throw new IllegalStateException("Already processed"); + } if (pluginName != null && !pluginName.equalsIgnoreCase(channel.getOwnerName())) { // Not the plugin we're looking for, ignore return; diff --git a/api/src/main/java/com/discordsrv/api/event/events/placeholder/PlaceholderLookupEvent.java b/api/src/main/java/com/discordsrv/api/event/events/placeholder/PlaceholderLookupEvent.java new file mode 100644 index 00000000..4d21ca3e --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/event/events/placeholder/PlaceholderLookupEvent.java @@ -0,0 +1,95 @@ +/* + * 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.event.events.placeholder; + +import com.discordsrv.api.event.events.Event; +import com.discordsrv.api.event.events.Processable; +import com.discordsrv.api.placeholder.PlaceholderLookupResult; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +public class PlaceholderLookupEvent implements Event, Processable { + + private final String placeholder; + private final Set context; + + private boolean processed; + private PlaceholderLookupResult result; + + public PlaceholderLookupEvent(String placeholder, Set context) { + this.placeholder = placeholder; + this.context = context; + } + + public String getPlaceholder() { + return placeholder; + } + + public Set getContext() { + return context; + } + + @Override + public boolean isProcessed() { + return processed; + } + + /** + * Returns the {@link PlaceholderLookupResult} from a {@link #process(PlaceholderLookupResult)} matching required criteria. + * @return the placeholder lookup result provided by a listener + * @throws IllegalStateException if {@link #isProcessed()} doesn't return true + */ + @NotNull + public PlaceholderLookupResult getResultFromProcessing() { + if (!processed) { + throw new IllegalStateException("This event has not been successfully processed yet, no result is available"); + } + return result; + } + + /** + * Provides a {@link PlaceholderLookupResult} for the provided {@link #getPlaceholder()} and {@link #getContext()}. + * @param result the result + * @throws IllegalStateException if the event is already processed + */ + public void process(@NotNull PlaceholderLookupResult result) { + if (processed) { + throw new IllegalStateException("Already processed"); + } + if (result.getType() == PlaceholderLookupResult.Type.UNKNOWN_PLACEHOLDER) { + // Ignore unknown + return; + } + + this.result = result; + this.processed = true; + } + + @Override + @Deprecated + public void markAsProcessed() { + throw new RuntimeException("Please use process(PlaceholderLookupResult) instead"); + } +} diff --git a/api/src/main/java/com/discordsrv/api/placeholder/Placeholder.java b/api/src/main/java/com/discordsrv/api/placeholder/Placeholder.java new file mode 100644 index 00000000..0c0900ce --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/placeholder/Placeholder.java @@ -0,0 +1,43 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.placeholder; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a Placeholder for DiscordSRV's {@link PlaceholderService}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface Placeholder { + + /** + * The name of the Placeholder. + * @return the placeholder's name, may contain any character besides {@code %}. + */ + String value(); +} diff --git a/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderLookupResult.java b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderLookupResult.java new file mode 100644 index 00000000..c31cf5a7 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderLookupResult.java @@ -0,0 +1,85 @@ +/* + * 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 java.util.Set; + +public class PlaceholderLookupResult { + + public static final PlaceholderLookupResult LOOKUP_FAILED = new PlaceholderLookupResult(Type.LOOKUP_FAILED); + public static final PlaceholderLookupResult DATA_NOT_AVAILABLE = new PlaceholderLookupResult(Type.DATA_NOT_AVAILABLE); + public static final PlaceholderLookupResult UNKNOWN_PLACEHOLDER = new PlaceholderLookupResult(Type.UNKNOWN_PLACEHOLDER); + + public static PlaceholderLookupResult success(Object result) { + return new PlaceholderLookupResult(String.valueOf(result)); + } + + public static PlaceholderLookupResult newLookup(String placeholder, Set extras) { + return new PlaceholderLookupResult(placeholder, extras); + } + + private final Type type; + private final String value; + private final Set extras; + + protected PlaceholderLookupResult(Type type) { + this.type = type; + this.value = null; + this.extras = null; + } + + protected PlaceholderLookupResult(String value) { + this.type = Type.SUCCESS; + this.value = value; + this.extras = null; + } + + protected PlaceholderLookupResult(String placeholder, Set extras) { + this.type = Type.NEW_LOOKUP; + this.value = placeholder; + this.extras = extras; + } + + public Type getType() { + return type; + } + + public String getValue() { + return value; + } + + public Set getExtras() { + return extras; + } + + public enum Type { + + SUCCESS, + NEW_LOOKUP, + + LOOKUP_FAILED, + DATA_NOT_AVAILABLE, + UNKNOWN_PLACEHOLDER + } +} diff --git a/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java new file mode 100644 index 00000000..5769e2e8 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java @@ -0,0 +1,46 @@ +/* + * 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 java.util.Set; +import java.util.regex.Pattern; + +public interface PlaceholderService { + + /** + * The primary pattern used by DiscordSRV to find placeholders. + */ + Pattern PATTERN = Pattern.compile("(%)([^%]+)(%)"); + + /** + * The pattern DiscordSRV uses to find recursive placeholders. + */ + Pattern RECURSIVE_PATTERN = Pattern.compile("(\\{)(.+)(})"); + + PlaceholderLookupResult lookupPlaceholder(String placeholder, Set context); + PlaceholderLookupResult lookupPlaceholder(String placeholder, Object... context); + + String replacePlaceholders(String placeholder, Set context); + String replacePlaceholders(String placeholder, Object... context); +} diff --git a/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java b/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java index ccf1388e..ae5e7f19 100644 --- a/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java +++ b/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java @@ -23,6 +23,7 @@ package com.discordsrv.api.player; +import com.discordsrv.api.placeholder.Placeholder; import org.jetbrains.annotations.NotNull; import java.util.UUID; @@ -36,6 +37,7 @@ public interface DiscordSRVPlayer { * The username of the player. * @return the player's username */ + @Placeholder("player_name") @NotNull String getUsername(); @@ -43,6 +45,7 @@ public interface DiscordSRVPlayer { * The {@link UUID} of the player. * @return the player's unique id */ + @Placeholder("player_uuid") @NotNull UUID uuid(); diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java index 7e6bd782..c6cabcbf 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java @@ -23,7 +23,6 @@ import com.discordsrv.common.player.IOfflinePlayer; import net.kyori.adventure.identity.Identity; import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; public class BukkitOfflinePlayer implements IOfflinePlayer { @@ -37,8 +36,9 @@ public class BukkitOfflinePlayer implements IOfflinePlayer { this.identity = Identity.identity(offlinePlayer.getUniqueId()); } + @SuppressWarnings("NullabilityProblems") @Override - public @Nullable String getUsername() { + public String getUsername() { return offlinePlayer.getName(); } diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index e66b4a4c..527821ca 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -21,6 +21,7 @@ package com.discordsrv.common; import com.discordsrv.api.discord.connection.DiscordConnectionDetails; import com.discordsrv.api.event.bus.EventBus; import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; +import com.discordsrv.api.placeholder.PlaceholderService; import com.discordsrv.common.api.util.ApiInstanceUtil; import com.discordsrv.common.channel.ChannelConfig; import com.discordsrv.common.channel.DefaultGlobalChannel; @@ -39,6 +40,7 @@ import com.discordsrv.common.listener.DefaultChannelLookupListener; 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 net.dv8tion.jda.api.JDA; import org.jetbrains.annotations.NotNull; @@ -54,8 +56,9 @@ public abstract class AbstractDiscordSRV playerProvider(); + ComponentFactory componentFactory(); @Override @NotNull - ComponentFactory componentFactory(); + AbstractPlayerProvider playerProvider(); @Override @NotNull @@ -74,6 +76,13 @@ public interface DiscordSRV extends DiscordSRVApi { Locale locale(); void setStatus(Status status); + @SuppressWarnings("unchecked") + @ApiStatus.NonExtendable + default Caffeine caffeineBuilder() { + return (Caffeine) Caffeine.newBuilder() + .executor(scheduler().forkExecutor()); + } + // Lifecycle CompletableFuture invokeEnable(); CompletableFuture invokeDisable(); diff --git a/common/src/main/java/com/discordsrv/common/channel/ChannelConfig.java b/common/src/main/java/com/discordsrv/common/channel/ChannelConfig.java index fb7e47e1..d68eac8d 100644 --- a/common/src/main/java/com/discordsrv/common/channel/ChannelConfig.java +++ b/common/src/main/java/com/discordsrv/common/channel/ChannelConfig.java @@ -25,7 +25,6 @@ import com.discordsrv.common.config.main.channels.BaseChannelConfig; import com.discordsrv.common.config.main.channels.ChannelConfigHolder; import com.discordsrv.common.function.OrDefault; import com.github.benmanes.caffeine.cache.CacheLoader; -import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -36,23 +35,24 @@ import java.util.concurrent.TimeUnit; public class ChannelConfig { private final DiscordSRV discordSRV; - private final LoadingCache CHANNELS = Caffeine.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .build(new CacheLoader() { - @Override - public @Nullable GameChannel load(@NonNull String channelName) { - GameChannelLookupEvent event = new GameChannelLookupEvent(null, channelName); - discordSRV.eventBus().publish(event); - if (!event.isProcessed()) { - return null; - } - - return event.getChannelFromProcessing(); - } - }); + private final LoadingCache channels; public ChannelConfig(DiscordSRV discordSRV) { this.discordSRV = discordSRV; + this.channels = discordSRV.caffeineBuilder() + .expireAfterWrite(30, TimeUnit.SECONDS) + .build(new CacheLoader() { + @Override + public @Nullable GameChannel load(@NonNull String channelName) { + GameChannelLookupEvent event = new GameChannelLookupEvent(null, channelName); + discordSRV.eventBus().publish(event); + if (!event.isProcessed()) { + return null; + } + + return event.getChannelFromProcessing(); + } + }); } private Map channels() { @@ -84,7 +84,7 @@ public class ChannelConfig { return config.get(); } - GameChannel gameChannel = CHANNELS.get(channelName); + GameChannel gameChannel = channels.get(channelName); if (gameChannel != null && gameChannel.getOwnerName().equals(ownerName)) { config = channels().get(channelName); return config != null ? config.get() : null; @@ -92,7 +92,7 @@ public class ChannelConfig { return null; } - GameChannel gameChannel = CHANNELS.get(channelName); + GameChannel gameChannel = channels.get(channelName); return gameChannel != null ? get(gameChannel) : null; } } diff --git a/common/src/main/java/com/discordsrv/common/config/annotation/DefaultOnly.java b/common/src/main/java/com/discordsrv/common/config/annotation/DefaultOnly.java index 54b824c9..ea99b0d3 100644 --- a/common/src/main/java/com/discordsrv/common/config/annotation/DefaultOnly.java +++ b/common/src/main/java/com/discordsrv/common/config/annotation/DefaultOnly.java @@ -26,7 +26,7 @@ import java.lang.annotation.Target; /** * Prevents the annotated options from being (partially) merged into existing configs (only being added to new configs). */ -@Retention(value = RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface DefaultOnly { diff --git a/common/src/main/java/com/discordsrv/common/config/annotation/Untranslated.java b/common/src/main/java/com/discordsrv/common/config/annotation/Untranslated.java index a6a73dc7..0fc52586 100644 --- a/common/src/main/java/com/discordsrv/common/config/annotation/Untranslated.java +++ b/common/src/main/java/com/discordsrv/common/config/annotation/Untranslated.java @@ -26,7 +26,7 @@ import java.lang.annotation.Target; /** * Specifies that the given option will be partially or completely undocumented. */ -@Retention(value = RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Untranslated { 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 af069fbc..ee97401e 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 @@ -18,17 +18,17 @@ package com.discordsrv.common.config.main.channels.minecraftodiscord; +import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Setting; @ConfigSerializable public class MinecraftToDiscordChatConfig { - @Setting("UsernameFormat") - public String usernameFormat = "%player_display_name%"; - - @Setting("MessageFormat") - public String messageFormat = "%message%"; + @Setting("Format") + public SendableDiscordMessage.Builder messageFormat = SendableDiscordMessage.builder() + .setWebhookUsername("%player_display_name%") + .setContent("%player_message%");// TODO @Setting("UseWebhooks") public boolean useWebhooks = false; 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 b5d90b8c..84642a3c 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 @@ -25,12 +25,15 @@ 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,11 +49,13 @@ public abstract class MainConfigManager @Override public ConfigurationOptions defaultOptions() { - return YamlConfigLoaderProvider.super.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 112603c8..a24202ed 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 @@ -43,18 +43,18 @@ public abstract class ConfigurateConfigManager. + */ + +package com.discordsrv.common.config.serializer; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.awt.Color; +import java.lang.reflect.Type; + +public class ColorSerializer implements TypeSerializer { + + @Override + public Color deserialize(Type type, ConfigurationNode node) { + String hexColor = node.getString(); + int length; + if (hexColor != null && ((length = hexColor.length()) == 6 || (length == 7 && hexColor.startsWith("#")))) { + if (length == 7) { + hexColor = hexColor.substring(1); + } + + try { + int r = Integer.parseInt(hexColor.substring(0, 2), 16); + int g = Integer.parseInt(hexColor.substring(2, 4), 16); + int b = Integer.parseInt(hexColor.substring(4, 6), 16); + + return new Color(r, g, b); + } catch (NumberFormatException ignored) {} + } else { + int intColor = node.getInt(Integer.MIN_VALUE); + if (intColor != Integer.MIN_VALUE) { + return new Color(intColor); + } + } + return null; + } + + @Override + public void serialize(Type type, @Nullable Color obj, ConfigurationNode node) throws SerializationException { + if (obj == null) { + return; + } + node.set(String.format("#%02x%02x%02x", obj.getRed(), obj.getGreen(), obj.getBlue())); + } +} diff --git a/common/src/main/java/com/discordsrv/common/config/serializer/DiscordMessageEmbedSerializer.java b/common/src/main/java/com/discordsrv/common/config/serializer/DiscordMessageEmbedSerializer.java index d3ead0d6..25530f79 100644 --- a/common/src/main/java/com/discordsrv/common/config/serializer/DiscordMessageEmbedSerializer.java +++ b/common/src/main/java/com/discordsrv/common/config/serializer/DiscordMessageEmbedSerializer.java @@ -1,23 +1,146 @@ +/* + * 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.serializer; import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed; +import net.dv8tion.jda.api.entities.Role; import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.serialize.SerializationException; import org.spongepowered.configurate.serialize.TypeSerializer; +import java.awt.Color; import java.lang.reflect.Type; +import java.util.Collections; public class DiscordMessageEmbedSerializer implements TypeSerializer { @Override public DiscordMessageEmbed.Builder deserialize(Type type, ConfigurationNode node) throws SerializationException { - // TODO - return null; + if (!node.node("Enabled").getBoolean(node.node("Enable").getBoolean(true))) { + return null; + } + + DiscordMessageEmbed.Builder builder = DiscordMessageEmbed.builder(); + + Color color = node.node("Color").get(Color.class); + builder.setColor(color != null ? color.getRGB() : Role.DEFAULT_COLOR_RAW); + + ConfigurationNode author = node.node("Author"); + builder.setAuthor( + author.node("Name").getString(), + author.node("Url").getString(), + author.node("ImageUrl").getString()); + + ConfigurationNode title = node.node("Title"); + builder.setTitle( + title.node("Text").getString(), + title.node("Url").getString()); + + builder.setDescription(node.node("Description").getString()); + for (DiscordMessageEmbed.Field field : node.getList(DiscordMessageEmbed.Field.class, Collections.emptyList())) { + builder.addField(field); + } + + builder.setThumbnailUrl(node.node("ThumbnailUrl").getString()); + builder.setImageUrl(node.node("ImageUrl").getString()); + + // TODO: timestamp + + ConfigurationNode footer = node.node("Footer"); + builder.setFooter( + footer.node("Text").getString(), + footer.node("ImageUrl").getString(footer.node("IconUrl").getString())); + + return builder; } @Override - public void serialize(Type type, DiscordMessageEmbed.@Nullable Builder obj, ConfigurationNode node) throws SerializationException { - // TODO + public void serialize(Type type, DiscordMessageEmbed.@Nullable Builder obj, ConfigurationNode node) + throws SerializationException { + if (obj == null) { + node.set(null); + return; + } + + node.node("Color").set(new Color(obj.getColor())); + + ConfigurationNode author = node.node("Author"); + author.node("Name").set(obj.getAuthorName()); + author.node("Url").set(obj.getAuthorUrl()); + author.node("ImageUrl").set(obj.getAuthorImageUrl()); + + ConfigurationNode title = node.node("Title"); + title.node("Text").set(obj.getTitle()); + title.node("Url").set(obj.getTitleUrl()); + + node.node("Description").set(obj.getDescription()); + node.node("Fields").setList(DiscordMessageEmbed.Field.class, obj.getFields()); + + node.node("ThumbnailUrl").set(obj.getThumbnailUrl()); + node.node("ImageUrl").set(obj.getImageUrl()); + + ConfigurationNode footer = node.node("Footer"); + footer.node("Text").set(obj.getFooter()); + footer.node("ImageUrl").set(obj.getFooterImageUrl()); + } + + public static class FieldSerializer implements TypeSerializer { + + @Override + public DiscordMessageEmbed.Field deserialize(Type type, ConfigurationNode node) { + // v1 compat + String footerString = node.getString(); + if (footerString != null) { + if (footerString.contains(";")) { + String[] parts = footerString.split(";", 3); + if (parts.length < 2) { + return null; + } + + boolean inline = parts.length < 3 || Boolean.parseBoolean(parts[2]); + return new DiscordMessageEmbed.Field(parts[0], parts[1], inline); + } else { + boolean inline = Boolean.parseBoolean(footerString); + return new DiscordMessageEmbed.Field("\u200e", "\u200e", inline); + } + } + + return new DiscordMessageEmbed.Field( + node.node("Title").getString(), + node.node("Value").getString(), + node.node("Inline").getBoolean() + ); + } + + @Override + public void serialize(Type type, DiscordMessageEmbed.@Nullable Field obj, ConfigurationNode node) + throws SerializationException { + if (obj == null) { + node.set(null); + return; + } + + node.node("Title").set(obj.getTitle()); + node.node("Value").set(obj.getValue()); + node.node("Inline").set(obj.isInline()); + } + } } 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 edcf048a..72e6d436 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 @@ -1,3 +1,21 @@ +/* + * 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.serializer; import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed; @@ -9,13 +27,14 @@ import org.spongepowered.configurate.serialize.TypeSerializer; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Optional; public class SendableDiscordMessageSerializer implements TypeSerializer { @Override - public SendableDiscordMessage.Builder deserialize(Type type, ConfigurationNode node) throws SerializationException { + public SendableDiscordMessage.Builder deserialize(Type type, ConfigurationNode node) + throws SerializationException { String contentOnly = node.getString(); if (contentOnly != null) { return SendableDiscordMessage.builder() @@ -25,16 +44,21 @@ public class SendableDiscordMessageSerializer implements TypeSerializer embeds = node.node("Embeds").getList(DiscordMessageEmbed.Builder.class); - if (embeds != null) { - for (DiscordMessageEmbed.Builder embed : embeds) { - builder.addEmbed(embed.build()); - } + // v1 compat + DiscordMessageEmbed.Builder singleEmbed = node.node("Embed").get( + DiscordMessageEmbed.Builder.class); + List embedList = singleEmbed != null + ? Collections.singletonList(singleEmbed) : Collections.emptyList(); + + for (DiscordMessageEmbed.Builder embed : node.node("Embeds") + .getList(DiscordMessageEmbed.Builder.class, embedList)) { + builder.addEmbed(embed.build()); } builder.setContent(node.node("Content").getString()); @@ -42,7 +66,8 @@ public class SendableDiscordMessageSerializer implements TypeSerializer embedBuilders = new ArrayList<>(); diff --git a/common/src/main/java/com/discordsrv/common/dependency/InitialDependencyLoader.java b/common/src/main/java/com/discordsrv/common/dependency/InitialDependencyLoader.java index cb1bae2d..f2ec0172 100644 --- a/common/src/main/java/com/discordsrv/common/dependency/InitialDependencyLoader.java +++ b/common/src/main/java/com/discordsrv/common/dependency/InitialDependencyLoader.java @@ -31,6 +31,9 @@ import java.util.concurrent.ForkJoinPool; /** * TODO: revamp + * - run DiscordSRV#load() after DiscordSRV is initialized + * - catch exceptions, so they don't go missing + * - make the whenComplete stuff less janky */ public class InitialDependencyLoader { 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 69681842..b37924d5 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 @@ -54,17 +54,18 @@ public class DiscordAPIImpl implements DiscordAPI { private final DiscordSRV discordSRV; - private final AsyncLoadingCache cachedClients = Caffeine.newBuilder() - .removalListener((RemovalListener) (id, client, cause) -> { - if (client != null) { - client.close(); - } - }) - .expireAfter(new WebhookCacheExpiry()) - .buildAsync(new WebhookCacheLoader()); + private final AsyncLoadingCache cachedClients; public DiscordAPIImpl(DiscordSRV discordSRV) { this.discordSRV = discordSRV; + this.cachedClients = discordSRV.caffeineBuilder() + .removalListener((RemovalListener) (id, client, cause) -> { + if (client != null) { + client.close(); + } + }) + .expireAfter(new WebhookCacheExpiry()) + .buildAsync(new WebhookCacheLoader()); } public CompletableFuture queryWebhookClient(String channelId) { diff --git a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java index 9049073f..9fadd6cc 100644 --- a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java +++ b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java @@ -22,8 +22,17 @@ import com.discordsrv.api.discord.connection.DiscordConnectionDetails; import com.discordsrv.api.event.bus.EventPriority; import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; +import com.discordsrv.api.event.events.placeholder.PlaceholderLookupEvent; +import com.discordsrv.api.placeholder.PlaceholderLookupResult; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.connection.ConnectionConfig; +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.guild.DiscordGuildMemberImpl; +import com.discordsrv.common.discord.api.guild.DiscordRoleImpl; +import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl; +import com.discordsrv.common.discord.api.user.DiscordUserImpl; import com.discordsrv.common.discord.connection.DiscordConnectionManager; import com.discordsrv.common.scheduler.Scheduler; import com.discordsrv.common.scheduler.threadfactory.CountingThreadFactory; @@ -31,6 +40,7 @@ import com.neovisionaries.ws.client.WebSocketFactory; import com.neovisionaries.ws.client.WebSocketFrame; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.events.DisconnectEvent; import net.dv8tion.jda.api.events.ShutdownEvent; import net.dv8tion.jda.api.events.StatusChangeEvent; @@ -39,12 +49,14 @@ import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.utils.AllowedMentions; import net.dv8tion.jda.api.utils.MemberCachePolicy; +import net.dv8tion.jda.internal.entities.ReceivedMessage; import net.dv8tion.jda.internal.hooks.EventManagerProxy; import net.dv8tion.jda.internal.utils.IOUtil; import okhttp3.OkHttpClient; import javax.security.auth.login.LoginException; import java.util.Collections; +import java.util.HashSet; import java.util.Set; import java.util.concurrent.*; @@ -79,6 +91,33 @@ public class JDAConnectionManager implements DiscordConnectionManager { shutdown().join(); } + @Subscribe(priority = EventPriority.EARLIEST) + public void onPlaceholderLookup(PlaceholderLookupEvent event) { + Set newContext = new HashSet<>(); + for (Object o : event.getContext()) { + Object converted; + if (o instanceof PrivateChannel) { + converted = new DiscordDMChannelImpl(discordSRV, (PrivateChannel) o); + } else if (o instanceof TextChannel) { + converted = new DiscordTextChannelImpl(discordSRV, (TextChannel) o); + } else if (o instanceof Guild) { + converted = new DiscordGuildImpl(discordSRV, (Guild) o); + } else if (o instanceof Member) { + converted = new DiscordGuildMemberImpl((Member) o); + } else if (o instanceof Role) { + converted = new DiscordRoleImpl((Role) o); + } else if (o instanceof ReceivedMessage) { + converted = ReceivedDiscordMessageImpl.fromJDA(discordSRV, (Message) o); + } else if (o instanceof User) { + converted = new DiscordUserImpl((User) o); + } else { + converted = o; + } + newContext.add(converted); + } + event.process(PlaceholderLookupResult.newLookup(event.getPlaceholder(), newContext)); + } + @Subscribe public void onStatusChange(StatusChangeEvent event) { DiscordSRV.Status currentStatus = discordSRV.status(); 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 0f7cde7b..8c3ae565 100644 --- a/common/src/main/java/com/discordsrv/common/listener/DefaultChatListener.java +++ b/common/src/main/java/com/discordsrv/common/listener/DefaultChatListener.java @@ -30,11 +30,7 @@ import com.discordsrv.common.config.main.channels.BaseChannelConfig; import com.discordsrv.common.config.main.channels.ChannelConfig; import com.discordsrv.common.function.OrDefault; import com.discordsrv.common.player.util.PlayerUtil; -import com.discordsrv.common.string.util.Placeholders; -import dev.vankka.enhancedlegacytext.EnhancedLegacyText; -import dev.vankka.mcdiscordreserializer.discord.DiscordSerializer; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import java.util.Collections; import java.util.List; @@ -57,19 +53,19 @@ public class DefaultChatListener extends AbstractListener { OrDefault channelConfig = discordSRV.channelConfig().orDefault(gameChannel); - 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(); +// 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(); discordSRV.eventBus().publish( new ChatMessageSendEvent( - DiscordSerializer.INSTANCE.serialize(discordMessage), - username, + null, + null, gameChannel ) ); diff --git a/common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java b/common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java new file mode 100644 index 00000000..73e77e35 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java @@ -0,0 +1,200 @@ +/* + * 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; + +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.PlaceholderService; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.placeholder.provider.AnnotationPlaceholderProvider; +import com.discordsrv.common.placeholder.provider.PlaceholderProvider; +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 java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PlaceholderServiceImpl implements PlaceholderService { + + private final DiscordSRV discordSRV; + private final LoadingCache, Set> classProviders; + + public PlaceholderServiceImpl(DiscordSRV discordSRV) { + this.discordSRV = discordSRV; + this.classProviders = discordSRV.caffeineBuilder() + .expireAfterAccess(10, TimeUnit.MINUTES) + .expireAfterWrite(15, TimeUnit.MINUTES) + .build(new ClassProviderLoader()); + } + + private static Set getArrayAsSet(Object[] array) { + return array.length == 0 + ? Collections.emptySet() + : new HashSet<>(Arrays.asList(array)); + } + + @Override + public PlaceholderLookupResult lookupPlaceholder(String placeholder, Object... context) { + return lookupPlaceholder(placeholder, getArrayAsSet(context)); + } + + @Override + public PlaceholderLookupResult lookupPlaceholder(String placeholder, Set context) { + for (Object o : context) { + Set providers = classProviders.get(o.getClass()); + if (providers == null) { + continue; + } + + for (PlaceholderProvider provider : providers) { + PlaceholderLookupResult result = provider.lookup(placeholder, context); + if (result.getType() != PlaceholderLookupResult.Type.UNKNOWN_PLACEHOLDER) { + return result; + } + } + } + + // Only go through this if a placeholder couldn't be looked up from the context + PlaceholderLookupEvent lookupEvent = new PlaceholderLookupEvent(placeholder, context); + discordSRV.eventBus().publish(lookupEvent); + + return lookupEvent.isProcessed() + ? lookupEvent.getResultFromProcessing() + : PlaceholderLookupResult.UNKNOWN_PLACEHOLDER; + } + + @Override + public String replacePlaceholders(String input, Object... context) { + return replacePlaceholders(input, getArrayAsSet(context)); + } + + @Override + public String replacePlaceholders(String input, Set context) { + return processReplacement(PATTERN, input, context); + } + + private String processReplacement(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); + } + return output; + } + + private String updateBasedOnResult( + PlaceholderLookupResult result, String input, String originalPlaceholder, Matcher matcher) { + String output = input; + while (result != null) { + PlaceholderLookupResult.Type type = result.getType(); + if (type == PlaceholderLookupResult.Type.UNKNOWN_PLACEHOLDER) { + break; + } + + boolean newLookup = false; + String replacement = null; + switch (type) { + case SUCCESS: + replacement = result.getValue(); + break; + case DATA_NOT_AVAILABLE: + replacement = "Unavailable"; + break; + case LOOKUP_FAILED: + replacement = "Error"; + break; + case NEW_LOOKUP: + // prevent infinite recursion + if (result.getValue().equals(originalPlaceholder)) { + break; + } + result = lookupPlaceholder(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); + } + if (!newLookup) { + break; + } + } + return output; + } + + private static class ClassProviderLoader implements CacheLoader, Set> { + + @Override + public @Nullable Set load(@NonNull Class key) { + Set providers = new HashSet<>(); + + Class currentClass = key; + do { + List> classes = new ArrayList<>(Arrays.asList(currentClass.getInterfaces())); + classes.add(currentClass); + + for (Class clazz : classes) { + for (Method method : clazz.getMethods()) { + Placeholder annotation = method.getAnnotation(Placeholder.class); + if (annotation == null) { + continue; + } + + boolean isStatic = Modifier.isStatic(method.getModifiers()); + providers.add(new AnnotationPlaceholderProvider(annotation, isStatic ? null : clazz, method)); + } + for (Field field : clazz.getFields()) { + Placeholder annotation = field.getAnnotation(Placeholder.class); + if (annotation == null) { + continue; + } + + boolean isStatic = Modifier.isStatic(field.getModifiers()); + providers.add(new AnnotationPlaceholderProvider(annotation, isStatic ? null : clazz, field)); + } + } + + currentClass = currentClass.getSuperclass(); + } while (currentClass != null); + + return providers; + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/placeholder/provider/AnnotationPlaceholderProvider.java b/common/src/main/java/com/discordsrv/common/placeholder/provider/AnnotationPlaceholderProvider.java new file mode 100644 index 00000000..2cfa9077 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/placeholder/provider/AnnotationPlaceholderProvider.java @@ -0,0 +1,88 @@ +/* + * 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.provider; + +import com.discordsrv.api.placeholder.Placeholder; +import com.discordsrv.api.placeholder.PlaceholderLookupResult; +import com.discordsrv.common.placeholder.provider.util.PlaceholderMethodUtil; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Set; + +public class AnnotationPlaceholderProvider implements PlaceholderProvider { + + private final Placeholder annotation; + + private final Class type; + private final Method method; + private final Field field; + + public AnnotationPlaceholderProvider(Placeholder annotation, Class type, Method method) { + this.annotation = annotation; + this.type = type; + this.method = method; + this.field = null; + } + + public AnnotationPlaceholderProvider(Placeholder annotation, Class type, Field field) { + this.annotation = annotation; + this.type = type; + this.method = null; + this.field = field; + } + + @Override + public @NotNull PlaceholderLookupResult lookup(@NotNull String placeholder, @NotNull Set context) { + if (!annotation.value().equals(placeholder) || (type != null && context.isEmpty())) { + return PlaceholderLookupResult.UNKNOWN_PLACEHOLDER; + } + + Object instance = null; + if (type != null) { + for (Object o : context) { + if (type.isAssignableFrom(o.getClass())) { + instance = o; + } + } + if (instance == null) { + return PlaceholderLookupResult.UNKNOWN_PLACEHOLDER; + } + } + + if (field != null) { + try { + return PlaceholderLookupResult.success(field.get(instance)); + } catch (IllegalAccessException e) { + e.printStackTrace(); // TODO + return PlaceholderLookupResult.LOOKUP_FAILED; + } + } else { + try { + assert method != null; + return PlaceholderMethodUtil.lookup(method, instance, context); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); // TODO + return PlaceholderLookupResult.LOOKUP_FAILED; + } + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/placeholder/provider/PlaceholderProvider.java b/common/src/main/java/com/discordsrv/common/placeholder/provider/PlaceholderProvider.java new file mode 100644 index 00000000..0aac1541 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/placeholder/provider/PlaceholderProvider.java @@ -0,0 +1,34 @@ +/* + * 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.provider; + +import com.discordsrv.api.placeholder.PlaceholderLookupResult; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +/** + * A placeholder provider used internally by DiscordSRV for {@link com.discordsrv.api.placeholder.Placeholder}. + * API users should use the {@link com.discordsrv.api.event.events.placeholder.PlaceholderLookupEvent} instead. + */ +public interface PlaceholderProvider { + + @NotNull + PlaceholderLookupResult lookup(@NotNull String placeholder, @NotNull Set context); +} diff --git a/common/src/main/java/com/discordsrv/common/placeholder/provider/util/PlaceholderMethodUtil.java b/common/src/main/java/com/discordsrv/common/placeholder/provider/util/PlaceholderMethodUtil.java new file mode 100644 index 00000000..a0ede651 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/placeholder/provider/util/PlaceholderMethodUtil.java @@ -0,0 +1,60 @@ +/* + * 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.provider.util; + +import com.discordsrv.api.placeholder.PlaceholderLookupResult; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Set; + +public final class PlaceholderMethodUtil { + + private PlaceholderMethodUtil() {} + + public static PlaceholderLookupResult lookup(Method method, Object instance, Set context) + throws InvocationTargetException, IllegalAccessException { + Class[] parameterTypes = method.getParameterTypes(); + Object[] parameters = new Object[parameterTypes.length]; + + for (Object o : context) { + Class objectType = o.getClass(); + for (int i = 0; i < parameterTypes.length; i++) { + Class parameterType = parameterTypes[i]; + if (parameterType == null) { + continue; + } + + if (parameterType.isAssignableFrom(objectType)) { + parameters[i] = o; + parameterTypes[i] = null; + } + } + } + for (Class parameterType : parameterTypes) { + if (parameterType != null) { + return PlaceholderLookupResult.UNKNOWN_PLACEHOLDER; + } + } + + return PlaceholderLookupResult.success( + method.invoke(instance, parameters) + ); + } +} diff --git a/common/src/main/java/com/discordsrv/common/player/IOfflinePlayer.java b/common/src/main/java/com/discordsrv/common/player/IOfflinePlayer.java index 3f491d75..38398a40 100644 --- a/common/src/main/java/com/discordsrv/common/player/IOfflinePlayer.java +++ b/common/src/main/java/com/discordsrv/common/player/IOfflinePlayer.java @@ -18,6 +18,7 @@ package com.discordsrv.common.player; +import com.discordsrv.api.placeholder.Placeholder; import net.kyori.adventure.identity.Identified; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -27,10 +28,12 @@ import java.util.UUID; public interface IOfflinePlayer extends Identified { + @Placeholder("player_name") @Nullable String getUsername(); @ApiStatus.NonExtendable + @Placeholder("player_uuid") @NotNull default UUID uuid() { return identity().uuid(); 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 946e5bb8..b6fd5dbb 100644 --- a/common/src/main/java/com/discordsrv/common/player/IPlayer.java +++ b/common/src/main/java/com/discordsrv/common/player/IPlayer.java @@ -18,9 +18,11 @@ package com.discordsrv.common.player; +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; @@ -28,7 +30,6 @@ import java.util.UUID; public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSender { - @SuppressWarnings("NullableProblems") // IOfflinePlayer != IPlayer @Override @NotNull String getUsername(); @@ -40,4 +41,11 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende } Component displayName(); + + @ApiStatus.NonExtendable + @Placeholder("player_display_name") + default String plainDisplayName() { + return PlainTextComponentSerializer.plainText() + .serialize(displayName()); + } }