Fully implement Discord bot

This commit is contained in:
Paul Reilly 2023-08-28 01:53:48 -05:00
parent a676207afa
commit c71ab845b9
33 changed files with 892 additions and 65 deletions

View File

@ -8,6 +8,8 @@ repositories {
dependencies { dependencies {
library 'io.projectreactor:reactor-core:3.5.4' library 'io.projectreactor:reactor-core:3.5.4'
library 'io.github.classgraph:classgraph:4.8.162' library 'io.github.classgraph:classgraph:4.8.162'
library 'org.tomlj:tomlj:1.1.0'
library 'com.google.code.gson:gson:2.8.9'
api 'org.slf4j:slf4j-api:1.7.36' api 'org.slf4j:slf4j-api:1.7.36'
testImplementation platform('org.junit:junit-bom:5.9.1') testImplementation platform('org.junit:junit-bom:5.9.1')

View File

@ -23,12 +23,12 @@
package fns.patchwork.base; package fns.patchwork.base;
import fns.patchwork.data.ConfigRegistry; import fns.patchwork.registry.ConfigRegistry;
import fns.patchwork.data.EventRegistry; import fns.patchwork.registry.EventRegistry;
import fns.patchwork.data.GroupRegistry; import fns.patchwork.registry.GroupRegistry;
import fns.patchwork.data.ModuleRegistry; import fns.patchwork.registry.ModuleRegistry;
import fns.patchwork.data.ServiceTaskRegistry; import fns.patchwork.registry.ServiceTaskRegistry;
import fns.patchwork.data.UserRegistry; import fns.patchwork.registry.UserRegistry;
/** /**
* This class is a holder for each registry in the data package. * This class is a holder for each registry in the data package.

View File

@ -23,9 +23,8 @@
package fns.patchwork.config; package fns.patchwork.config;
import fns.patchwork.api.Context; import fns.patchwork.provider.Context;
import fns.patchwork.provider.ContextProvider; import fns.patchwork.provider.ContextProvider;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Unmodifiable;
import java.io.File; import java.io.File;

View File

@ -0,0 +1,184 @@
/*
* This file is part of Freedom-Network-Suite - https://github.com/AtlasMediaGroup/Freedom-Network-Suite
* Copyright (C) 2023 Total Freedom Server Network 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 java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.Unmodifiable;
import org.tomlj.Toml;
import org.tomlj.TomlParseResult;
// TODO: Finish implementation
public class WrappedTomlConfiguration implements Configuration
{
private final Map<String, Object> previousValues = new HashMap<>();
private final TomlParseResult toml;
private final File file;
public WrappedTomlConfiguration(final JavaPlugin plugin, final File file) throws IOException
{
if (!file.exists() && file.createNewFile())
{
plugin.saveResource(file.getName(), true);
}
this.toml = Toml.parse(Path.of(file.toURI()));
this.file = file;
}
@Override
public void save() throws IOException
{
// Create a backup file
final File backup = new File(this.file.getParentFile(), this.file.getName() + ".bak");
if (backup.exists() && !Files.deleteIfExists(Path.of(backup.toURI())))
{
throw new IOException("Failed to delete existing backup file: " + backup.getName());
}
// Serialize the current configuration to a temporary file
final File tempFile = new File(this.file.getParentFile(), this.file.getName() + ".temp");
try (FileWriter tempFileWriter = new FileWriter(tempFile))
{
// Convert the updated TomlTable to TOML format and write it to the temporary file
String tomlString = this.toml.toToml();
tempFileWriter.write(tomlString);
}
// Compare the new configuration with the previous one
TomlParseResult newToml = Toml.parse(Path.of(tempFile.toURI()));
for (Map.Entry<String, Object> entry : newToml.toMap().entrySet())
{
String key = entry.getKey();
Object newValue = entry.getValue();
Object oldValue = previousValues.get(key);
if (oldValue == null || !oldValue.equals(newValue))
{
// Value has changed, update it
this.toml.toMap().replace(key, newValue);
previousValues.put(key, newValue);
}
}
// Save the updated configuration to the original file
try (FileWriter fileWriter = new FileWriter(this.file))
{
// Convert the updated TomlTable to TOML format and write it to the original file
String tomlString = this.toml.toToml();
fileWriter.write(tomlString);
}
// Delete the temporary file and the backup file
Files.delete(Path.of(tempFile.toURI()));
Files.delete(Path.of(backup.toURI()));
}
@Override
public void load() throws IOException
{
// TODO: Implement
}
@Override
public String getFileName()
{
return null;
}
@Override
public File getConfigurationFile()
{
return null;
}
@Override
public String getString(String path)
{
return null;
}
@Override
public boolean getBoolean(String path)
{
return false;
}
@Override
public @Unmodifiable <T> List<T> getList(String path, Class<T> clazz)
{
return null;
}
@Override
public @Unmodifiable List<String> getStringList(String path)
{
return null;
}
@Override
public int getInt(String path)
{
return 0;
}
@Override
public long getLong(String path)
{
return 0;
}
@Override
public double getDouble(String path)
{
return 0;
}
@Override
public <T> void set(String path, T value)
{
// TODO: Implement
}
@Override
public <T> Optional<T> get(String path, Class<T> clazz)
{
return Optional.empty();
}
@Override
public <T> T getOrDefault(String path, Class<T> clazz, T fallback)
{
return null;
}
}

View File

@ -23,7 +23,7 @@
package fns.patchwork.event; package fns.patchwork.event;
import fns.patchwork.api.Context; import fns.patchwork.provider.Context;
import fns.patchwork.base.Patchwork; import fns.patchwork.base.Patchwork;
import fns.patchwork.service.Service; import fns.patchwork.service.Service;
import java.util.HashSet; import java.util.HashSet;

View File

@ -23,7 +23,6 @@
package fns.patchwork.particle; package fns.patchwork.particle;
import fns.patchwork.api.Interpolator;
import fns.patchwork.utils.InterpolationUtils; import fns.patchwork.utils.InterpolationUtils;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -101,7 +100,7 @@ public interface Trail
* @see #getColor() * @see #getColor()
* @see Particle * @see Particle
* @see InterpolationUtils * @see InterpolationUtils
* @see Interpolator * @see InterpolationUtils.Interpolator
*/ */
@Nullable @Nullable
Set<Color> getColors(); Set<Color> getColors();

View File

@ -21,9 +21,8 @@
* SOFTWARE. * SOFTWARE.
*/ */
package fns.patchwork.api; package fns.patchwork.provider;
import fns.patchwork.provider.ContextProvider;
import java.util.function.Function; import java.util.function.Function;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.bukkit.Location; import org.bukkit.Location;

View File

@ -21,7 +21,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
package fns.patchwork.data; package fns.patchwork.registry;
import fns.patchwork.config.Configuration; import fns.patchwork.config.Configuration;
import java.util.HashMap; import java.util.HashMap;

View File

@ -21,7 +21,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
package fns.patchwork.data; package fns.patchwork.registry;
import fns.patchwork.event.FEvent; import fns.patchwork.event.FEvent;
import fns.patchwork.provider.EventProvider; import fns.patchwork.provider.EventProvider;

View File

@ -21,7 +21,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
package fns.patchwork.data; package fns.patchwork.registry;
import fns.patchwork.permissible.Group; import fns.patchwork.permissible.Group;
import java.util.ArrayList; import java.util.ArrayList;

View File

@ -21,7 +21,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
package fns.patchwork.data; package fns.patchwork.registry;
import fns.patchwork.provider.ModuleProvider; import fns.patchwork.provider.ModuleProvider;
import java.util.ArrayList; import java.util.ArrayList;

View File

@ -21,7 +21,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
package fns.patchwork.data; package fns.patchwork.registry;
import fns.patchwork.service.Service; import fns.patchwork.service.Service;
import fns.patchwork.service.ServiceSubscription; import fns.patchwork.service.ServiceSubscription;

View File

@ -21,7 +21,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
package fns.patchwork.data; package fns.patchwork.registry;
import fns.patchwork.user.User; import fns.patchwork.user.User;
import fns.patchwork.user.UserData; import fns.patchwork.user.UserData;

View File

@ -21,7 +21,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
package fns.patchwork.api; package fns.patchwork.serializer;
/** /**
* This interface represents a Serializable object. Objects which require custom serialization and cannot simply * This interface represents a Serializable object. Objects which require custom serialization and cannot simply

View File

@ -23,7 +23,6 @@
package fns.patchwork.utils; package fns.patchwork.utils;
import fns.patchwork.api.Interpolator;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
@ -155,4 +154,24 @@ public final class InterpolationUtils
{ {
return componentRGBGradient(length, from, to, InterpolationUtils::linear); return componentRGBGradient(length, from, to, InterpolationUtils::linear);
} }
/**
* Interpolates a range of values and returns the results in a {@link Double} array.
* <br>
* This is a functional interface, to allow for lambda expressions, but also for anonymous custom interpolation
* implementations.
*/
@FunctionalInterface
public static interface Interpolator
{
/**
* Interpolates a range of values and returns the results in a {@link Double} array.
*
* @param from The starting value.
* @param to The ending value.
* @param max The number of values to interpolate.
* @return The interpolated values.
*/
double[] interpolate(final double from, final double to, final int max);
}
} }

View File

@ -24,31 +24,53 @@
package fns.veritas; package fns.veritas;
import fns.patchwork.utils.logging.FNS4J; import fns.patchwork.utils.logging.FNS4J;
import fns.veritas.bukkit.BukkitNative;
import fns.veritas.bukkit.ServerListener;
import fns.veritas.client.BotClient; import fns.veritas.client.BotClient;
import fns.veritas.client.BotConfig; import fns.veritas.client.BotConfig;
import org.bukkit.Bukkit;
public class Aggregate public class Aggregate
{ {
private final FNS4J logger; private static final FNS4J logger = FNS4J.getLogger("Veritas");
private final BotClient bot; private final BotClient bot;
private final Veritas plugin; private final Veritas plugin;
private final BukkitNative bukkitNativeListener;
private final ServerListener serverListener;
public Aggregate(final Veritas plugin) public Aggregate(final Veritas plugin)
{ {
this.plugin = plugin; this.plugin = plugin;
this.logger = FNS4J.getLogger(plugin.getName());
this.bot = new BotClient(new BotConfig(plugin)); this.bot = new BotClient(new BotConfig(plugin));
this.bukkitNativeListener = new BukkitNative(plugin);
this.serverListener = new ServerListener(plugin);
Bukkit.getServer().getPluginManager().registerEvents(this.getBukkitNativeListener(), plugin);
this.getServerListener().minecraftChatBound().subscribe();
} }
public FNS4J getLogger() { public static FNS4J getLogger()
{
return logger; return logger;
} }
public BotClient getBot() { public ServerListener getServerListener()
{
return serverListener;
}
public BukkitNative getBukkitNativeListener()
{
return bukkitNativeListener;
}
public BotClient getBot()
{
return bot; return bot;
} }
public Veritas getPlugin() { public Veritas getPlugin()
{
return plugin; return plugin;
} }
} }

View File

@ -34,8 +34,7 @@ public class Veritas extends JavaPlugin
{ {
this.aggregate = new Aggregate(this); this.aggregate = new Aggregate(this);
getAggregate() Aggregate.getLogger()
.getLogger()
.info("Veritas has been enabled!"); .info("Veritas has been enabled!");
} }

View File

@ -109,7 +109,7 @@ public class BukkitNative implements Listener
if (!plugin.getServer().hasWhitelist() && bot != null) if (!plugin.getServer().hasWhitelist() && bot != null)
{ {
plugin.getAggregate().getBot().messageChatChannel(player.getName() plugin.getAggregate().getBot().messageChatChannel(player.getName()
+ " \u00BB " + " » "
+ message, true); + message, true);
} }
} }

View File

@ -27,6 +27,7 @@ import discord4j.core.event.domain.message.MessageCreateEvent;
import discord4j.core.object.entity.Attachment; import discord4j.core.object.entity.Attachment;
import discord4j.core.object.entity.Member; import discord4j.core.object.entity.Member;
import discord4j.core.object.entity.Message; import discord4j.core.object.entity.Message;
import fns.veritas.Aggregate;
import fns.veritas.Veritas; import fns.veritas.Veritas;
import fns.veritas.client.BotClient; import fns.veritas.client.BotClient;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@ -35,6 +36,8 @@ import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
public class ServerListener public class ServerListener
{ {
@ -48,9 +51,9 @@ public class ServerListener
this.bot = plugin.getAggregate().getBot(); this.bot = plugin.getAggregate().getBot();
} }
public void minecraftChatBound() public Mono<Void> minecraftChatBound()
{ {
bot.getClient() return bot.getClient()
.getEventDispatcher() .getEventDispatcher()
.on(MessageCreateEvent.class) .on(MessageCreateEvent.class)
.filter(m -> m.getMessage() .filter(m -> m.getMessage()
@ -62,8 +65,9 @@ public class ServerListener
.orElseThrow(IllegalAccessError::new) .orElseThrow(IllegalAccessError::new)
.getId() .getId()
.equals(plugin.getAggregate().getBot().getClient().getSelfId())) .equals(plugin.getAggregate().getBot().getClient().getSelfId()))
.doOnError(plugin.getAggregate().getLogger()::error) .doOnError(Aggregate.getLogger()::error)
.subscribe(this::doMessageBodyDetails); .doOnNext(this::doMessageBodyDetails)
.then();
} }
private void doMessageBodyDetails(MessageCreateEvent m) private void doMessageBodyDetails(MessageCreateEvent m)
@ -83,6 +87,14 @@ public class ServerListener
user = user.append(Component.text(member.getDisplayName().trim())); user = user.append(Component.text(member.getDisplayName().trim()));
final TextComponent message = builder(msg);
Bukkit.broadcast(builder.append(prefix, user, message).build());
}
@NotNull
private TextComponent builder(Message msg)
{
TextComponent message = Component.text(": ", NamedTextColor.DARK_GRAY) TextComponent message = Component.text(": ", NamedTextColor.DARK_GRAY)
.append( .append(
Component.text(msg.getContent(), NamedTextColor.WHITE)); Component.text(msg.getContent(), NamedTextColor.WHITE));
@ -102,7 +114,6 @@ public class ServerListener
.clickEvent(ClickEvent.openUrl(attachment.getUrl()))); .clickEvent(ClickEvent.openUrl(attachment.getUrl())));
} }
} }
return message;
Bukkit.broadcast(builder.append(prefix, user, message).build());
} }
} }

View File

@ -23,36 +23,40 @@
package fns.veritas.client; package fns.veritas.client;
import com.google.common.collect.ImmutableList;
import discord4j.common.util.Snowflake; import discord4j.common.util.Snowflake;
import discord4j.core.DiscordClientBuilder; import discord4j.core.DiscordClientBuilder;
import discord4j.core.GatewayDiscordClient; import discord4j.core.GatewayDiscordClient;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.object.entity.Guild; import discord4j.core.object.entity.Guild;
import discord4j.core.object.entity.Message; import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.channel.TextChannel; import discord4j.core.object.entity.channel.TextChannel;
import discord4j.core.spec.MessageCreateSpec; import discord4j.core.spec.MessageCreateSpec;
import fns.veritas.cmd.base.BotCommandHandler;
import java.util.List;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
public class BotClient public class BotClient
{ {
private final GatewayDiscordClient client; private final GatewayDiscordClient client;
private final BotConfig config; private final BotConfig config;
private final ImmutableList<String> DISCORD_SUBDOMAINS; private final List<String> subdomains;
public BotClient(final BotConfig config) public BotClient(final BotConfig config)
{ {
this.config = config; this.config = config;
this.DISCORD_SUBDOMAINS = ImmutableList.of("discordapp.com", "discord.com", "discord.gg"); this.subdomains = List.of("discordapp.com", "discord.com", "discord.gg");
this.client = DiscordClientBuilder.create(config.getToken()) this.client = DiscordClientBuilder.create(config.getToken())
.build() .build()
.login() .login()
.block(); .block();
}
public void validateConnection()
{
if (client == null) if (client == null)
throw new IllegalStateException(); throw new IllegalStateException();
final BotCommandHandler handler = new BotCommandHandler(client.getRestClient());
client.on(ChatInputInteractionEvent.class, handler::handle);
} }
public String getBotId() public String getBotId()
@ -87,14 +91,14 @@ public class BotClient
public void messageChatChannel(String message, boolean system) public void messageChatChannel(String message, boolean system)
{ {
String chat_channel_id = config.getChatChannelId().asString(); String channelID = config.getChatChannelId().asString();
String sanitizedMessage = (system) ? message : sanitizeChatMessage(message); String sanitizedMessage = (system) ? message : sanitizeChatMessage(message);
if (sanitizedMessage.isBlank()) if (sanitizedMessage.isBlank())
return; return;
if (!chat_channel_id.isEmpty()) if (!channelID.isEmpty())
{ {
MessageCreateSpec spec = MessageCreateSpec.builder() MessageCreateSpec spec = MessageCreateSpec.builder()
.content(sanitizedMessage) .content(sanitizedMessage)
@ -124,7 +128,7 @@ public class BotClient
return ""; return "";
} }
for (String subdomain : DISCORD_SUBDOMAINS) for (String subdomain : subdomains)
{ {
if (message.toLowerCase().contains(subdomain + "/invite")) if (message.toLowerCase().contains(subdomain + "/invite"))
{ {

View File

@ -24,8 +24,8 @@
package fns.veritas.client; package fns.veritas.client;
import discord4j.common.util.Snowflake; import discord4j.common.util.Snowflake;
import discord4j.discordjson.Id;
import fns.patchwork.config.WrappedBukkitConfiguration; import fns.patchwork.config.WrappedBukkitConfiguration;
import fns.veritas.Aggregate;
import fns.veritas.Veritas; import fns.veritas.Veritas;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -108,7 +108,7 @@ public class BotConfig
} }
catch (IOException e) catch (IOException e)
{ {
plugin.getAggregate().getLogger().error(e); Aggregate.getLogger().error(e);
} }
} }
} }

View File

@ -0,0 +1,69 @@
/*
* This file is part of Freedom-Network-Suite - https://github.com/AtlasMediaGroup/Freedom-Network-Suite
* Copyright (C) 2023 Total Freedom Server Network 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 fns.veritas.cmd.base.BotCommand;
import fns.veritas.messaging.Embed;
import fns.veritas.messaging.EmbedWrapper;
import java.util.ArrayList;
import java.util.List;
import reactor.core.publisher.Mono;
public class HelpCommand implements BotCommand
{
@Override
public String getName()
{
return "help";
}
@Override
public Mono<Void> handle(ChatInputInteractionEvent event)
{
final List<Embed> content = new ArrayList<>();
final EmbedWrapper e = new EmbedWrapper();
content.add(embedContent("help",
"Shows this message. \n" +
"Use /help info to see information about the server.",
false));
content.add(embedContent("tps",
"Shows the server's current TPS.",
false));
content.add(embedContent("list",
"Shows a list of all online players. \n" +
"Use /list staff to show online staff.",
false));
e.quickEmbed("Command List:",
"A list of all currently supported commands",
content);
return event.reply()
.withContent("Here is a list of all currently supported commands:")
.withEmbeds(e.getEmbeds())
.withEphemeral(true)
.then();
}
}

View File

@ -0,0 +1,102 @@
/*
* This file is part of Freedom-Network-Suite - https://github.com/AtlasMediaGroup/Freedom-Network-Suite
* Copyright (C) 2023 Total Freedom Server Network 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.PlainTextWrapper;
import fns.veritas.cmd.base.BotCommand;
import fns.veritas.messaging.Embed;
import fns.veritas.messaging.EmbedWrapper;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Bukkit;
import reactor.core.publisher.Mono;
public class ListCommand implements BotCommand
{
@Override
public String getName()
{
return "list";
}
@Override
public Mono<Void> handle(final ChatInputInteractionEvent event)
{
final boolean showStaff = event.getOption("staff")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asBoolean)
.orElse(false);
if (showStaff)
return staffList(event);
final EmbedWrapper e = new EmbedWrapper();
final List<Embed> embeds = new ArrayList<>();
Bukkit.getOnlinePlayers()
.forEach(player ->
{
final String display = PlainTextWrapper.toPlainText(player.displayName());
final String actual = PlainTextWrapper.toPlainText(player.name());
final Embed embed = new Embed(display, actual, false);
embeds.add(embed);
});
e.quickEmbed("Player List", "List of currently online players:", embeds);
return event.reply()
.withEmbeds(e.getEmbeds())
.withEphemeral(true)
.then();
}
private Mono<Void> staffList(final ChatInputInteractionEvent event)
{
final EmbedWrapper wrapper = new EmbedWrapper();
final List<Embed> embeds = new ArrayList<>();
Bukkit.getOnlinePlayers()
.stream()
.filter(player -> player.hasPermission("fns.marker.staff"))
.forEach(player ->
{
final String display = PlainTextWrapper.toPlainText(player.displayName());
final String actual = PlainTextWrapper.toPlainText(player.name());
final Embed embed = new Embed(display, actual, false);
embeds.add(embed);
});
wrapper.quickEmbed("Staff List", "List of currently online staff members:", embeds);
return event.reply()
.withEmbeds(wrapper.getEmbeds())
.withEphemeral(true)
.then();
}
}

View File

@ -0,0 +1,64 @@
/*
* This file is part of Freedom-Network-Suite - https://github.com/AtlasMediaGroup/Freedom-Network-Suite
* Copyright (C) 2023 Total Freedom Server Network 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 fns.veritas.cmd.base.BotCommand;
import fns.veritas.messaging.Embed;
import fns.veritas.messaging.EmbedWrapper;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Bukkit;
import reactor.core.publisher.Mono;
public class TpsCommand implements BotCommand
{
@Override
public String getName()
{
return "tps";
}
@Override
public Mono<Void> handle(ChatInputInteractionEvent event)
{
final double[] tps = Bukkit.getServer().getTPS();
final EmbedWrapper e = new EmbedWrapper();
final List<Embed> embeds = new ArrayList<>();
embeds.add(embedContent("1 Minute:", String.valueOf(tps[0]), false));
embeds.add(embedContent("5 Minutes:", String.valueOf(tps[1]), false));
embeds.add(embedContent("15 Minutes:", String.valueOf(tps[2]), false));
e.quickEmbed("Server TPS:",
"Current TPS (1m, 5m, 15m)",
embeds);
return event.reply()
.withEmbeds(e.getEmbeds())
.withEphemeral(true)
.then();
}
}

View File

@ -21,24 +21,23 @@
* SOFTWARE. * SOFTWARE.
*/ */
package fns.patchwork.api; package fns.veritas.cmd.base;
/** import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
* Interpolates a range of values and returns the results in a {@link Double} array. import fns.patchwork.utils.container.Trio;
* <br> import fns.veritas.messaging.Embed;
* This is a functional interface, to allow for lambda expressions, but also for anonymous custom interpolation import reactor.core.publisher.Mono;
* implementations.
*/ public interface BotCommand
@FunctionalInterface
public interface Interpolator
{ {
/** String getName();
* Interpolates a range of values and returns the results in a {@link Double} array.
* Mono<Void> handle(final ChatInputInteractionEvent event);
* @param from The starting value.
* @param to The ending value. default Embed embedContent(final String field,
* @param max The number of values to interpolate. final String value,
* @return The interpolated values. final boolean inline)
*/ {
double[] interpolate(final double from, final double to, final int max); return new Embed(field, value, inline);
}
} }

View File

@ -0,0 +1,139 @@
/*
* This file is part of Freedom-Network-Suite - https://github.com/AtlasMediaGroup/Freedom-Network-Suite
* Copyright (C) 2023 Total Freedom Server Network 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.base;
import discord4j.common.JacksonResources;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.discordjson.json.ApplicationCommandRequest;
import discord4j.rest.RestClient;
import discord4j.rest.service.ApplicationService;
import fns.patchwork.utils.logging.FNS4J;
import fns.veritas.Veritas;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class BotCommandHandler
{
private final List<BotCommand> commands = new ArrayList<>();
private final RestClient restClient;
public BotCommandHandler(final RestClient restClient)
{
this.restClient = restClient;
}
public void registerFromPluginDirectory(final Veritas plugin) throws IOException
{
final List<String> jsonFiles = new ArrayList<>();
final File commandsFolder = new File(plugin.getDataFolder(), "commands");
if (!commandsFolder.exists() && commandsFolder.mkdirs())
{
FNS4J.getLogger("Veritas").info("Created cmds folder. Copying default cmds...");
plugin.saveResource("commands/", true);
}
final File[] files = commandsFolder.listFiles();
if (files == null)
throw new IOException("Commands folder is empty or is not a valid directory!");
Stream.of(files)
.map(File::getName)
.filter(name -> name.endsWith(".json"))
.forEach(jsonFiles::add);
final JacksonResources d4jMapper = JacksonResources.create();
final ApplicationService applicationService = restClient.getApplicationService();
final long applicationId = Objects.requireNonNull(restClient.getApplicationId().block());
final List<ApplicationCommandRequest> cmds = new ArrayList<>();
for (final String json : getCommandsJson(plugin, jsonFiles))
{
final ApplicationCommandRequest request = d4jMapper.getObjectMapper()
.readValue(json, ApplicationCommandRequest.class);
cmds.add(request);
}
applicationService.bulkOverwriteGlobalApplicationCommand(applicationId, cmds)
.doOnNext(cmd -> Bukkit.getLogger().info("Successfully registered Global Command "
+ cmd.name()))
.doOnError(e -> Bukkit.getLogger().severe("Failed to register global cmds.\n"
+ e.getMessage()))
.subscribe();
}
private @NotNull List<String> getCommandsJson(final JavaPlugin plugin, final List<String> fileNames) throws IOException
{
final String commandsFolderName = "commands/";
final URL url = this.getClass().getClassLoader().getResource(commandsFolderName);
Objects.requireNonNull(url, commandsFolderName + " could not be found");
final List<String> list = new ArrayList<>();
for (final String file : fileNames)
{
final String resourceFileAsString = getResourceFileAsString(plugin, commandsFolderName + file);
list.add(Objects.requireNonNull(resourceFileAsString, "Command file not found: " + file));
}
return list;
}
private @Nullable String getResourceFileAsString(final JavaPlugin plugin, final String fileName) throws IOException
{
try (final InputStream resourceAsStream = plugin.getResource(fileName))
{
if (resourceAsStream == null)
return null;
try (final InputStreamReader inputStreamReader = new InputStreamReader(resourceAsStream);
final BufferedReader reader = new BufferedReader(inputStreamReader))
{
return reader.lines().collect(Collectors.joining(System.lineSeparator()));
}
}
}
public Mono<Void> handle(final ChatInputInteractionEvent event)
{
return Flux.fromIterable(commands)
.filter(cmd -> cmd.getName().equals(event.getCommandName()))
.next()
.flatMap(cmd -> cmd.handle(event));
}
}

View File

@ -0,0 +1,28 @@
/*
* This file is part of Freedom-Network-Suite - https://github.com/AtlasMediaGroup/Freedom-Network-Suite
* Copyright (C) 2023 Total Freedom Server Network 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.messaging;
public record Embed(String fieldName, String value, boolean inline)
{
}

View File

@ -0,0 +1,63 @@
/*
* This file is part of Freedom-Network-Suite - https://github.com/AtlasMediaGroup/Freedom-Network-Suite
* Copyright (C) 2023 Total Freedom Server Network 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.messaging;
import discord4j.core.spec.EmbedCreateSpec;
import java.util.ArrayList;
import java.util.List;
public class EmbedWrapper
{
private final List<EmbedCreateSpec> embeds = new ArrayList<>();
public List<EmbedCreateSpec> getEmbeds()
{
return embeds;
}
public void addEmbed(final EmbedCreateSpec embed)
{
this.embeds.add(embed);
}
public EmbedCreateSpec.Builder create()
{
return EmbedCreateSpec.builder();
}
public void quickEmbed(final String title,
final String description,
final List<Embed> content)
{
final EmbedCreateSpec.Builder builder = create()
.title(title)
.description(description);
content.forEach(t -> builder.addField(t.fieldName(),
t.value(),
t.inline()));
addEmbed(builder.build());
}
}

View File

@ -0,0 +1,72 @@
/*
* This file is part of Freedom-Network-Suite - https://github.com/AtlasMediaGroup/Freedom-Network-Suite
* Copyright (C) 2023 Total Freedom Server Network 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.messaging;
import discord4j.core.object.component.LayoutComponent;
import discord4j.core.spec.MessageCreateFields;
import discord4j.core.spec.MessageCreateSpec;
import discord4j.rest.util.AllowedMentions;
public class SimpleMessageWrapper
{
private final MessageCreateSpec.Builder spec;
public SimpleMessageWrapper()
{
this.spec = MessageCreateSpec.builder();
}
public void setContent(final String content)
{
this.spec.content(content);
}
public void setEmbeds(final EmbedWrapper embed)
{
this.spec.addAllEmbeds(embed.getEmbeds());
}
public void setAttachments(final MessageCreateFields.File... files)
{
this.spec.addFiles(files);
}
public void setSpoilerAttachments(final MessageCreateFields.FileSpoiler... files)
{
this.spec.addFileSpoilers(files);
}
public void setAllowedMentions(final AllowedMentions allowedMentions)
{
this.spec.allowedMentions(allowedMentions);
}
public void setLayoutComponents(final LayoutComponent... components)
{
for (final LayoutComponent component : components)
{
this.spec.addComponent(component);
}
}
}

View File

@ -0,0 +1,33 @@
{
"name": "",
"description": "",
"options": [
{
"name": "",
"description": "",
"type": 3,
"required": true
}
]
}
# <-- Types --> #
1 -> Sub Command
2 -> Sub Command Group
3 -> String
4 -> Integer
5 -> Boolean
6 -> User
7 -> Channel
8 -> Role
9 -> Mentionable
10 -> Number
# <-- Choices --> #
(From the official documentation)
Choices can be defined on the STRING, INTEGER, and NUMBER option types.
Choices are preset values the user can pick when selecting the option that contains them.
CAUTION
If you specify choices for an option, these are the only valid values a user may pick.

View File

@ -0,0 +1,4 @@
{
"name": "help",
"description": "Shows a list of commands."
}

View File

@ -0,0 +1,12 @@
{
"name": "list",
"description": "List all players on the server.",
"options": [
{
"type": 5,
"name": "staff",
"description": "Show only staff members currently online.",
"required": false
}
]
}

View File

@ -0,0 +1,4 @@
{
"name": "tps",
"description": "Shows the current server TPS."
}