diff --git a/Patchwork/src/main/java/fns/patchwork/command/BukkitDelegate.java b/Patchwork/src/main/java/fns/patchwork/command/BukkitDelegate.java index 01162c6..46efcf9 100644 --- a/Patchwork/src/main/java/fns/patchwork/command/BukkitDelegate.java +++ b/Patchwork/src/main/java/fns/patchwork/command/BukkitDelegate.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.Set; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; @@ -156,50 +157,19 @@ private void processSubCommands(final @NotNull String @NotNull [] args, if (argTypes.length > args.length) return; - final Object[] objects = new Object[argTypes.length + 1]; - - for (int i = 0; i < argTypes.length; i++) - { - final Class argType = argTypes[i]; - final String arg = args[i]; + final Player p = (sender instanceof Player player) ? player : null; - if (argType.equals(String.class)) - { - if (i == argTypes.length - 1) - { - final String[] reasonArgs = Arrays.copyOfRange(args, i, args.length - 1); - final String reason = String.join(" ", reasonArgs); - objects[i] = reason; - } - else - { - continue; - } - } + final Object[] objects = new Object[argTypes.length + 1]; - if (argType.equals(Location.class)) - { - final String[] locationArgs = Arrays.copyOfRange(args, i, i + 3); - final String location = String.join(" ", locationArgs); - objects[i] = location; - } + parseArguments(args, provider, argTypes, objects); - final Object obj = provider.fromString(arg, argType); - if (obj == null) - { - FNS4J.getLogger("Datura") - .error("Failed to parse argument " + arg + " for type " + argType.getName()); - return; - } - objects[i] = obj; - } try { if (noConsole) { command.getSubcommands() .get(node) - .invoke(command, (Player) sender, objects); + .invoke(command, p, objects); } else { @@ -215,11 +185,55 @@ private void processSubCommands(final @NotNull String @NotNull [] args, } } + private void parseArguments(@NotNull String @NotNull [] args, + ContextProvider provider, + Class[] argTypes, + Object[] objects) + { + for (int i = 0; i < argTypes.length; i++) + { + final Class argType = argTypes[i]; + String arg = args[i]; + + boolean wasResolved = false; + + if (argType.equals(String.class) && (i == argTypes.length - 1)) + { + final String[] reasonArgs = Arrays.copyOfRange(args, i, args.length - 1); + final String reason = String.join(" ", reasonArgs); + objects[i] = reason; + wasResolved = true; + } + + if (argType.equals(Location.class)) + { + final String[] locationArgs = Arrays.copyOfRange(args, i, i + 3); + arg = String.join(" ", locationArgs); + } + + if (!wasResolved) + { + final Optional obj = provider.fromString(arg, argType); + if (obj.isEmpty()) + { + FNS4J.getLogger("Datura") + .error("Failed to parse argument " + arg + " for type " + argType.getName()); + continue; + } + objects[i] = obj.get(); + } + } + } + @Override - public List tabComplete(final CommandSender sender, final String alias, final String[] args) + public @NotNull List tabComplete(final @NotNull CommandSender sender, final @NotNull String alias, + final String[] args) { - final Set completions = command.getCompletions(); final List results = new ArrayList<>(); + final Set completions = command.getCompletions(); + + if (completions == null || completions.isEmpty()) + return results; if (args.length == 0) { diff --git a/Patchwork/src/main/java/fns/patchwork/config/Configuration.java b/Patchwork/src/main/java/fns/patchwork/config/Configuration.java index b7b4780..19ef8c2 100644 --- a/Patchwork/src/main/java/fns/patchwork/config/Configuration.java +++ b/Patchwork/src/main/java/fns/patchwork/config/Configuration.java @@ -25,6 +25,7 @@ import fns.patchwork.provider.Context; import fns.patchwork.provider.ContextProvider; +import java.util.Collection; import org.jetbrains.annotations.Unmodifiable; import java.io.File; @@ -87,7 +88,7 @@ public interface Configuration * @param clazz The class of the type. * @return The List object. */ - @Unmodifiable List getList(String path, Class clazz); + @Unmodifiable Collection getCollection(String path, Class clazz); /** * Gets a List object from the associated path. The List that is returned will be the String values which are stored diff --git a/Patchwork/src/main/java/fns/patchwork/config/GenericConfig.java b/Patchwork/src/main/java/fns/patchwork/config/GenericConfig.java new file mode 100644 index 0000000..507eb8f --- /dev/null +++ b/Patchwork/src/main/java/fns/patchwork/config/GenericConfig.java @@ -0,0 +1,280 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and 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 fns.patchwork.config; + +import com.electronwill.nightconfig.core.Config; +import com.electronwill.nightconfig.core.ConfigFormat; +import com.electronwill.nightconfig.core.UnmodifiableConfig; +import fns.patchwork.provider.ContextProvider; +import fns.patchwork.utils.FileUtils; +import fns.patchwork.utils.logging.FNS4J; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +public final class GenericConfig implements Configuration +{ + private static final ContextProvider PROVIDER = new ContextProvider(); + private final File configFile; + private final String fileName; + private final Config config; + private final ConfigType configType; + + public GenericConfig(@NotNull final ConfigType configType, + @Nullable final JavaPlugin plugin, + @NotNull final File dataFolder, + @NotNull final String fileName, + final boolean isConcurrent) throws IOException + { + if (!fileName.endsWith(configType.getExtension())) + throw new IllegalArgumentException("File name must end with " + configType.getExtension() + "!"); + + // Ternary just to piss off Allink :) + final Optional file = (plugin != null) ? + FileUtils.getOrCreateFileWithResource(dataFolder, fileName, plugin) : + FileUtils.getOrCreateFile(dataFolder, fileName); + + if (file.isEmpty()) + throw new FileNotFoundException(); + + this.configFile = file.get(); + this.fileName = fileName; + this.configType = configType; + + final ConfigFormat format = configType.getFormat(); + + // Another ternary just to piss off Allink :) + this.config = isConcurrent ? format.createConcurrentConfig() : format.createConfig(); + + this.load(); + } + + public GenericConfig(final ConfigType type, final File dataFolder, final String fileName) + throws IOException + { + this(type, null, dataFolder, fileName, false); + } + + public GenericConfig(final ConfigType type, final JavaPlugin plugin, final String fileName) + throws IOException + { + this(type, plugin, plugin.getDataFolder(), fileName, false); + } + + public GenericConfig(final ConfigType type, final File dataFolder, final String fileName, + final boolean isConcurrent) + throws IOException + { + this(type, null, dataFolder, fileName, isConcurrent); + } + + @Override + public void save() throws IOException + { + final File backup = new File(this.configFile.getParentFile(), this.fileName + ".bak"); + + if (backup.exists()) + Files.delete(backup.toPath()); + + Files.copy(this.configFile.toPath(), backup.toPath()); + + try (final FileWriter writer = new FileWriter(this.configFile)) + { + this.configType.getWriter().write(this.getConfig(), writer); + } + } + + @Override + public void load() throws IOException + { + try (final FileReader reader = new FileReader(this.configFile)) + { + this.config.clear(); + + final UnmodifiableConfig parsed = this.configType.getParser().parse(reader).unmodifiable(); + this.config.putAll(parsed); + } + } + + @Override + public String getFileName() + { + return fileName; + } + + @Override + public File getConfigurationFile() + { + return configFile; + } + + @Override + public String getString(final String path) + { + if (!(this.getConfig().get(path) instanceof String)) + throw new IllegalArgumentException(String.format("Value at path %s is not a string!", path)); + + return this.getConfig().get(path); + } + + @Override + public boolean getBoolean(final String path) + { + if (!(this.getConfig().get(path) instanceof Boolean)) + throw new IllegalArgumentException(String.format("Value at path %s is not a boolean!", path)); + + return this.getConfig().get(path); + } + + + /* + * I am pretty sure that this works, but not really. + * This is sort of a shot in the dark because Night Config did specify that they support collections + * in TOML and JSON files, but there is no specific get method for objects that are not primitives. + * Additionally, not all primitives are natively supported. + */ + @Override + public @Unmodifiable Collection getCollection(String path, Class clazz) + { + if (!(this.getConfig().get(path) instanceof Collection)) + throw new IllegalArgumentException(String.format("Value at path %s is not a collection!", path)); + + final Collection collection = this.getConfig().get(path); + final Collection collected = new ArrayList<>(); + + collection.stream() + .map(obj -> + { + final Optional optional; + + if (obj instanceof String string) + optional = PROVIDER.fromString(string, clazz); + else if (clazz.isInstance(obj)) + optional = Optional.of(clazz.cast(obj)); + else + optional = Optional.empty(); + + return optional; + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toCollection(() -> collected)); + + return collected; + } + + /* + * I am pretty sure that this works, but not really. + * This is sort of a shot in the dark because Night Config did specify that they support collections + * in TOML and JSON files, but there is no specific get method for objects that are not primitives. + * Additionally, not all primitives are natively supported. + */ + @Override + public @Unmodifiable List getStringList(String path) + { + if (!(this.getConfig().get(path) instanceof Collection c)) + throw new IllegalArgumentException(String.format("Value at path %s is not a collection!", path)); + + final Collection collection = this.getConfig().get(path); + final List list = new ArrayList<>(); + + if (c.isEmpty() || !(c.toArray()[0] instanceof String)) + { + FNS4J.PATCHWORK.warn(String.format("Collection at path %s is empty or does not contain strings!", path)); + FNS4J.PATCHWORK.warn("Returning empty list!"); + return list; + } + + collection.stream() + .map(String.class::cast) + .collect(Collectors.toCollection(() -> list)); + + return list; + } + + @Override + public int getInt(String path) + { + return this.getConfig().getInt(path); + } + + @Override + public long getLong(String path) + { + return this.getConfig().getLong(path); + } + + @Override + public double getDouble(String path) + { + if (!(this.getConfig().get(path) instanceof Double)) + throw new IllegalArgumentException(String.format("Value at path %s is not a double!", path)); + + return this.getConfig().get(path); + } + + @Override + public Optional get(String path, Class clazz) + { + return this.getConfig() + .getOptional(path) + .filter(clazz::isInstance) + .map(clazz::cast); + } + + @Override + public T getOrDefault(String path, Class clazz, T fallback) + { + return this.get(path, clazz) + .orElse(fallback); + } + + @Override + public void set(final String path, final T value) + { + this.config.set(path, value); + } + + private UnmodifiableConfig getConfig() + { + return config.unmodifiable(); + } + + public ConfigType getConfigType() + { + return configType; + } +} diff --git a/Patchwork/src/main/java/fns/patchwork/config/WrappedBukkitConfiguration.java b/Patchwork/src/main/java/fns/patchwork/config/WrappedBukkitConfiguration.java index f50e146..1baf944 100644 --- a/Patchwork/src/main/java/fns/patchwork/config/WrappedBukkitConfiguration.java +++ b/Patchwork/src/main/java/fns/patchwork/config/WrappedBukkitConfiguration.java @@ -94,7 +94,7 @@ public boolean getBoolean(String path) } @Override - public List getList(String path, Class clazz) + public List getCollection(String path, Class clazz) { return this.contextProvider.getList(this.getStringList(path), clazz); } @@ -132,7 +132,7 @@ public void set(String path, T value) @Override public Optional get(String path, Class clazz) { - return Optional.ofNullable(this.contextProvider.fromString(path, clazz)); + return this.contextProvider.fromString(path, clazz); } @Override diff --git a/Patchwork/src/main/java/fns/patchwork/kyori/MiniMessageWrapper.java b/Patchwork/src/main/java/fns/patchwork/kyori/MiniMessageWrapper.java index c814ab9..fcb292e 100644 --- a/Patchwork/src/main/java/fns/patchwork/kyori/MiniMessageWrapper.java +++ b/Patchwork/src/main/java/fns/patchwork/kyori/MiniMessageWrapper.java @@ -37,14 +37,14 @@ public class MiniMessageWrapper private static final MiniMessage unsafe = MiniMessage.miniMessage(); private static final MiniMessage safe = MiniMessage.builder() .tags(TagResolver.resolver( - StandardTags.color(), - StandardTags.rainbow(), - StandardTags.gradient(), - StandardTags.newline(), - StandardTags.decorations(TextDecoration.ITALIC), - StandardTags.decorations(TextDecoration.BOLD), - StandardTags.decorations(TextDecoration.STRIKETHROUGH), - StandardTags.decorations(TextDecoration.UNDERLINED) + StandardTags.color(), + StandardTags.rainbow(), + StandardTags.gradient(), + StandardTags.newline(), + StandardTags.decorations(TextDecoration.ITALIC), + StandardTags.decorations(TextDecoration.BOLD), + StandardTags.decorations(TextDecoration.STRIKETHROUGH), + StandardTags.decorations(TextDecoration.UNDERLINED) )) .build(); diff --git a/Patchwork/src/main/java/fns/patchwork/provider/ContextProvider.java b/Patchwork/src/main/java/fns/patchwork/provider/ContextProvider.java index 9bb86f3..476a3a6 100644 --- a/Patchwork/src/main/java/fns/patchwork/provider/ContextProvider.java +++ b/Patchwork/src/main/java/fns/patchwork/provider/ContextProvider.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; @@ -60,7 +61,7 @@ */ public class ContextProvider { - public T fromString(final String string, final Class clazz) + public Optional fromString(final String string, final Class clazz) { return Stream.of(toBoolean(string, clazz), toLong(string, clazz), @@ -74,9 +75,9 @@ public T fromString(final String string, final Class clazz) toLocation(string, clazz), toComponent(string, clazz)) .filter(Objects::nonNull) - .findFirst() + .filter(clazz::isInstance) .map(clazz::cast) - .orElse(null); + .findFirst(); } private @Nullable Boolean toBoolean(final String string, final Class clazz) @@ -227,10 +228,9 @@ private OfflinePlayer toOfflinePlayer(final String string, final Class clazz) public @NotNull List<@Nullable T> getList(final List resolvable, final Class clazz) { final List resolved = new ArrayList<>(); - for (final String entry : resolvable) - { - resolved.add(this.fromString(entry, clazz)); - } + + resolvable.forEach(entry -> this.fromString(entry, clazz).ifPresent(resolved::add)); + return resolved; } } diff --git a/Veritas/src/main/java/fns/veritas/Aggregate.java b/Veritas/src/main/java/fns/veritas/Aggregate.java index 51afc09..16d8607 100644 --- a/Veritas/src/main/java/fns/veritas/Aggregate.java +++ b/Veritas/src/main/java/fns/veritas/Aggregate.java @@ -28,11 +28,19 @@ import fns.veritas.bukkit.ServerListener; import fns.veritas.client.BotClient; import fns.veritas.client.BotConfig; +import java.io.IOException; import org.bukkit.Bukkit; public class Aggregate { private static final FNS4J logger = FNS4J.getLogger("Veritas"); + private static final String FAILED_PACKET = """ + Failed to process inbound chat packet. + An offending element was found transmitted through the stream. + The element has been dropped, and ignored. + Offending element: %s + Caused by: %s + Stack Trace: %s"""; private final BotClient bot; private final Veritas plugin; private final BukkitNative bukkitNativeListener; @@ -40,13 +48,39 @@ public class Aggregate public Aggregate(final Veritas plugin) { + BotClient bot1; this.plugin = plugin; - this.bot = new BotClient(new BotConfig(plugin)); + + try + { + bot1 = new BotClient(new BotConfig(plugin)); + } + catch (IOException ex) + { + getLogger().error("Failed to load bot config! Shutting down..."); + getLogger().error(ex); + this.bot = null; + this.serverListener = null; + this.bukkitNativeListener = null; + Bukkit.getPluginManager().disablePlugin(plugin); + return; + } + this.bukkitNativeListener = new BukkitNative(plugin); this.serverListener = new ServerListener(plugin); - Bukkit.getServer().getPluginManager().registerEvents(this.getBukkitNativeListener(), plugin); - this.getServerListener().minecraftChatBound().subscribe(); + Bukkit.getServer() + .getPluginManager() + .registerEvents(this.getBukkitNativeListener(), plugin); + this.getServerListener() + .minecraftChatBound() + .onErrorContinue((th, v) -> Aggregate.getLogger() + .error(FAILED_PACKET.formatted( + v.getClass().getName(), + th.getCause(), + th.getMessage()))) + .subscribe(); + this.bot = bot1; } public static FNS4J getLogger() @@ -69,6 +103,11 @@ public BotClient getBot() return bot; } + public BotConfig getBotConfig() + { + return bot.getConfig(); + } + public Veritas getPlugin() { return plugin; diff --git a/Veritas/src/main/java/fns/veritas/bukkit/ServerListener.java b/Veritas/src/main/java/fns/veritas/bukkit/ServerListener.java index 6d9e113..f0ac173 100644 --- a/Veritas/src/main/java/fns/veritas/bukkit/ServerListener.java +++ b/Veritas/src/main/java/fns/veritas/bukkit/ServerListener.java @@ -54,20 +54,20 @@ public ServerListener(final Veritas plugin) public Mono minecraftChatBound() { return bot.getClient() - .getEventDispatcher() - .on(MessageCreateEvent.class) - .filter(m -> m.getMessage() - .getChannelId() - .equals(bot.getChatChannelId())) - .filter(m -> m.getMember().orElse(null) != null) - .filter(m -> !m.getMessage() - .getAuthor() - .orElseThrow(IllegalAccessError::new) - .getId() - .equals(plugin.getAggregate().getBot().getClient().getSelfId())) - .doOnError(Aggregate.getLogger()::error) - .doOnNext(this::doMessageBodyDetails) - .then(); + .getEventDispatcher() + .on(MessageCreateEvent.class) + .filter(m -> m.getMessage() + .getChannelId() + .equals(bot.getConfig().getChatChannelId())) + .filter(m -> m.getMember().orElse(null) != null) + .filter(m -> !m.getMessage() + .getAuthor() + .orElseThrow(IllegalAccessError::new) + .getId() + .equals(bot.getClient().getSelfId())) + .doOnError(Aggregate.getLogger()::error) + .doOnNext(this::doMessageBodyDetails) + .then(); } private void doMessageBodyDetails(MessageCreateEvent m) @@ -81,7 +81,7 @@ private void doMessageBodyDetails(MessageCreateEvent m) .hoverEvent(HoverEvent.showText( Component.text("Click to join our Discord server!"))) .clickEvent(ClickEvent.openUrl( - plugin.getAggregate().getBot().getInviteLink()))) + plugin.getAggregate().getBotConfig().getInviteLink()))) .append(Component.text("] ", NamedTextColor.DARK_GRAY)); TextComponent user = Component.empty(); diff --git a/Veritas/src/main/java/fns/veritas/client/BotClient.java b/Veritas/src/main/java/fns/veritas/client/BotClient.java index 46bc391..a20a3ad 100644 --- a/Veritas/src/main/java/fns/veritas/client/BotClient.java +++ b/Veritas/src/main/java/fns/veritas/client/BotClient.java @@ -23,16 +23,20 @@ package fns.veritas.client; -import discord4j.common.util.Snowflake; import discord4j.core.DiscordClientBuilder; import discord4j.core.GatewayDiscordClient; import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; import discord4j.core.object.entity.Guild; import discord4j.core.object.entity.Message; +import discord4j.core.object.entity.PartialMember; +import discord4j.core.object.entity.Role; +import discord4j.core.object.entity.User; +import discord4j.core.object.entity.channel.Channel; import discord4j.core.object.entity.channel.TextChannel; import discord4j.core.spec.MessageCreateSpec; import fns.veritas.cmd.base.BotCommandHandler; import java.util.List; +import java.util.Objects; import reactor.core.publisher.Mono; public class BotClient @@ -58,37 +62,11 @@ public BotClient(final BotConfig config) client.on(ChatInputInteractionEvent.class, handler::handle); } - - public String getBotId() - { - return client.getSelfId().asString(); - } - - public Mono getServerGuildId() - { - return client.getGuildById(config.getId()); - } - public GatewayDiscordClient getClient() { return client; } - public Snowflake getChatChannelId() - { - return config.getChatChannelId(); - } - - public Snowflake getLogChannelId() - { - return config.getLogChannelId(); - } - - public String getInviteLink() - { - return config.getInviteLink(); - } - public void messageChatChannel(String message, boolean system) { String channelID = config.getChatChannelId().asString(); @@ -119,7 +97,6 @@ private String sanitizeChatMessage(String message) if (message.contains("@")) { - // \u200B is Zero Width Space, invisible on Discord newMessage = message.replace("@", "@\u200B"); } @@ -144,6 +121,33 @@ private String sanitizeChatMessage(String message) return deformat(newMessage); } + public Mono isAdmin(final User user) + { + return getGuild().flatMap(guild -> guild.getMemberById(user.getId())) + .flatMapMany(PartialMember::getRoles) + .filter(role -> getConfig().getAdminRoleId().asLong() == role.getId().asLong()) + .filter(Objects::nonNull) + .next() + .hasElement(); + } + + public Mono getLogsChannel() { + return getGuild().flatMap(guild -> guild.getChannelById(getConfig().getLogChannelId())); + } + + public Mono getChatChannel() { + return getGuild().flatMap(guild -> guild.getChannelById(getConfig().getChatChannelId())); + } + + public Mono getGuild() { + return getClient().getGuildById(getConfig().getGuildId()); + } + + public BotConfig getConfig() + { + return config; + } + public String deformat(String input) { return input.replaceAll("([_\\\\`*>|])", "\\\\$1"); diff --git a/Veritas/src/main/java/fns/veritas/client/BotConfig.java b/Veritas/src/main/java/fns/veritas/client/BotConfig.java index 08407ad..c498c4a 100644 --- a/Veritas/src/main/java/fns/veritas/client/BotConfig.java +++ b/Veritas/src/main/java/fns/veritas/client/BotConfig.java @@ -24,7 +24,8 @@ package fns.veritas.client; import discord4j.common.util.Snowflake; -import fns.patchwork.config.WrappedBukkitConfiguration; +import fns.patchwork.config.ConfigType; +import fns.patchwork.config.GenericConfig; import fns.veritas.Aggregate; import fns.veritas.Veritas; import java.io.File; @@ -38,15 +39,22 @@ public class BotConfig { @NonNls - public static final String GUILD_ID = "guild_id"; + private static final String GUILD_ID = "bot_settings.guild_id"; @NonNls - private static final String BOT_TOKEN = "bot_token"; - private final WrappedBukkitConfiguration config; + private static final String BOT_TOKEN = "bot_settings.bot_token"; + @NonNls + private static final String MC_CHANNEL_ID = "bot_settings.mc_channel_id"; + @NonNls + private static final String LOG_CHANNEL_ID = "bot_settings.log_channel_id"; + @NonNls + private static final String INVITE_LINK = "bot_settings.invite_link"; + + + private final GenericConfig config; - public BotConfig(final Veritas plugin) + public BotConfig(final Veritas plugin) throws IOException { - this.config = new WrappedBukkitConfiguration(f0(plugin), - new File(plugin.getDataFolder(), "config.yml")); + this.config = new GenericConfig(ConfigType.TOML, plugin, "config.toml"); } public String getToken() @@ -54,29 +62,29 @@ public String getToken() return config.getString(BOT_TOKEN); } - public String getPrefix() + public Snowflake getGuildId() { - return config.getString("bot_prefix"); + return Snowflake.of(config.getLong(GUILD_ID)); } - public Snowflake getId() + public Snowflake getChatChannelId() { - return Snowflake.of(config.getString(GUILD_ID)); + return Snowflake.of(config.getLong(MC_CHANNEL_ID)); } - public Snowflake getChatChannelId() + public Snowflake getLogChannelId() { - return Snowflake.of(config.getString("channel_id")); + return Snowflake.of(config.getLong(LOG_CHANNEL_ID)); } - public Snowflake getLogChannelId() + public Snowflake getAdminRoleId() { - return Snowflake.of(config.getString("log_channel_id")); + return Snowflake.of(config.getLong("admin_settings.admin_role_id")); } public String getInviteLink() { - return config.getString("invite_link"); + return config.getString(INVITE_LINK); } private Function f0(final Veritas plugin) @@ -94,11 +102,10 @@ private Function f0(final Veritas plugin) catch (IOException | InvalidConfigurationException ex) { fc.addDefault(BOT_TOKEN, "token"); - fc.addDefault("bot_prefix", "!"); - fc.addDefault(GUILD_ID, GUILD_ID); - fc.addDefault("channel_id", "nil"); - fc.addDefault("log_channel_id", "nil"); - fc.addDefault("invite_link", "https://discord.gg/invite"); + fc.addDefault(GUILD_ID, 0); + fc.addDefault(MC_CHANNEL_ID, 0); + fc.addDefault(LOG_CHANNEL_ID, 0); + fc.addDefault(INVITE_LINK, "https://discord.gg/invite"); fc.options().copyDefaults(true); diff --git a/Veritas/src/main/java/fns/veritas/cmd/BanCommand.java b/Veritas/src/main/java/fns/veritas/cmd/BanCommand.java new file mode 100644 index 0000000..564a5fe --- /dev/null +++ b/Veritas/src/main/java/fns/veritas/cmd/BanCommand.java @@ -0,0 +1,98 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and 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 fns.veritas.cmd; + + +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.object.command.ApplicationCommandInteractionOption; +import discord4j.core.object.command.ApplicationCommandInteractionOptionValue; +import discord4j.core.object.entity.User; +import fns.patchwork.base.Shortcuts; +import fns.veritas.Veritas; +import fns.veritas.cmd.base.BotCommand; +import java.time.Duration; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import reactor.core.publisher.Mono; + +public class BanCommand implements BotCommand +{ + + + @Override + public String getName() + { + return "ban"; + } + + @Override + public Mono handle(ChatInputInteractionEvent event) + { + final String playerName = event.getOption("player") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + final String reason = event.getOption("reason") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + final Duration duration = event.getOption("duration") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asLong) + .map(Duration::ofMinutes) + .orElse(Duration.ofMinutes(5)); + + final User user = event.getInteraction().getUser(); + return Shortcuts.provideModule(Veritas.class) + .getAggregate() + .getBot() + .isAdmin(user) + .doOnSuccess(b -> + { + if (Boolean.FALSE.equals(b)) + return; + + final Player player = Bukkit.getPlayer(playerName); + if (player == null) + { + event.reply() + .withEphemeral(true) + .withContent("Player not found") + .block(); + return; + } + + player.ban(reason, duration, user.getUsername()); + event.reply() + .withContent("Kicked " + playerName) + .withEphemeral(true) + .block(); + + event.getInteractionResponse() + .createFollowupMessage(user.getUsername() + ": Kicked " + playerName) + .then(); + }) + .then(); + } +} diff --git a/Veritas/src/main/java/fns/veritas/cmd/KickCommand.java b/Veritas/src/main/java/fns/veritas/cmd/KickCommand.java new file mode 100644 index 0000000..22e5e11 --- /dev/null +++ b/Veritas/src/main/java/fns/veritas/cmd/KickCommand.java @@ -0,0 +1,90 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and 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 fns.veritas.cmd; + +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.object.command.ApplicationCommandInteractionOption; +import discord4j.core.object.command.ApplicationCommandInteractionOptionValue; +import discord4j.core.object.entity.User; +import fns.patchwork.base.Shortcuts; +import fns.patchwork.kyori.MiniMessageWrapper; +import fns.veritas.Veritas; +import fns.veritas.cmd.base.BotCommand; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import reactor.core.publisher.Mono; + +public class KickCommand implements BotCommand +{ + @Override + public String getName() + { + return "kick"; + } + + @Override + public Mono handle(ChatInputInteractionEvent event) + { + final String playerName = event.getOption("player") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + final String reason = event.getOption("reason") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + + final User user = event.getInteraction().getUser(); + return Shortcuts.provideModule(Veritas.class) + .getAggregate() + .getBot() + .isAdmin(user) + .doOnSuccess(b -> + { + if (Boolean.FALSE.equals(b)) + return; + + final Player player = Bukkit.getPlayer(playerName); + if (player == null) + { + event.reply() + .withEphemeral(true) + .withContent("Player not found") + .block(); + return; + } + + player.kick(MiniMessageWrapper.deserialize(true, reason)); + event.reply() + .withContent("Kicked " + playerName) + .withEphemeral(true) + .block(); + + event.getInteractionResponse() + .createFollowupMessage(user.getUsername() + ": Kicked " + playerName) + .then(); + }) + .then(); + } +} diff --git a/Veritas/src/main/java/fns/veritas/cmd/WhisperCommand.java b/Veritas/src/main/java/fns/veritas/cmd/WhisperCommand.java new file mode 100644 index 0000000..2717910 --- /dev/null +++ b/Veritas/src/main/java/fns/veritas/cmd/WhisperCommand.java @@ -0,0 +1,75 @@ +/* + * This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite + * Copyright (C) 2023 Simplex Development and 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 fns.veritas.cmd; + +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.object.command.ApplicationCommandInteractionOption; +import discord4j.core.object.command.ApplicationCommandInteractionOptionValue; +import fns.patchwork.kyori.MiniMessageWrapper; +import fns.veritas.cmd.base.BotCommand; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import reactor.core.publisher.Mono; + +public class WhisperCommand implements BotCommand +{ + @Override + public String getName() + { + return "whisper"; + } + + @Override + public Mono handle(ChatInputInteractionEvent event) + { + final String player = event.getOption("player") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + + final String message = event.getOption("message") + .flatMap(ApplicationCommandInteractionOption::getValue) + .map(ApplicationCommandInteractionOptionValue::asString) + .orElseThrow(); + final Component c = MiniMessageWrapper.deserialize(true, + "[Whisper] " + + event.getInteraction().getUser().getUsername() + + ": " + + message); + + final Player actual = Bukkit.getPlayer(player); + if (actual == null) { + return event.reply("Player not found!") + .withEphemeral(true) + .then(); + } + + actual.sendMessage(c); + + return event.reply("Sent!") + .withEphemeral(true) + .then(); + } +} diff --git a/Veritas/src/main/resources/commands/ban.json b/Veritas/src/main/resources/commands/ban.json new file mode 100644 index 0000000..cddf194 --- /dev/null +++ b/Veritas/src/main/resources/commands/ban.json @@ -0,0 +1,24 @@ +{ + "name": "ban", + "description": "Bans a user from the server.", + "options": [ + { + "name": "player", + "description": "The player to ban.", + "type": 3, + "required": true + }, + { + "name": "reason", + "description": "The reason for the ban.", + "type": 3, + "required": true + }, + { + "name": "duration", + "description": "The duration of the ban, in minutes. Default is 5 minutes.", + "type": 4, + "required": false + } + ] +} \ No newline at end of file diff --git a/Veritas/src/main/resources/commands/kick.json b/Veritas/src/main/resources/commands/kick.json new file mode 100644 index 0000000..9129e21 --- /dev/null +++ b/Veritas/src/main/resources/commands/kick.json @@ -0,0 +1,18 @@ +{ + "name": "kick", + "description": "Kicks a user from the server", + "options": [ + { + "name": "player", + "type": 3, + "description": "The player to kick", + "required": true + }, + { + "name": "reason", + "type": 3, + "description": "The reason for kicking the player", + "required": true + } + ] +} \ No newline at end of file diff --git a/Veritas/src/main/resources/commands/whisper.json b/Veritas/src/main/resources/commands/whisper.json new file mode 100644 index 0000000..c6d26ac --- /dev/null +++ b/Veritas/src/main/resources/commands/whisper.json @@ -0,0 +1,18 @@ +{ + "name": "whisper", + "description": "Whisper to a user.", + "options": [ + { + "name": "player", + "type": 3, + "description": "The in-game user to whisper to.", + "required": true + }, + { + "name": "message", + "type": 3, + "description": "The message to send.", + "required": true + } + ] +} \ No newline at end of file diff --git a/Veritas/src/main/resources/config.toml b/Veritas/src/main/resources/config.toml new file mode 100644 index 0000000..5aa33e2 --- /dev/null +++ b/Veritas/src/main/resources/config.toml @@ -0,0 +1,10 @@ +[bot_settings] +bot_token = "xyz-123-REPLACE-ME" +invite_link = "https://discord.gg/invite" +guild_id = 0 +mc_channel_id = 0 +log_channel_id = 0 + +[admin_settings] +# This role will be able to use the /kick and /ban commands. +admin_role_id = 0 \ No newline at end of file