More progress on placeholders & Minecraft -> Discord chat

This commit is contained in:
Vankka 2021-08-14 21:16:32 +03:00
parent 3fb79cc50c
commit ff8385dfda
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
40 changed files with 670 additions and 298 deletions

View File

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

View File

@ -66,7 +66,7 @@ public interface MinecraftComponent {
*
* @param gsonSerializerClass the gson serializer class
* @return a adapter that will convert to/from relocated or unrelocated adventure classes to/from json
* @throws IllegalArgumentException if the provided class is not a Adventure GsonComponentSerializer
* @throws IllegalArgumentException if the provided class is not an Adventure GsonComponentSerializer
*/
@NotNull
Adapter adventureAdapter(@NotNull Class<?> gsonSerializerClass);
@ -96,7 +96,7 @@ public interface MinecraftComponent {
}
/**
* A Adventure adapter, converts from/to given adventure components from/to json.
* An Adventure adapter, converts from/to given adventure components from/to json.
*/
interface Adapter {

View File

@ -39,4 +39,6 @@ public interface MinecraftComponentFactory {
*/
@NotNull
MinecraftComponent empty();
EnhancedTextBuilder enhancedBuilder(String content);
}

View File

@ -27,6 +27,7 @@ import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel;
import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage;
import com.discordsrv.api.discord.api.entity.user.DiscordUser;
import org.jetbrains.annotations.NotNull;
@ -37,6 +38,8 @@ import java.util.Optional;
*/
public interface DiscordAPI {
SendableDiscordMessage.Formatter format(SendableDiscordMessage.Builder message);
/**
* Gets a Discord message channel by id, the provided entity can be cached and will not update if it changes on Discord.
* @param id the id for the message channel

View File

@ -29,6 +29,10 @@ import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A message that can be sent to Discord.
@ -190,5 +194,33 @@ public interface SendableDiscordMessage {
SendableDiscordMessage build();
}
interface Formatter {
Formatter addContext(Object... context);
default Formatter addReplacement(String target, Object replacement) {
return addReplacement(Pattern.compile(target, Pattern.LITERAL), replacement);
}
default Formatter addReplacement(Pattern target, Object replacement) {
return addReplacement(target, matcher -> replacement);
}
default Formatter addReplacement(String target, Supplier<Object> replacement) {
return addReplacement(Pattern.compile(target, Pattern.LITERAL), replacement);
}
default Formatter addReplacement(Pattern target, Supplier<Object> replacement) {
return addReplacement(target, matcher -> replacement.get());
}
default Formatter addReplacement(String target, Function<Matcher, Object> replacement) {
return addReplacement(Pattern.compile(target, Pattern.LITERAL), replacement);
}
Formatter addReplacement(Pattern target, Function<Matcher, Object> replacement);
SendableDiscordMessage build();
}
}

View File

@ -42,10 +42,10 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
private final String webhookAvatarUrl;
protected SendableDiscordMessageImpl(String content,
List<DiscordMessageEmbed> embeds,
Set<AllowedMention> allowedMentions,
String webhookUsername,
String webhookAvatarUrl) {
List<DiscordMessageEmbed> embeds,
Set<AllowedMention> allowedMentions,
String webhookUsername,
String webhookAvatarUrl) {
this.content = content;
this.embeds = embeds;
this.allowedMentions = allowedMentions;
@ -78,7 +78,7 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
return webhookAvatarUrl;
}
public static class BuilderImpl implements Builder {
public static class BuilderImpl implements SendableDiscordMessage.Builder {
private String content;
private final List<DiscordMessageEmbed> embeds = new ArrayList<>();

View File

@ -24,43 +24,32 @@
package com.discordsrv.api.event.events.message.send.game;
import com.discordsrv.api.channel.GameChannel;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage;
import com.discordsrv.api.event.events.Cancellable;
import com.discordsrv.api.event.events.Processable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public abstract class AbstractGameMessageSendEvent implements Cancellable, Processable {
private String discordMessage;
private String discordUsername;
private SendableDiscordMessage discordMessage;
private GameChannel targetChannel;
private boolean cancelled;
private boolean processed;
public AbstractGameMessageSendEvent(@NotNull String discordMessage, @Nullable String discordUsername, @NotNull GameChannel targetChannel) {
public AbstractGameMessageSendEvent(@NotNull SendableDiscordMessage discordMessage, @NotNull GameChannel targetChannel) {
this.discordMessage = discordMessage;
this.discordUsername = discordUsername;
this.targetChannel = targetChannel;
}
@NotNull
public String getDiscordMessage() {
public SendableDiscordMessage getDiscordMessage() {
return discordMessage;
}
public void setDiscordMessage(@NotNull String discordMessage) {
public void setDiscordMessage(@NotNull SendableDiscordMessage discordMessage) {
this.discordMessage = discordMessage;
}
@Nullable
public String getDiscordUsername() {
return discordUsername;
}
public void setDiscordUsername(@Nullable String discordUsername) {
this.discordUsername = discordUsername;
}
@NotNull
public GameChannel getTargetChannel() {
return targetChannel;

View File

@ -24,12 +24,12 @@
package com.discordsrv.api.event.events.message.send.game;
import com.discordsrv.api.channel.GameChannel;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ChatMessageSendEvent extends AbstractGameMessageSendEvent {
public ChatMessageSendEvent(@NotNull String discordMessage, @Nullable String discordUsername, @NotNull GameChannel targetChannel) {
super(discordMessage, discordUsername, targetChannel);
public ChatMessageSendEvent(@NotNull SendableDiscordMessage discordMessage, @NotNull GameChannel targetChannel) {
super(discordMessage, targetChannel);
}
}

View File

@ -32,7 +32,7 @@ public class PlaceholderLookupResult {
public static final PlaceholderLookupResult UNKNOWN_PLACEHOLDER = new PlaceholderLookupResult(Type.UNKNOWN_PLACEHOLDER);
public static PlaceholderLookupResult success(Object result) {
return new PlaceholderLookupResult(String.valueOf(result));
return new PlaceholderLookupResult(result);
}
public static PlaceholderLookupResult newLookup(String placeholder, Set<Object> extras) {
@ -40,7 +40,7 @@ public class PlaceholderLookupResult {
}
private final Type type;
private final String value;
private final Object value;
private final Set<Object> extras;
protected PlaceholderLookupResult(Type type) {
@ -49,7 +49,7 @@ public class PlaceholderLookupResult {
this.extras = null;
}
protected PlaceholderLookupResult(String value) {
protected PlaceholderLookupResult(Object value) {
this.type = Type.SUCCESS;
this.value = value;
this.extras = null;
@ -65,7 +65,7 @@ public class PlaceholderLookupResult {
return type;
}
public String getValue() {
public Object getValue() {
return value;
}

View File

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

View File

@ -23,7 +23,10 @@
package com.discordsrv.api.placeholder;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public interface PlaceholderService {
@ -38,9 +41,16 @@ public interface PlaceholderService {
*/
Pattern RECURSIVE_PATTERN = Pattern.compile("(\\{)(.+)(})");
PlaceholderLookupResult lookupPlaceholder(String placeholder, Set<Object> context);
PlaceholderLookupResult lookupPlaceholder(String placeholder, Object... context);
void addResultConverter(@NotNull PlaceholderResultConverter resultConverter);
void removeResultConverter(@NotNull PlaceholderResultConverter resultConverter);
String replacePlaceholders(@NotNull String placeholder, @NotNull Set<Object> context);
String replacePlaceholders(@NotNull String placeholder, @NotNull Object... context);
PlaceholderLookupResult lookupPlaceholder(@NotNull String placeholder, @NotNull Set<Object> context);
PlaceholderLookupResult lookupPlaceholder(@NotNull String placeholder, @NotNull Object... context);
Object getResult(@NotNull Matcher matcher, @NotNull Set<Object> context);
String getResultAsString(@NotNull Matcher matcher, @NotNull Set<Object> context);
String replacePlaceholders(String placeholder, Set<Object> context);
String replacePlaceholders(String placeholder, Object... context);
}

View File

@ -26,9 +26,11 @@ import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.craftbukkit.BukkitComponentSerializer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Method;
@SuppressWarnings("NullableProblems") // BukkitOfflinePlayer nullability
public class BukkitPlayer extends BukkitOfflinePlayer implements IPlayer {
private static final Method DISPLAY_NAME_METHOD; // Paper 1.16+
@ -72,12 +74,13 @@ public class BukkitPlayer extends BukkitOfflinePlayer implements IPlayer {
@SuppressWarnings("deprecation") // Paper
@Override
public Component displayName() {
public @NotNull Component displayName() {
if (DISPLAY_NAME_METHOD != null) {
try {
return ComponentUtil.fromUnrelocated(DISPLAY_NAME_METHOD.invoke(player));
} catch (Throwable ignored) {}
}
// Use the legacy method
return BukkitComponentSerializer.legacy().deserialize(player.getDisplayName());
}

View File

@ -67,7 +67,7 @@ public class BungeePlayer implements IPlayer {
}
@Override
public Component displayName() {
public @NotNull Component displayName() {
return BungeeComponentUtil.fromLegacy(player.getDisplayName());
}
}

View File

@ -41,6 +41,7 @@ import com.discordsrv.common.listener.DefaultChatListener;
import com.discordsrv.common.logging.DependencyLoggingFilter;
import com.discordsrv.common.logging.logger.backend.LoggingBackend;
import com.discordsrv.common.placeholder.PlaceholderServiceImpl;
import com.discordsrv.common.placeholder.converter.ComponentResultConverter;
import net.dv8tion.jda.api.JDA;
import org.jetbrains.annotations.NotNull;
@ -84,7 +85,7 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
protected final void load() {
this.eventBus = new EventBusImpl(this);
this.placeholderService = new PlaceholderServiceImpl(this);
this.componentFactory = new ComponentFactory();
this.componentFactory = new ComponentFactory(this);
this.discordAPI = new DiscordAPIImpl(this);
this.discordConnectionDetails = new DiscordConnectionDetailsImpl(this);
}
@ -239,6 +240,9 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
discordConnectionManager = new JDAConnectionManager(this);
discordConnectionManager.connect().join();
// Placeholder result converters
placeholderService().addResultConverter(new ComponentResultConverter());
// Register PlayerProvider listeners
playerProvider().subscribe();

View File

@ -22,7 +22,6 @@ import com.discordsrv.api.channel.GameChannel;
import com.discordsrv.api.event.events.channel.GameChannelLookupEvent;
import com.discordsrv.common.DiscordSRV;
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.LoadingCache;
@ -55,7 +54,7 @@ public class ChannelConfig {
});
}
private Map<String, ChannelConfigHolder> channels() {
private Map<String, BaseChannelConfig> channels() {
return discordSRV.config().channels;
}
@ -64,12 +63,12 @@ public class ChannelConfig {
}
public OrDefault<BaseChannelConfig> orDefault(String ownerName, String channelName) {
ChannelConfigHolder defaultConfig = channels().computeIfAbsent(
"default", key -> new ChannelConfigHolder(new BaseChannelConfig()));
BaseChannelConfig defaultConfig = channels().computeIfAbsent(
"default", key -> new BaseChannelConfig());
return new OrDefault<>(
get(ownerName, channelName),
defaultConfig.get()
defaultConfig
);
}
@ -79,15 +78,15 @@ public class ChannelConfig {
public BaseChannelConfig get(String ownerName, String channelName) {
if (ownerName != null) {
ChannelConfigHolder config = channels().get(ownerName + ":" + channelName);
BaseChannelConfig config = channels().get(ownerName + ":" + channelName);
if (config != null) {
return config.get();
return config;
}
GameChannel gameChannel = channels.get(channelName);
if (gameChannel != null && gameChannel.getOwnerName().equals(ownerName)) {
config = channels().get(channelName);
return config != null ? config.get() : null;
return config;
}
return null;
}

View File

@ -18,14 +18,27 @@
package com.discordsrv.common.component;
import com.discordsrv.api.component.EnhancedTextBuilder;
import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.api.component.MinecraftComponentFactory;
import com.discordsrv.common.DiscordSRV;
import org.jetbrains.annotations.NotNull;
public class ComponentFactory implements MinecraftComponentFactory {
private final DiscordSRV discordSRV;
public ComponentFactory(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
}
@Override
public @NotNull MinecraftComponent empty() {
return MinecraftComponentImpl.empty();
}
@Override
public EnhancedTextBuilder enhancedBuilder(String content) {
return new EnhancedTextBuilderImpl(discordSRV, content);
}
}

View File

@ -0,0 +1,72 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.component;
import com.discordsrv.api.component.EnhancedTextBuilder;
import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.api.placeholder.PlaceholderService;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil;
import dev.vankka.enhancedlegacytext.EnhancedComponentBuilder;
import dev.vankka.enhancedlegacytext.EnhancedLegacyText;
import net.kyori.adventure.text.Component;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class EnhancedTextBuilderImpl implements EnhancedTextBuilder {
private final Set<Object> context = new HashSet<>();
private final Map<Pattern, Function<Matcher, Object>> replacements = new HashMap<>();
private final DiscordSRV discordSRV;
private final String enhancedFormat;
public EnhancedTextBuilderImpl(DiscordSRV discordSRV, String enhancedFormat) {
this.discordSRV = discordSRV;
this.enhancedFormat = enhancedFormat;
}
@Override
public EnhancedTextBuilder addContext(Object... context) {
this.context.addAll(Arrays.asList(context));
return this;
}
@Override
public EnhancedTextBuilder addReplacement(Pattern target, Function<Matcher, Object> replacement) {
this.replacements.put(target, replacement);
return this;
}
@Override
public MinecraftComponent build() {
EnhancedComponentBuilder builder = EnhancedLegacyText.get()
.buildComponent(enhancedFormat);
replacements.forEach(builder::replaceAll);
builder.replaceAll(PlaceholderService.PATTERN,
matcher -> discordSRV.placeholderService().getResult(matcher, context));
Component component = builder.build();
return ComponentUtil.toAPI(component);
}
}

View File

@ -26,6 +26,8 @@ import org.spongepowered.configurate.objectmapping.meta.Comment;
public class ConnectionConfig implements Config {
public static final String FILE_NAME = "connections.yaml";
@Override
public final String getFileName() {
return FILE_NAME;
}
@ -37,5 +39,6 @@ public class ConnectionConfig implements Config {
@Comment("Don't know what this is? Neither do I")
public String token = "Token here";
}
}

View File

@ -22,7 +22,6 @@ import com.discordsrv.common.config.Config;
import com.discordsrv.common.config.annotation.DefaultOnly;
import com.discordsrv.common.config.main.channels.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.ChannelConfig;
import com.discordsrv.common.config.main.channels.ChannelConfigHolder;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.util.HashMap;
@ -32,13 +31,15 @@ import java.util.Map;
public class MainConfig implements Config {
public static final String FILE_NAME = "config.yaml";
@Override
public final String getFileName() {
return FILE_NAME;
}
@DefaultOnly("default")
public Map<String, ChannelConfigHolder> channels = new HashMap<String, ChannelConfigHolder>() {{
put("default", new ChannelConfigHolder(new BaseChannelConfig()));
put("global", new ChannelConfigHolder(new ChannelConfig()));
public Map<String, BaseChannelConfig> channels = new HashMap<String, BaseChannelConfig>() {{
put("default", new BaseChannelConfig());
put("global", new ChannelConfig());
}};
}

View File

@ -19,10 +19,50 @@
package com.discordsrv.common.config.main.channels;
import com.discordsrv.common.config.main.channels.minecraftodiscord.MinecraftToDiscordChatConfig;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.serialize.TypeSerializer;
import java.lang.reflect.Type;
@ConfigSerializable
public class BaseChannelConfig {
public MinecraftToDiscordChatConfig minecraftToDiscord = new MinecraftToDiscordChatConfig();
public static class Serializer implements TypeSerializer<BaseChannelConfig> {
private final ObjectMapper.Factory mapperFactory;
public Serializer(ObjectMapper.Factory mapperFactory) {
this.mapperFactory = mapperFactory;
}
@Override
public BaseChannelConfig deserialize(Type type, ConfigurationNode node) throws SerializationException {
return (BaseChannelConfig) mapperFactory.asTypeSerializer()
.deserialize(
"default".equals(node.key()) ? BaseChannelConfig.class : ChannelConfig.class,
node
);
}
@Override
public void serialize(Type type, @Nullable BaseChannelConfig obj, ConfigurationNode node) throws SerializationException {
if (obj == null) {
node.set(null);
return;
}
mapperFactory.asTypeSerializer().serialize(
"default".equals(node.key()) ? BaseChannelConfig.class : ChannelConfig.class,
obj,
node
);
}
}
}

View File

@ -20,7 +20,6 @@ package com.discordsrv.common.config.main.channels;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import org.spongepowered.configurate.objectmapping.meta.Setting;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
@ -31,8 +30,6 @@ import java.util.List;
@ConfigSerializable
public class ChannelConfig extends BaseChannelConfig {
protected static final String CHANNEL_IDS_OPTION_NAME = "ChannelIds";
public ChannelConfig() {
// Clear everything besides channelIds by default (these will be filled back in by Configurate if they are in the config itself)
for (Field field : getClass().getFields()) {
@ -50,7 +47,6 @@ public class ChannelConfig extends BaseChannelConfig {
}
}
@Setting(CHANNEL_IDS_OPTION_NAME)
@Comment("The channels this in-game channel will forward to in Discord")
public List<String> channelIds = new ArrayList<>(Collections.singletonList("channel-id-here"));

View File

@ -1,74 +0,0 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main.channels;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.serialize.TypeSerializer;
import java.lang.reflect.Type;
/**
* A bit of a trick to have two different types in the same map with Configurate.
*/
public class ChannelConfigHolder {
private final BaseChannelConfig obj;
public ChannelConfigHolder(BaseChannelConfig obj) {
this.obj = obj;
}
public BaseChannelConfig get() {
return obj;
}
public static class Serializer implements TypeSerializer<ChannelConfigHolder> {
private final ObjectMapper.Factory mapperFactory;
public Serializer(ObjectMapper.Factory mapperFactory) {
this.mapperFactory = mapperFactory;
}
@Override
public ChannelConfigHolder deserialize(Type type, ConfigurationNode node) throws SerializationException {
boolean channelsNotPresent = node.node(ChannelConfig.CHANNEL_IDS_OPTION_NAME).empty();
BaseChannelConfig channelConfig = (BaseChannelConfig) mapperFactory
.get(channelsNotPresent ? BaseChannelConfig.class : ChannelConfig.class)
.load(node);
return new ChannelConfigHolder(channelConfig);
}
@Override
public void serialize(Type type, @Nullable ChannelConfigHolder obj, ConfigurationNode node) throws SerializationException {
map(obj != null ? obj.get() : null, node);
}
@SuppressWarnings("unchecked")
private <T extends BaseChannelConfig> void map(BaseChannelConfig obj, ConfigurationNode node) throws SerializationException {
mapperFactory
.get((Class<T>) (obj instanceof ChannelConfig ? ChannelConfig.class : BaseChannelConfig.class))
.save((T) obj, node);
}
}
}

View File

@ -28,8 +28,6 @@ public class MinecraftToDiscordChatConfig {
@Setting("Format")
public SendableDiscordMessage.Builder messageFormat = SendableDiscordMessage.builder()
.setWebhookUsername("%player_display_name%")
.setContent("%player_message%");// TODO
@Setting("UseWebhooks")
public boolean useWebhooks = false;
.setContent("%message%");// TODO
}

View File

@ -24,7 +24,9 @@ import com.discordsrv.common.config.manager.loader.YamlConfigLoaderProvider;
import com.discordsrv.common.config.manager.manager.TranslatedConfigManager;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
public abstract class ConnectionConfigManager<C extends ConnectionConfig> extends TranslatedConfigManager<C, YamlConfigurationLoader> implements YamlConfigLoaderProvider {
public abstract class ConnectionConfigManager<C extends ConnectionConfig>
extends TranslatedConfigManager<C, YamlConfigurationLoader>
implements YamlConfigLoaderProvider {
public ConnectionConfigManager(DiscordSRV discordSRV) {
super(discordSRV);

View File

@ -18,22 +18,12 @@
package com.discordsrv.common.config.manager;
import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.MainConfig;
import com.discordsrv.common.config.main.channels.ChannelConfigHolder;
import com.discordsrv.common.config.manager.loader.YamlConfigLoaderProvider;
import com.discordsrv.common.config.manager.manager.TranslatedConfigManager;
import com.discordsrv.common.config.serializer.ColorSerializer;
import com.discordsrv.common.config.serializer.DiscordMessageEmbedSerializer;
import com.discordsrv.common.config.serializer.SendableDiscordMessageSerializer;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import java.awt.Color;
public abstract class MainConfigManager<C extends MainConfig>
extends TranslatedConfigManager<C, YamlConfigurationLoader>
implements YamlConfigLoaderProvider {
@ -46,17 +36,4 @@ public abstract class MainConfigManager<C extends MainConfig>
protected String fileName() {
return MainConfig.FILE_NAME;
}
@Override
public ConfigurationOptions defaultOptions() {
return super.defaultOptions()
.serializers(builder -> {
ObjectMapper.Factory objectMapper = defaultObjectMapper();
builder.register(Color.class, new ColorSerializer());
builder.register(ChannelConfigHolder.class, new ChannelConfigHolder.Serializer(objectMapper));
builder.register(DiscordMessageEmbed.Builder.class, new DiscordMessageEmbedSerializer());
builder.register(DiscordMessageEmbed.Field.class, new DiscordMessageEmbedSerializer.FieldSerializer());
builder.register(SendableDiscordMessage.Builder.class, new SendableDiscordMessageSerializer());
});
}
}

View File

@ -18,9 +18,15 @@
package com.discordsrv.common.config.manager.manager;
import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.annotation.DefaultOnly;
import com.discordsrv.common.config.main.channels.BaseChannelConfig;
import com.discordsrv.common.config.manager.loader.ConfigLoaderProvider;
import com.discordsrv.common.config.serializer.ColorSerializer;
import com.discordsrv.common.config.serializer.DiscordMessageEmbedSerializer;
import com.discordsrv.common.config.serializer.SendableDiscordMessageSerializer;
import com.discordsrv.common.exception.ConfigException;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.CommentedConfigurationNode;
@ -31,7 +37,9 @@ import org.spongepowered.configurate.loader.AbstractConfigurationLoader;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.util.NamingSchemes;
import java.awt.Color;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
@ -73,31 +81,40 @@ public abstract class ConfigurateConfigManager<T, LT extends AbstractConfigurati
public ConfigurationOptions defaultOptions() {
return ConfigurationOptions.defaults()
.shouldCopyDefaults(false);
}
protected ObjectMapper.Factory.Builder objectMapperBuilder() {
return ObjectMapper.factoryBuilder();
.shouldCopyDefaults(false)
.implicitInitialization(false)
.serializers(builder -> {
ObjectMapper.Factory objectMapper = configObjectMapper();
builder.register(Color.class, new ColorSerializer());
builder.register(BaseChannelConfig.class, new BaseChannelConfig.Serializer(objectMapper));
builder.register(DiscordMessageEmbed.Builder.class, new DiscordMessageEmbedSerializer());
builder.register(DiscordMessageEmbed.Field.class, new DiscordMessageEmbedSerializer.FieldSerializer());
builder.register(SendableDiscordMessage.Builder.class, new SendableDiscordMessageSerializer());
});
}
public ConfigurationOptions configNodeOptions() {
return defaultOptions();
}
protected ObjectMapper.Factory.Builder configObjectMapperBuilder() {
return objectMapperBuilder();
}
public ObjectMapper.Factory configObjectMapper() {
return configObjectMapper;
}
public ConfigurationOptions defaultNodeOptions() {
return defaultOptions();
}
protected ObjectMapper.Factory.Builder objectMapperBuilder() {
return ObjectMapper.factoryBuilder()
.defaultNamingScheme(input -> {
String camelCase = NamingSchemes.CAMEL_CASE.coerce(input); // Gets rid of underscores and dashes
return Character.toUpperCase(camelCase.charAt(0)) + camelCase.substring(1);
});
}
protected ObjectMapper.Factory.Builder configObjectMapperBuilder() {
return objectMapperBuilder();
}
protected ObjectMapper.Factory.Builder defaultObjectMapperBuilder() {
return configObjectMapperBuilder()
return objectMapperBuilder()
.addProcessor(DefaultOnly.class, (data, value) -> (value1, destination) -> {
String[] children = data.value();
boolean whitelist = data.whitelist();
@ -131,6 +148,10 @@ public abstract class ConfigurateConfigManager<T, LT extends AbstractConfigurati
});
}
public ObjectMapper.Factory configObjectMapper() {
return configObjectMapper;
}
public ObjectMapper.Factory defaultObjectMapper() {
return defaultObjectMapper;
}
@ -190,14 +211,19 @@ public abstract class ConfigurateConfigManager<T, LT extends AbstractConfigurati
}
}
@SuppressWarnings("unchecked")
@Override
public void save() throws ConfigException {
try {
CommentedConfigurationNode node = loader.createNode();
node.set(configuration);
save(configuration, (Class<T>) configuration.getClass(), node);
loader.save(node);
} catch (ConfigurateException e) {
throw new ConfigException("Failed to load configuration", e);
}
}
protected void save(T config, Class<T> clazz, CommentedConfigurationNode node) throws SerializationException {
configObjectMapper().get(clazz).save(config, node);
}
}

View File

@ -31,7 +31,8 @@ import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import java.net.URL;
public abstract class TranslatedConfigManager<T extends Config, LT extends AbstractConfigurationLoader<CommentedConfigurationNode>> extends ConfigurateConfigManager<T, LT> {
public abstract class TranslatedConfigManager<T extends Config, LT extends AbstractConfigurationLoader<CommentedConfigurationNode>>
extends ConfigurateConfigManager<T, LT> {
public TranslatedConfigManager(DiscordSRV discordSRV) {
super(discordSRV);
@ -55,6 +56,7 @@ public abstract class TranslatedConfigManager<T extends Config, LT extends Abstr
return translation;
}
@SuppressWarnings("unchecked")
public void translate() throws ConfigException {
T config = config();
if (config == null) {
@ -70,7 +72,8 @@ public abstract class TranslatedConfigManager<T extends Config, LT extends Abstr
translation = translation.node(config.getFileName());
CommentedConfigurationNode node = loader().createNode();
node.set(config);
save(config, (Class<T>) config.getClass(), node);
//node.set(config);
translateNode(node, translation, translation.node("_comments"));
} catch (ConfigurateException e) {
throw new ConfigException(e);

View File

@ -45,7 +45,9 @@ public class SendableDiscordMessageSerializer implements TypeSerializer<Sendable
ConfigurationNode webhook = node.node("Webhook");
String webhookUsername = webhook.node("Username").getString();
if (webhook.node("Enabled").getBoolean(webhook.node("Enable").getBoolean(webhookUsername != null))) {
if (webhook.node("Enabled").getBoolean(
webhook.node("Enable").getBoolean(
webhookUsername != null))) {
builder.setWebhookUsername(webhookUsername);
builder.setWebhookAvatarUrl(webhook.node("AvatarUrl").getString());
}
@ -82,7 +84,9 @@ public class SendableDiscordMessageSerializer implements TypeSerializer<Sendable
List<DiscordMessageEmbed.Builder> embedBuilders = new ArrayList<>();
obj.getEmbeds().forEach(embed -> embedBuilders.add(embed.toBuilder()));
node.setList(DiscordMessageEmbed.Builder.class, embedBuilders);
if (!embedBuilders.isEmpty()) {
node.node("Embeds").setList(DiscordMessageEmbed.Builder.class, embedBuilders);
}
node.node("Content").set(obj.getContent());
}

View File

@ -25,18 +25,22 @@ import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel;
import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage;
import com.discordsrv.api.discord.api.entity.user.DiscordUser;
import com.discordsrv.api.discord.api.exception.NotReadyException;
import com.discordsrv.api.discord.api.exception.UnknownChannelException;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.channels.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.ChannelConfig;
import com.discordsrv.common.config.main.channels.ChannelConfigHolder;
import com.discordsrv.common.discord.api.channel.DiscordDMChannelImpl;
import com.discordsrv.common.discord.api.channel.DiscordTextChannelImpl;
import com.discordsrv.common.discord.api.guild.DiscordGuildImpl;
import com.discordsrv.common.discord.api.message.SendableDiscordMessageFormatterImpl;
import com.discordsrv.common.discord.api.user.DiscordUserImpl;
import com.github.benmanes.caffeine.cache.*;
import com.github.benmanes.caffeine.cache.AsyncCacheLoader;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Expiry;
import com.github.benmanes.caffeine.cache.RemovalListener;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.User;
@ -72,6 +76,11 @@ public class DiscordAPIImpl implements DiscordAPI {
return cachedClients.get(channelId);
}
@Override
public SendableDiscordMessage.Formatter format(SendableDiscordMessage.Builder message) {
return new SendableDiscordMessageFormatterImpl(discordSRV, message);
}
@Override
public @NotNull Optional<? extends DiscordMessageChannel> getMessageChannelById(@NotNull String id) {
Optional<DiscordTextChannel> textChannel = getTextChannelById(id);
@ -163,8 +172,7 @@ public class DiscordAPIImpl implements DiscordAPI {
private class WebhookCacheExpiry implements Expiry<String, WebhookClient> {
private boolean isConfiguredChannel(String channelId) {
for (ChannelConfigHolder value : discordSRV.config().channels.values()) {
BaseChannelConfig config = value.get();
for (BaseChannelConfig config : discordSRV.config().channels.values()) {
if (config instanceof ChannelConfig
&& ((ChannelConfig) config).channelIds.contains(channelId)) {
return true;

View File

@ -23,11 +23,11 @@ import club.minnced.discord.webhook.receive.ReadonlyMessage;
import club.minnced.discord.webhook.receive.ReadonlyUser;
import club.minnced.discord.webhook.send.WebhookEmbed;
import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel;
import com.discordsrv.api.discord.api.exception.UnknownChannelException;
import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage;
import com.discordsrv.api.discord.api.entity.message.impl.SendableDiscordMessageImpl;
import com.discordsrv.api.discord.api.exception.UnknownChannelException;
import com.discordsrv.common.DiscordSRV;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;

View File

@ -0,0 +1,79 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api.message;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage;
import com.discordsrv.api.placeholder.PlaceholderService;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.placeholder.converter.ComponentResultConverter;
import com.discordsrv.common.string.util.Placeholders;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SendableDiscordMessageFormatterImpl implements SendableDiscordMessage.Formatter {
private final Set<Object> context = new HashSet<>();
private final Map<Pattern, Function<Matcher, Object>> replacements = new HashMap<>();
private final DiscordSRV discordSRV;
private final SendableDiscordMessage.Builder builder;
public SendableDiscordMessageFormatterImpl(DiscordSRV discordSRV, SendableDiscordMessage.Builder builder) {
this.discordSRV = discordSRV;
this.builder = builder;
}
@Override
public SendableDiscordMessage.Formatter addContext(Object... context) {
this.context.addAll(Arrays.asList(context));
return this;
}
@Override
public SendableDiscordMessage.Formatter addReplacement(Pattern target, Function<Matcher, Object> replacement) {
this.replacements.put(target, replacement);
return this;
}
@Override
public SendableDiscordMessage build() {
Function<String, String> placeholders = input -> {
if (input == null) {
return null;
}
return new Placeholders(input)
.addAll(replacements)
.replaceAll(PlaceholderService.PATTERN,
matcher -> discordSRV.placeholderService().getResultAsString(matcher, context))
.get();
};
ComponentResultConverter.plain(() ->
builder.setWebhookUsername(placeholders.apply(builder.getWebhookUsername())));
builder.setContent(placeholders.apply(builder.getContent()));
// TODO: rest of the content, escaping unwanted characters
return builder.build();
}
}

View File

@ -28,8 +28,9 @@ import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.config.main.channels.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.ChannelConfig;
import com.discordsrv.common.config.main.channels.minecraftodiscord.MinecraftToDiscordChatConfig;
import com.discordsrv.common.function.OrDefault;
import com.discordsrv.common.player.util.PlayerUtil;
import dev.vankka.mcdiscordreserializer.discord.DiscordSerializer;
import net.kyori.adventure.text.Component;
import java.util.Collections;
@ -49,23 +50,23 @@ public class DefaultChatListener extends AbstractListener {
GameChannel gameChannel = event.getGameChannel();
Component message = ComponentUtil.fromAPI(event.message());
Component displayName = PlayerUtil.displayName(event.getPlayer());
OrDefault<BaseChannelConfig> channelConfig = discordSRV.channelConfig().orDefault(gameChannel);
OrDefault<MinecraftToDiscordChatConfig> chatConfig = channelConfig.map(cfg -> cfg.minecraftToDiscord);
// Component discordMessage = EnhancedLegacyText.get().buildComponent(channelConfig.map(cfg -> cfg.minecraftToDiscord).get(cfg -> cfg.messageFormat))
// .replace("%message%", message)
// .replace("%player_display_name%", displayName)
// .build();
//
// String username = new Placeholders(channelConfig.map(cfg -> cfg.minecraftToDiscord).get(cfg -> cfg.usernameFormat))
// .replace("%player_display_name%", () -> PlainTextComponentSerializer.plainText().serialize(displayName))
// .get();
SendableDiscordMessage.Builder builder = chatConfig.get(cfg -> cfg.messageFormat);
if (builder == null) {
return;
}
SendableDiscordMessage discordMessage = discordSRV.discordAPI().format(builder)
.addContext(event.getPlayer())
.addReplacement("%message%", DiscordSerializer.INSTANCE.serialize(message))
.build();
discordSRV.eventBus().publish(
new ChatMessageSendEvent(
null,
null,
discordMessage,
gameChannel
)
);
@ -86,13 +87,7 @@ public class DefaultChatListener extends AbstractListener {
for (String channelId : channelIds) {
discordSRV.discordAPI().getTextChannelById(channelId).ifPresent(textChannel ->
textChannel.sendMessage(
SendableDiscordMessage.builder()
.setWebhookUsername(event.getDiscordUsername())
.setContent(event.getDiscordMessage())
.build()
)
);
textChannel.sendMessage(event.getDiscordMessage()));
}
}
}

View File

@ -21,6 +21,7 @@ package com.discordsrv.common.placeholder;
import com.discordsrv.api.event.events.placeholder.PlaceholderLookupEvent;
import com.discordsrv.api.placeholder.Placeholder;
import com.discordsrv.api.placeholder.PlaceholderLookupResult;
import com.discordsrv.api.placeholder.PlaceholderResultConverter;
import com.discordsrv.api.placeholder.PlaceholderService;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.placeholder.provider.AnnotationPlaceholderProvider;
@ -29,11 +30,13 @@ import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -42,6 +45,7 @@ public class PlaceholderServiceImpl implements PlaceholderService {
private final DiscordSRV discordSRV;
private final LoadingCache<Class<?>, Set<PlaceholderProvider>> classProviders;
private final Set<PlaceholderResultConverter> converters = new CopyOnWriteArraySet<>();
public PlaceholderServiceImpl(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
@ -58,13 +62,20 @@ public class PlaceholderServiceImpl implements PlaceholderService {
}
@Override
public PlaceholderLookupResult lookupPlaceholder(String placeholder, Object... context) {
public PlaceholderLookupResult lookupPlaceholder(@NotNull String placeholder, Object... context) {
return lookupPlaceholder(placeholder, getArrayAsSet(context));
}
@Override
public PlaceholderLookupResult lookupPlaceholder(String placeholder, Set<Object> context) {
public PlaceholderLookupResult lookupPlaceholder(@NotNull String placeholder, @NotNull Set<Object> context) {
for (Object o : context) {
if (o instanceof PlaceholderProvider) {
PlaceholderLookupResult result = ((PlaceholderProvider) o).lookup(placeholder, context);
if (result.getType() != PlaceholderLookupResult.Type.UNKNOWN_PLACEHOLDER) {
return result;
}
}
Set<PlaceholderProvider> providers = classProviders.get(o.getClass());
if (providers == null) {
continue;
@ -88,35 +99,108 @@ public class PlaceholderServiceImpl implements PlaceholderService {
}
@Override
public String replacePlaceholders(String input, Object... context) {
public String replacePlaceholders(@NotNull String input, Object... context) {
return replacePlaceholders(input, getArrayAsSet(context));
}
@Override
public String replacePlaceholders(String input, Set<Object> context) {
return processReplacement(PATTERN, input, context);
public void addResultConverter(@NotNull PlaceholderResultConverter resultConverter) {
converters.add(resultConverter);
}
private String processReplacement(Pattern pattern, String input, Set<Object> context) {
@Override
public void removeResultConverter(@NotNull PlaceholderResultConverter resultConverter) {
converters.remove(resultConverter);
}
@Override
public String replacePlaceholders(@NotNull String input, @NotNull Set<Object> context) {
return getReplacement(PATTERN, input, context);
}
private String getReplacement(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);
PlaceholderLookupResult result = resolve(placeholder, context);
output = updateContent(result, placeholder, matcher, output);
}
return output;
}
private String updateBasedOnResult(
PlaceholderLookupResult result, String input, String originalPlaceholder, Matcher matcher) {
String output = input;
@Override
public Object getResult(@NotNull Matcher matcher, @NotNull Set<Object> context) {
if (matcher.groupCount() < 3) {
throw new IllegalStateException("Matcher must have atleast 3 groups");
}
String placeholder = matcher.group(2);
PlaceholderLookupResult result = resolve(matcher, context);
return getResultRepresentation(result, placeholder, matcher);
}
@Override
public String getResultAsString(@NotNull Matcher matcher, @NotNull Set<Object> context) {
Object result = getResult(matcher, context);
return getResultAsString(result);
}
private String getResultAsString(Object result) {
if (result == null) {
return "null";
} else if (result instanceof String) {
return (String) result;
}
String output = null;
for (PlaceholderResultConverter converter : converters) {
output = converter.convertPlaceholderResult(result);
if (output != null) {
break;
}
}
if (output == null) {
output = String.valueOf(result);
}
return output;
}
public PlaceholderLookupResult resolve(Matcher matcher, Set<Object> context) {
return resolve(matcher.group(2), context);
}
private PlaceholderLookupResult resolve(String placeholder, Set<Object> context) {
// Recursive
placeholder = getReplacement(RECURSIVE_PATTERN, placeholder, context);
return lookupPlaceholder(placeholder, context);
}
private String updateContent(
PlaceholderLookupResult result, String placeholder, Matcher matcher, String input) {
Object representation = getResultRepresentation(result, placeholder, matcher);
String output = getResultAsString(representation);
for (PlaceholderResultConverter converter : converters) {
output = converter.convertPlaceholderResult(representation);
if (output != null) {
break;
}
}
if (output == null) {
output = String.valueOf(representation);
}
return Pattern.compile(
matcher.group(1) + placeholder + matcher.group(3),
Pattern.LITERAL
)
.matcher(input)
.replaceFirst(output);
}
private Object getResultRepresentation(PlaceholderLookupResult result, String placeholder, Matcher matcher) {
while (result != null) {
PlaceholderLookupResult.Type type = result.getType();
if (type == PlaceholderLookupResult.Type.UNKNOWN_PLACEHOLDER) {
@ -124,7 +208,7 @@ public class PlaceholderServiceImpl implements PlaceholderService {
}
boolean newLookup = false;
String replacement = null;
Object replacement = null;
switch (type) {
case SUCCESS:
replacement = result.getValue();
@ -136,27 +220,18 @@ public class PlaceholderServiceImpl implements PlaceholderService {
replacement = "Error";
break;
case NEW_LOOKUP:
// prevent infinite recursion
if (result.getValue().equals(originalPlaceholder)) {
break;
}
result = lookupPlaceholder(result.getValue(), result.getExtras());
result = lookupPlaceholder((String) result.getValue(), result.getExtras());
newLookup = true;
break;
}
if (replacement != null) {
output = Pattern.compile(
matcher.group(1)
+ originalPlaceholder
+ matcher.group(3),
Pattern.LITERAL
).matcher(output).replaceFirst(replacement);
return replacement;
}
if (!newLookup) {
break;
}
}
return output;
return matcher.group(1) + placeholder + matcher.group(3);
}
private static class ClassProviderLoader implements CacheLoader<Class<?>, Set<PlaceholderProvider>> {

View File

@ -0,0 +1,56 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.placeholder.converter;
import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.api.placeholder.PlaceholderResultConverter;
import com.discordsrv.common.component.util.ComponentUtil;
import dev.vankka.mcdiscordreserializer.discord.DiscordSerializer;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.jetbrains.annotations.NotNull;
public class ComponentResultConverter implements PlaceholderResultConverter {
private static final ThreadLocal<Boolean> PLAIN_CONTEXT = new ThreadLocal<>();
public static void plain(Runnable runnable) {
PLAIN_CONTEXT.set(true);
runnable.run();
PLAIN_CONTEXT.set(false);
}
@Override
public String convertPlaceholderResult(@NotNull Object result) {
if (result instanceof MinecraftComponent) {
result = ComponentUtil.fromAPI((MinecraftComponent) result);
}
if (result instanceof Component) {
Component component = (Component) result;
if (PLAIN_CONTEXT.get()) {
return PlainTextComponentSerializer.plainText()
.serialize(component);
} else {
return DiscordSerializer.INSTANCE
.serialize(component);
}
}
return null;
}
}

View File

@ -22,7 +22,6 @@ import com.discordsrv.api.placeholder.Placeholder;
import com.discordsrv.api.player.DiscordSRVPlayer;
import com.discordsrv.common.command.game.sender.ICommandSender;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@ -40,12 +39,8 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende
return identity().uuid();
}
@NotNull
@Placeholder("player_display_name")
Component displayName();
@ApiStatus.NonExtendable
@Placeholder("player_display_name")
default String plainDisplayName() {
return PlainTextComponentSerializer.plainText()
.serialize(displayName());
}
}

View File

@ -1,36 +0,0 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.player.util;
import com.discordsrv.api.player.DiscordSRVPlayer;
import com.discordsrv.common.player.IPlayer;
import net.kyori.adventure.text.Component;
public final class PlayerUtil {
private PlayerUtil() {}
public static Component displayName(DiscordSRVPlayer player) {
if (player instanceof IPlayer) {
return ((IPlayer) player).displayName();
} else {
return Component.text(player.getUsername());
}
}
}

View File

@ -18,9 +18,9 @@
package com.discordsrv.common.string.util;
import javax.annotation.CheckReturnValue;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -28,54 +28,55 @@ import java.util.regex.Pattern;
public class Placeholders {
private final String inputText;
private final Map<Pattern, Supplier<String>> replacements = new HashMap<>();
private final Map<Pattern, Function<Matcher, Object>> replacements = new HashMap<>();
public Placeholders(String inputText) {
this.inputText = inputText;
}
@CheckReturnValue
public Placeholders replace(String replace, String replacement) {
return replace(replace, () -> replacement);
public Placeholders addAll(Map<Pattern, Function<Matcher, Object>> replacements) {
replacements.forEach(this.replacements::put);
return this;
}
@CheckReturnValue
public Placeholders replaceAll(String regex, String replacement) {
return replaceAll(regex, () -> replacement);
public Placeholders replace(String target, Object replacement) {
return replace(target, matcher -> replacement);
}
@CheckReturnValue
public Placeholders replaceAll(Pattern pattern, String replacement) {
return replaceAll(pattern, () -> replacement);
public Placeholders replaceAll(Pattern pattern, Object replacement) {
return replaceAll(pattern, matcher -> replacement);
}
@CheckReturnValue
public Placeholders replace(String replace, Supplier<String> replacement) {
return replaceAll(Pattern.compile(replace, Pattern.LITERAL), replacement);
public Placeholders replace(String target, Supplier<Object> replacement) {
return replaceAll(Pattern.compile(target, Pattern.LITERAL), matcher -> replacement);
}
@CheckReturnValue
public Placeholders replaceAll(String regex, Supplier<String> replacement) {
return replaceAll(Pattern.compile(regex), replacement);
public Placeholders replaceAll(Pattern pattern, Supplier<Object> replacement) {
return replaceAll(pattern, matcher -> replacement);
}
@CheckReturnValue
public Placeholders replaceAll(Pattern pattern, Supplier<String> replacement) {
replacements.put(pattern, replacement);
public Placeholders replace(String target, Function<Matcher, Object> replacement) {
return replaceAll(Pattern.compile(target, Pattern.LITERAL), replacement);
}
public Placeholders replaceAll(Pattern pattern, Function<Matcher, Object> replacement) {
this.replacements.put(pattern, replacement);
return this;
}
public String get() {
String input = inputText;
for (Map.Entry<Pattern, Supplier<String>> entry : replacements.entrySet()) {
for (Map.Entry<Pattern, Function<Matcher, Object>> entry : replacements.entrySet()) {
Pattern pattern = entry.getKey();
Matcher matcher = pattern.matcher(input);
if (!matcher.find()) {
continue;
}
Supplier<String> replacement = entry.getValue();
input = matcher.replaceAll(replacement.get());
Function<Matcher, Object> replacement = entry.getValue();
Object value = replacement.apply(matcher);
input = matcher.replaceAll(String.valueOf(value));
}
return input;
}

View File

@ -22,7 +22,7 @@ import com.discordsrv.bukkit.config.connection.BukkitConnectionConfig;
import com.discordsrv.bukkit.config.main.BukkitConfig;
import com.discordsrv.common.config.Config;
import com.discordsrv.common.config.annotation.Untranslated;
import com.discordsrv.common.config.main.channels.ChannelConfigHolder;
import com.discordsrv.common.config.main.channels.BaseChannelConfig;
import org.spongepowered.configurate.CommentedConfigurationNode;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
@ -75,9 +75,9 @@ public final class DiscordSRVTranslation {
.build();
ChannelConfigHolder.Serializer channelSerializer = new ChannelConfigHolder.Serializer(objectMapper);
BaseChannelConfig.Serializer channelSerializer = new BaseChannelConfig.Serializer(objectMapper);
CommentedConfigurationNode node = CommentedConfigurationNode.root(ConfigurationOptions.defaults()
.serializers(builder -> builder.register(ChannelConfigHolder.class, channelSerializer)));
.serializers(builder -> builder.register(BaseChannelConfig.class, channelSerializer)));
for (Config config : CONFIG_INSTANCES) {
ConfigurationNode section = node.node(config.getFileName());
ConfigurationNode configSection = section.copy();

View File

@ -22,6 +22,7 @@ import com.discordsrv.common.player.IPlayer;
import com.discordsrv.sponge.SpongeDiscordSRV;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import org.spongepowered.api.command.exception.CommandException;
import org.spongepowered.api.entity.living.player.server.ServerPlayer;
@ -54,7 +55,7 @@ public class SpongePlayer extends SpongeOfflinePlayer implements IPlayer {
}
@Override
public Component displayName() {
public @NotNull Component displayName() {
return player.displayName().get();
}
}

View File

@ -61,7 +61,7 @@ public class VelocityPlayer implements IPlayer {
}
@Override
public Component displayName() {
public @NotNull Component displayName() {
// Use Adventure's Pointer, otherwise username
return player.getOrDefaultFrom(
Identity.DISPLAY_NAME,