Placeholder service, misc bug fixes & improvements

This commit is contained in:
Vankka 2021-08-04 22:58:50 +03:00
parent 2c3031dc75
commit 7d14a92158
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
29 changed files with 1026 additions and 76 deletions

View File

@ -27,6 +27,7 @@ import com.discordsrv.api.component.MinecraftComponentFactory;
import com.discordsrv.api.discord.api.DiscordAPI; import com.discordsrv.api.discord.api.DiscordAPI;
import com.discordsrv.api.discord.connection.DiscordConnectionDetails; import com.discordsrv.api.discord.connection.DiscordConnectionDetails;
import com.discordsrv.api.event.bus.EventBus; import com.discordsrv.api.event.bus.EventBus;
import com.discordsrv.api.placeholder.PlaceholderService;
import com.discordsrv.api.player.DiscordSRVPlayer; import com.discordsrv.api.player.DiscordSRVPlayer;
import com.discordsrv.api.player.IPlayerProvider; import com.discordsrv.api.player.IPlayerProvider;
import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA;
@ -57,6 +58,12 @@ public interface DiscordSRVApi {
@NotNull @NotNull
EventBus eventBus(); EventBus eventBus();
/**
* DiscordSRV's own placeholder service.
* @return the {@link PlaceholderService} instance.
*/
PlaceholderService placeholderService();
/** /**
* A provider for {@link com.discordsrv.api.component.MinecraftComponent}s. * A provider for {@link com.discordsrv.api.component.MinecraftComponent}s.
* @return the {@link com.discordsrv.api.component.MinecraftComponentFactory} instance. * @return the {@link com.discordsrv.api.component.MinecraftComponentFactory} instance.

View File

@ -28,10 +28,10 @@ import com.discordsrv.api.event.events.Processable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; 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 { 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()}). * Provides a {@link GameChannel} for the provided channel name ({@link #getChannelName()}).
* @param channel the channel * @param channel the channel
* @throws IllegalStateException if the event is already processed
*/ */
public void process(GameChannel channel) { public void process(@NotNull GameChannel channel) {
Objects.requireNonNull(channel, "channel"); if (processed) {
throw new IllegalStateException("Already processed");
}
if (pluginName != null && !pluginName.equalsIgnoreCase(channel.getOwnerName())) { if (pluginName != null && !pluginName.equalsIgnoreCase(channel.getOwnerName())) {
// Not the plugin we're looking for, ignore // Not the plugin we're looking for, ignore
return; return;

View File

@ -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<Object> context;
private boolean processed;
private PlaceholderLookupResult result;
public PlaceholderLookupEvent(String placeholder, Set<Object> context) {
this.placeholder = placeholder;
this.context = context;
}
public String getPlaceholder() {
return placeholder;
}
public Set<Object> 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");
}
}

View File

@ -0,0 +1,43 @@
/*
* This file is part of the DiscordSRV API, licensed under the MIT License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.discordsrv.api.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();
}

View File

@ -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<Object> extras) {
return new PlaceholderLookupResult(placeholder, extras);
}
private final Type type;
private final String value;
private final Set<Object> 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<Object> extras) {
this.type = Type.NEW_LOOKUP;
this.value = placeholder;
this.extras = extras;
}
public Type getType() {
return type;
}
public String getValue() {
return value;
}
public Set<Object> getExtras() {
return extras;
}
public enum Type {
SUCCESS,
NEW_LOOKUP,
LOOKUP_FAILED,
DATA_NOT_AVAILABLE,
UNKNOWN_PLACEHOLDER
}
}

View File

@ -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<Object> context);
PlaceholderLookupResult lookupPlaceholder(String placeholder, Object... context);
String replacePlaceholders(String placeholder, Set<Object> context);
String replacePlaceholders(String placeholder, Object... context);
}

View File

@ -23,6 +23,7 @@
package com.discordsrv.api.player; package com.discordsrv.api.player;
import com.discordsrv.api.placeholder.Placeholder;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.UUID; import java.util.UUID;
@ -36,6 +37,7 @@ public interface DiscordSRVPlayer {
* The username of the player. * The username of the player.
* @return the player's username * @return the player's username
*/ */
@Placeholder("player_name")
@NotNull @NotNull
String getUsername(); String getUsername();
@ -43,6 +45,7 @@ public interface DiscordSRVPlayer {
* The {@link UUID} of the player. * The {@link UUID} of the player.
* @return the player's unique id * @return the player's unique id
*/ */
@Placeholder("player_uuid")
@NotNull @NotNull
UUID uuid(); UUID uuid();

View File

@ -23,7 +23,6 @@ import com.discordsrv.common.player.IOfflinePlayer;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class BukkitOfflinePlayer implements IOfflinePlayer { public class BukkitOfflinePlayer implements IOfflinePlayer {
@ -37,8 +36,9 @@ public class BukkitOfflinePlayer implements IOfflinePlayer {
this.identity = Identity.identity(offlinePlayer.getUniqueId()); this.identity = Identity.identity(offlinePlayer.getUniqueId());
} }
@SuppressWarnings("NullabilityProblems")
@Override @Override
public @Nullable String getUsername() { public String getUsername() {
return offlinePlayer.getName(); return offlinePlayer.getName();
} }

View File

@ -21,6 +21,7 @@ package com.discordsrv.common;
import com.discordsrv.api.discord.connection.DiscordConnectionDetails; import com.discordsrv.api.discord.connection.DiscordConnectionDetails;
import com.discordsrv.api.event.bus.EventBus; import com.discordsrv.api.event.bus.EventBus;
import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; 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.api.util.ApiInstanceUtil;
import com.discordsrv.common.channel.ChannelConfig; import com.discordsrv.common.channel.ChannelConfig;
import com.discordsrv.common.channel.DefaultGlobalChannel; 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.listener.DefaultChatListener;
import com.discordsrv.common.logging.DependencyLoggingFilter; import com.discordsrv.common.logging.DependencyLoggingFilter;
import com.discordsrv.common.logging.logger.backend.LoggingBackend; import com.discordsrv.common.logging.logger.backend.LoggingBackend;
import com.discordsrv.common.placeholder.PlaceholderServiceImpl;
import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -54,8 +56,9 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
// DiscordSRVApi // DiscordSRVApi
private final EventBus eventBus; private final EventBus eventBus;
private PlaceholderService placeholderService;
private final ComponentFactory componentFactory; private final ComponentFactory componentFactory;
private final DiscordAPIImpl discordAPI; private DiscordAPIImpl discordAPI;
private final DiscordConnectionDetails discordConnectionDetails; private final DiscordConnectionDetails discordConnectionDetails;
// DiscordSRV // DiscordSRV
@ -70,7 +73,6 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
ApiInstanceUtil.setInstance(this); ApiInstanceUtil.setInstance(this);
this.eventBus = new EventBusImpl(this); this.eventBus = new EventBusImpl(this);
this.componentFactory = new ComponentFactory(); this.componentFactory = new ComponentFactory();
this.discordAPI = new DiscordAPIImpl(this);
this.discordConnectionDetails = new DiscordConnectionDetailsImpl(this); this.discordConnectionDetails = new DiscordConnectionDetailsImpl(this);
} }
@ -86,6 +88,11 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
return eventBus; return eventBus;
} }
@Override
public @NotNull PlaceholderService placeholderService() {
return placeholderService;
}
@Override @Override
public @NotNull ComponentFactory componentFactory() { public @NotNull ComponentFactory componentFactory() {
return componentFactory; return componentFactory;
@ -183,6 +190,10 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
@OverridingMethodsMustInvokeSuper @OverridingMethodsMustInvokeSuper
protected void enable() throws Throwable { protected void enable() throws Throwable {
// API Stuff
this.placeholderService = new PlaceholderServiceImpl(this);
this.discordAPI = new DiscordAPIImpl(this);
// Config // Config
try { try {
connectionConfigManager().load(); connectionConfigManager().load();

View File

@ -32,6 +32,8 @@ import com.discordsrv.common.discord.connection.DiscordConnectionManager;
import com.discordsrv.common.logging.logger.Logger; import com.discordsrv.common.logging.logger.Logger;
import com.discordsrv.common.player.provider.AbstractPlayerProvider; import com.discordsrv.common.player.provider.AbstractPlayerProvider;
import com.discordsrv.common.scheduler.Scheduler; import com.discordsrv.common.scheduler.Scheduler;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.nio.file.Path; import java.nio.file.Path;
@ -50,11 +52,11 @@ public interface DiscordSRV extends DiscordSRVApi {
// DiscordSRVApi // DiscordSRVApi
@Override @Override
@NotNull @NotNull
AbstractPlayerProvider<?> playerProvider(); ComponentFactory componentFactory();
@Override @Override
@NotNull @NotNull
ComponentFactory componentFactory(); AbstractPlayerProvider<?> playerProvider();
@Override @Override
@NotNull @NotNull
@ -74,6 +76,13 @@ public interface DiscordSRV extends DiscordSRVApi {
Locale locale(); Locale locale();
void setStatus(Status status); void setStatus(Status status);
@SuppressWarnings("unchecked")
@ApiStatus.NonExtendable
default <K, V> Caffeine<K, V> caffeineBuilder() {
return (Caffeine<K, V>) Caffeine.newBuilder()
.executor(scheduler().forkExecutor());
}
// Lifecycle // Lifecycle
CompletableFuture<Void> invokeEnable(); CompletableFuture<Void> invokeEnable();
CompletableFuture<Void> invokeDisable(); CompletableFuture<Void> invokeDisable();

View File

@ -25,7 +25,6 @@ import com.discordsrv.common.config.main.channels.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.ChannelConfigHolder; import com.discordsrv.common.config.main.channels.ChannelConfigHolder;
import com.discordsrv.common.function.OrDefault; import com.discordsrv.common.function.OrDefault;
import com.github.benmanes.caffeine.cache.CacheLoader; import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache; import com.github.benmanes.caffeine.cache.LoadingCache;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
@ -36,23 +35,24 @@ import java.util.concurrent.TimeUnit;
public class ChannelConfig { public class ChannelConfig {
private final DiscordSRV discordSRV; private final DiscordSRV discordSRV;
private final LoadingCache<String, GameChannel> CHANNELS = Caffeine.newBuilder() private final LoadingCache<String, GameChannel> channels;
.expireAfterWrite(30, TimeUnit.SECONDS)
.build(new CacheLoader<String, GameChannel>() {
@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();
}
});
public ChannelConfig(DiscordSRV discordSRV) { public ChannelConfig(DiscordSRV discordSRV) {
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
this.channels = discordSRV.caffeineBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build(new CacheLoader<String, GameChannel>() {
@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<String, ChannelConfigHolder> channels() { private Map<String, ChannelConfigHolder> channels() {
@ -84,7 +84,7 @@ public class ChannelConfig {
return config.get(); return config.get();
} }
GameChannel gameChannel = CHANNELS.get(channelName); GameChannel gameChannel = channels.get(channelName);
if (gameChannel != null && gameChannel.getOwnerName().equals(ownerName)) { if (gameChannel != null && gameChannel.getOwnerName().equals(ownerName)) {
config = channels().get(channelName); config = channels().get(channelName);
return config != null ? config.get() : null; return config != null ? config.get() : null;
@ -92,7 +92,7 @@ public class ChannelConfig {
return null; return null;
} }
GameChannel gameChannel = CHANNELS.get(channelName); GameChannel gameChannel = channels.get(channelName);
return gameChannel != null ? get(gameChannel) : null; return gameChannel != null ? get(gameChannel) : null;
} }
} }

View File

@ -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). * 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) @Target(ElementType.FIELD)
public @interface DefaultOnly { public @interface DefaultOnly {

View File

@ -26,7 +26,7 @@ import java.lang.annotation.Target;
/** /**
* Specifies that the given option will be partially or completely undocumented. * Specifies that the given option will be partially or completely undocumented.
*/ */
@Retention(value = RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD) @Target(ElementType.FIELD)
public @interface Untranslated { public @interface Untranslated {

View File

@ -18,17 +18,17 @@
package com.discordsrv.common.config.main.channels.minecraftodiscord; 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.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Setting; import org.spongepowered.configurate.objectmapping.meta.Setting;
@ConfigSerializable @ConfigSerializable
public class MinecraftToDiscordChatConfig { public class MinecraftToDiscordChatConfig {
@Setting("UsernameFormat") @Setting("Format")
public String usernameFormat = "%player_display_name%"; public SendableDiscordMessage.Builder messageFormat = SendableDiscordMessage.builder()
.setWebhookUsername("%player_display_name%")
@Setting("MessageFormat") .setContent("%player_message%");// TODO
public String messageFormat = "%message%";
@Setting("UseWebhooks") @Setting("UseWebhooks")
public boolean useWebhooks = false; public boolean useWebhooks = false;

View File

@ -25,12 +25,15 @@ import com.discordsrv.common.config.main.MainConfig;
import com.discordsrv.common.config.main.channels.ChannelConfigHolder; import com.discordsrv.common.config.main.channels.ChannelConfigHolder;
import com.discordsrv.common.config.manager.loader.YamlConfigLoaderProvider; import com.discordsrv.common.config.manager.loader.YamlConfigLoaderProvider;
import com.discordsrv.common.config.manager.manager.TranslatedConfigManager; 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.DiscordMessageEmbedSerializer;
import com.discordsrv.common.config.serializer.SendableDiscordMessageSerializer; import com.discordsrv.common.config.serializer.SendableDiscordMessageSerializer;
import org.spongepowered.configurate.ConfigurationOptions; import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.objectmapping.ObjectMapper; import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader; import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import java.awt.Color;
public abstract class MainConfigManager<C extends MainConfig> public abstract class MainConfigManager<C extends MainConfig>
extends TranslatedConfigManager<C, YamlConfigurationLoader> extends TranslatedConfigManager<C, YamlConfigurationLoader>
implements YamlConfigLoaderProvider { implements YamlConfigLoaderProvider {
@ -46,11 +49,13 @@ public abstract class MainConfigManager<C extends MainConfig>
@Override @Override
public ConfigurationOptions defaultOptions() { public ConfigurationOptions defaultOptions() {
return YamlConfigLoaderProvider.super.defaultOptions() return super.defaultOptions()
.serializers(builder -> { .serializers(builder -> {
ObjectMapper.Factory objectMapper = defaultObjectMapper(); ObjectMapper.Factory objectMapper = defaultObjectMapper();
builder.register(Color.class, new ColorSerializer());
builder.register(ChannelConfigHolder.class, new ChannelConfigHolder.Serializer(objectMapper)); builder.register(ChannelConfigHolder.class, new ChannelConfigHolder.Serializer(objectMapper));
builder.register(DiscordMessageEmbed.Builder.class, new DiscordMessageEmbedSerializer()); builder.register(DiscordMessageEmbed.Builder.class, new DiscordMessageEmbedSerializer());
builder.register(DiscordMessageEmbed.Field.class, new DiscordMessageEmbedSerializer.FieldSerializer());
builder.register(SendableDiscordMessage.Builder.class, new SendableDiscordMessageSerializer()); builder.register(SendableDiscordMessage.Builder.class, new SendableDiscordMessageSerializer());
}); });
} }

View File

@ -43,18 +43,18 @@ public abstract class ConfigurateConfigManager<T, LT extends AbstractConfigurati
protected final DiscordSRV discordSRV; protected final DiscordSRV discordSRV;
private final Path filePath; private final Path filePath;
private final LT loader;
private final ObjectMapper.Factory configObjectMapper; private final ObjectMapper.Factory configObjectMapper;
private final ObjectMapper.Factory defaultObjectMapper; private final ObjectMapper.Factory defaultObjectMapper;
private final LT loader;
protected T configuration; protected T configuration;
public ConfigurateConfigManager(DiscordSRV discordSRV) { public ConfigurateConfigManager(DiscordSRV discordSRV) {
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
this.filePath = new File(discordSRV.dataDirectory().toFile(), fileName()).toPath(); this.filePath = new File(discordSRV.dataDirectory().toFile(), fileName()).toPath();
this.loader = createLoader(filePath, configNodeOptions());
this.configObjectMapper = configObjectMapperBuilder().build(); this.configObjectMapper = configObjectMapperBuilder().build();
this.defaultObjectMapper = defaultObjectMapperBuilder().build(); this.defaultObjectMapper = defaultObjectMapperBuilder().build();
this.loader = createLoader(filePath, configNodeOptions());
} }
public Path filePath() { public Path filePath() {

View File

@ -0,0 +1,63 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<Color> {
@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()));
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.serializer; package com.discordsrv.common.config.serializer;
import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed; 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.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.serialize.SerializationException; import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.serialize.TypeSerializer; import org.spongepowered.configurate.serialize.TypeSerializer;
import java.awt.Color;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Collections;
public class DiscordMessageEmbedSerializer implements TypeSerializer<DiscordMessageEmbed.Builder> { public class DiscordMessageEmbedSerializer implements TypeSerializer<DiscordMessageEmbed.Builder> {
@Override @Override
public DiscordMessageEmbed.Builder deserialize(Type type, ConfigurationNode node) throws SerializationException { public DiscordMessageEmbed.Builder deserialize(Type type, ConfigurationNode node) throws SerializationException {
// TODO if (!node.node("Enabled").getBoolean(node.node("Enable").getBoolean(true))) {
return null; 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 @Override
public void serialize(Type type, DiscordMessageEmbed.@Nullable Builder obj, ConfigurationNode node) throws SerializationException { public void serialize(Type type, DiscordMessageEmbed.@Nullable Builder obj, ConfigurationNode node)
// TODO 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<DiscordMessageEmbed.Field> {
@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());
}
} }
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.serializer; package com.discordsrv.common.config.serializer;
import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed; 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.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
public class SendableDiscordMessageSerializer implements TypeSerializer<SendableDiscordMessage.Builder> { public class SendableDiscordMessageSerializer implements TypeSerializer<SendableDiscordMessage.Builder> {
@Override @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(); String contentOnly = node.getString();
if (contentOnly != null) { if (contentOnly != null) {
return SendableDiscordMessage.builder() return SendableDiscordMessage.builder()
@ -25,16 +44,21 @@ public class SendableDiscordMessageSerializer implements TypeSerializer<Sendable
SendableDiscordMessage.Builder builder = SendableDiscordMessage.builder(); SendableDiscordMessage.Builder builder = SendableDiscordMessage.builder();
ConfigurationNode webhook = node.node("Webhook"); ConfigurationNode webhook = node.node("Webhook");
if (webhook.node("Enabled").getBoolean(false)) { String webhookUsername = webhook.node("Username").getString();
builder.setWebhookUsername(webhook.node("Username").getString()); if (webhook.node("Enabled").getBoolean(webhook.node("Enable").getBoolean(webhookUsername != null))) {
builder.setWebhookUsername(webhookUsername);
builder.setWebhookAvatarUrl(webhook.node("AvatarUrl").getString()); builder.setWebhookAvatarUrl(webhook.node("AvatarUrl").getString());
} }
List<DiscordMessageEmbed.Builder> embeds = node.node("Embeds").getList(DiscordMessageEmbed.Builder.class); // v1 compat
if (embeds != null) { DiscordMessageEmbed.Builder singleEmbed = node.node("Embed").get(
for (DiscordMessageEmbed.Builder embed : embeds) { DiscordMessageEmbed.Builder.class);
builder.addEmbed(embed.build()); List<DiscordMessageEmbed.Builder> 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()); builder.setContent(node.node("Content").getString());
@ -42,7 +66,8 @@ public class SendableDiscordMessageSerializer implements TypeSerializer<Sendable
} }
@Override @Override
public void serialize(Type type, SendableDiscordMessage.@Nullable Builder obj, ConfigurationNode node) throws SerializationException { public void serialize(Type type, SendableDiscordMessage.@Nullable Builder obj, ConfigurationNode node)
throws SerializationException {
if (obj == null) { if (obj == null) {
node.set(null); node.set(null);
return; return;
@ -52,7 +77,7 @@ public class SendableDiscordMessageSerializer implements TypeSerializer<Sendable
if (webhookUsername != null) { if (webhookUsername != null) {
ConfigurationNode webhook = node.node("Webhook"); ConfigurationNode webhook = node.node("Webhook");
webhook.node("Username").set(webhookUsername); webhook.node("Username").set(webhookUsername);
webhook.node("AvatarUrl").set(Optional.ofNullable(obj.getWebhookAvatarUrl()).orElse("")); webhook.node("AvatarUrl").set(obj.getWebhookAvatarUrl());
} }
List<DiscordMessageEmbed.Builder> embedBuilders = new ArrayList<>(); List<DiscordMessageEmbed.Builder> embedBuilders = new ArrayList<>();

View File

@ -31,6 +31,9 @@ import java.util.concurrent.ForkJoinPool;
/** /**
* TODO: revamp * 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 { public class InitialDependencyLoader {

View File

@ -54,17 +54,18 @@ public class DiscordAPIImpl implements DiscordAPI {
private final DiscordSRV discordSRV; private final DiscordSRV discordSRV;
private final AsyncLoadingCache<String, WebhookClient> cachedClients = Caffeine.newBuilder() private final AsyncLoadingCache<String, WebhookClient> cachedClients;
.removalListener((RemovalListener<String, WebhookClient>) (id, client, cause) -> {
if (client != null) {
client.close();
}
})
.expireAfter(new WebhookCacheExpiry())
.buildAsync(new WebhookCacheLoader());
public DiscordAPIImpl(DiscordSRV discordSRV) { public DiscordAPIImpl(DiscordSRV discordSRV) {
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
this.cachedClients = discordSRV.caffeineBuilder()
.removalListener((RemovalListener<String, WebhookClient>) (id, client, cause) -> {
if (client != null) {
client.close();
}
})
.expireAfter(new WebhookCacheExpiry())
.buildAsync(new WebhookCacheLoader());
} }
public CompletableFuture<WebhookClient> queryWebhookClient(String channelId) { public CompletableFuture<WebhookClient> queryWebhookClient(String channelId) {

View File

@ -22,8 +22,17 @@ import com.discordsrv.api.discord.connection.DiscordConnectionDetails;
import com.discordsrv.api.event.bus.EventPriority; import com.discordsrv.api.event.bus.EventPriority;
import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.bus.Subscribe;
import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; 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.DiscordSRV;
import com.discordsrv.common.config.connection.ConnectionConfig; 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.discord.connection.DiscordConnectionManager;
import com.discordsrv.common.scheduler.Scheduler; import com.discordsrv.common.scheduler.Scheduler;
import com.discordsrv.common.scheduler.threadfactory.CountingThreadFactory; import com.discordsrv.common.scheduler.threadfactory.CountingThreadFactory;
@ -31,6 +40,7 @@ import com.neovisionaries.ws.client.WebSocketFactory;
import com.neovisionaries.ws.client.WebSocketFrame; import com.neovisionaries.ws.client.WebSocketFrame;
import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder; 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.DisconnectEvent;
import net.dv8tion.jda.api.events.ShutdownEvent; import net.dv8tion.jda.api.events.ShutdownEvent;
import net.dv8tion.jda.api.events.StatusChangeEvent; 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.requests.RestAction;
import net.dv8tion.jda.api.utils.AllowedMentions; import net.dv8tion.jda.api.utils.AllowedMentions;
import net.dv8tion.jda.api.utils.MemberCachePolicy; 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.hooks.EventManagerProxy;
import net.dv8tion.jda.internal.utils.IOUtil; import net.dv8tion.jda.internal.utils.IOUtil;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import javax.security.auth.login.LoginException; import javax.security.auth.login.LoginException;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.*; import java.util.concurrent.*;
@ -79,6 +91,33 @@ public class JDAConnectionManager implements DiscordConnectionManager {
shutdown().join(); shutdown().join();
} }
@Subscribe(priority = EventPriority.EARLIEST)
public void onPlaceholderLookup(PlaceholderLookupEvent event) {
Set<Object> 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 @Subscribe
public void onStatusChange(StatusChangeEvent event) { public void onStatusChange(StatusChangeEvent event) {
DiscordSRV.Status currentStatus = discordSRV.status(); DiscordSRV.Status currentStatus = discordSRV.status();

View File

@ -30,11 +30,7 @@ import com.discordsrv.common.config.main.channels.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.ChannelConfig; import com.discordsrv.common.config.main.channels.ChannelConfig;
import com.discordsrv.common.function.OrDefault; import com.discordsrv.common.function.OrDefault;
import com.discordsrv.common.player.util.PlayerUtil; 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.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -57,19 +53,19 @@ public class DefaultChatListener extends AbstractListener {
OrDefault<BaseChannelConfig> channelConfig = discordSRV.channelConfig().orDefault(gameChannel); OrDefault<BaseChannelConfig> channelConfig = discordSRV.channelConfig().orDefault(gameChannel);
Component discordMessage = EnhancedLegacyText.get().buildComponent(channelConfig.map(cfg -> cfg.minecraftToDiscord).get(cfg -> cfg.messageFormat)) // Component discordMessage = EnhancedLegacyText.get().buildComponent(channelConfig.map(cfg -> cfg.minecraftToDiscord).get(cfg -> cfg.messageFormat))
.replace("%message%", message) // .replace("%message%", message)
.replace("%player_display_name%", displayName) // .replace("%player_display_name%", displayName)
.build(); // .build();
//
String username = new Placeholders(channelConfig.map(cfg -> cfg.minecraftToDiscord).get(cfg -> cfg.usernameFormat)) // String username = new Placeholders(channelConfig.map(cfg -> cfg.minecraftToDiscord).get(cfg -> cfg.usernameFormat))
.replace("%player_display_name%", () -> PlainTextComponentSerializer.plainText().serialize(displayName)) // .replace("%player_display_name%", () -> PlainTextComponentSerializer.plainText().serialize(displayName))
.get(); // .get();
discordSRV.eventBus().publish( discordSRV.eventBus().publish(
new ChatMessageSendEvent( new ChatMessageSendEvent(
DiscordSerializer.INSTANCE.serialize(discordMessage), null,
username, null,
gameChannel gameChannel
) )
); );

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Class<?>, Set<PlaceholderProvider>> 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<Object> 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<Object> context) {
for (Object o : context) {
Set<PlaceholderProvider> 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<Object> context) {
return processReplacement(PATTERN, input, context);
}
private String processReplacement(Pattern pattern, String input, Set<Object> 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<Class<?>, Set<PlaceholderProvider>> {
@Override
public @Nullable Set<PlaceholderProvider> load(@NonNull Class<?> key) {
Set<PlaceholderProvider> providers = new HashSet<>();
Class<?> currentClass = key;
do {
List<Class<?>> 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;
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Object> 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;
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Object> context);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Object> 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)
);
}
}

View File

@ -18,6 +18,7 @@
package com.discordsrv.common.player; package com.discordsrv.common.player;
import com.discordsrv.api.placeholder.Placeholder;
import net.kyori.adventure.identity.Identified; import net.kyori.adventure.identity.Identified;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -27,10 +28,12 @@ import java.util.UUID;
public interface IOfflinePlayer extends Identified { public interface IOfflinePlayer extends Identified {
@Placeholder("player_name")
@Nullable @Nullable
String getUsername(); String getUsername();
@ApiStatus.NonExtendable @ApiStatus.NonExtendable
@Placeholder("player_uuid")
@NotNull @NotNull
default UUID uuid() { default UUID uuid() {
return identity().uuid(); return identity().uuid();

View File

@ -18,9 +18,11 @@
package com.discordsrv.common.player; package com.discordsrv.common.player;
import com.discordsrv.api.placeholder.Placeholder;
import com.discordsrv.api.player.DiscordSRVPlayer; import com.discordsrv.api.player.DiscordSRVPlayer;
import com.discordsrv.common.command.game.sender.ICommandSender; import com.discordsrv.common.command.game.sender.ICommandSender;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -28,7 +30,6 @@ import java.util.UUID;
public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSender { public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSender {
@SuppressWarnings("NullableProblems") // IOfflinePlayer != IPlayer
@Override @Override
@NotNull @NotNull
String getUsername(); String getUsername();
@ -40,4 +41,11 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende
} }
Component displayName(); Component displayName();
@ApiStatus.NonExtendable
@Placeholder("player_display_name")
default String plainDisplayName() {
return PlainTextComponentSerializer.plainText()
.serialize(displayName());
}
} }