diff --git a/DiscordBot/pom.xml b/DiscordBot/pom.xml index 8e2ef60..f8ae5b0 100644 --- a/DiscordBot/pom.xml +++ b/DiscordBot/pom.xml @@ -11,10 +11,11 @@ - 8 + 17 ${java.version} ${java.version} UTF-8 + 2.1.0-alpha1 @@ -100,6 +101,8 @@ 1.18.32 provided + + net.dv8tion JDA @@ -117,5 +120,19 @@ 1.0.0 compile + + + + org.slf4j + slf4j-api + ${slf4j.version} + compile + + + org.slf4j + slf4j-simple + ${slf4j.version} + compile + \ No newline at end of file diff --git a/DiscordBot/src/main/java/cc/restfulmc/bot/DiscordBot.java b/DiscordBot/src/main/java/cc/restfulmc/bot/DiscordBot.java index 03b8c64..809ea37 100644 --- a/DiscordBot/src/main/java/cc/restfulmc/bot/DiscordBot.java +++ b/DiscordBot/src/main/java/cc/restfulmc/bot/DiscordBot.java @@ -1,8 +1,62 @@ package cc.restfulmc.bot; +import cc.restfulmc.bot.command.CommandManager; +import cc.restfulmc.sdk.client.ClientConfig; +import cc.restfulmc.sdk.client.RESTfulMCClient; +import lombok.Getter; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.entities.SelfUser; + /** * @author Braydon */ +@Slf4j(topic = "RESTfulMC Bot") @Getter public final class DiscordBot { - public static void main(String[] args) { } + /** + * The JDA bot instance. + */ + private JDA jda; + + @SneakyThrows + public DiscordBot() { + String token = System.getenv("BOT_TOKEN"); + if (token == null) { // Missing BOT_TOKEN + throw new NullPointerException("Missing BOT_TOKEN environment variable"); + } + jda = JDABuilder.createLight(token) + .setActivity(Activity.watching("Minecraft servers")) + .build(); + jda.awaitReady(); // Wait for JDA to become ready + + // Setup the API SDK + RESTfulMCClient apiClient = new RESTfulMCClient(ClientConfig.defaultConfig()); + + // Commands + new CommandManager(this, apiClient); + + SelfUser self = jda.getSelfUser(); + log.info("Logged in as bot {} ({})", self.getAsTag(), self.getId()); + + // Add a cleanup hook to cleanup the bot when the JVM shuts down + Runtime.getRuntime().addShutdownHook(new Thread(this::cleanup)); + } + + /** + * Cleanup the bot. + */ + public void cleanup() { + log.info("Cleaning up..."); + jda.shutdown(); + jda = null; + log.info("Goodbye!"); + } + + public static void main(@NonNull String[] args) { + new DiscordBot(); + } } \ No newline at end of file diff --git a/DiscordBot/src/main/java/cc/restfulmc/bot/command/CommandManager.java b/DiscordBot/src/main/java/cc/restfulmc/bot/command/CommandManager.java new file mode 100644 index 0000000..06d70e9 --- /dev/null +++ b/DiscordBot/src/main/java/cc/restfulmc/bot/command/CommandManager.java @@ -0,0 +1,55 @@ +package cc.restfulmc.bot.command; + +import cc.restfulmc.bot.DiscordBot; +import cc.restfulmc.bot.command.impl.PlayerCommand; +import cc.restfulmc.sdk.client.RESTfulMCClient; +import lombok.NonNull; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Braydon + */ +public final class CommandManager extends ListenerAdapter { + private final List commands = Collections.synchronizedList(new ArrayList<>()); + + public CommandManager(@NonNull DiscordBot bot, @NonNull RESTfulMCClient apiClient) { + registerCommand(new PlayerCommand(apiClient)); + + // Update the commands on Discord + CommandListUpdateAction updateCommands = bot.getJda().updateCommands(); + for (SlashCommand command : commands) { + updateCommands.addCommands(Commands.slash(command.getName(), command.getDescription()).addOptions(command.getOptions())); + } + updateCommands.queue(); + + // Handle registered events + bot.getJda().addEventListener(this); + } + + @Override + public void onSlashCommandInteraction(@NonNull SlashCommandInteractionEvent event) { + for (SlashCommand command : commands) { + if (command.getName().equals(event.getName())) { + event.deferReply().queue(); // Inform Discord we received the command + command.onExecute(event.getUser(), event.getMember(), event); // Invoke the command + break; + } + } + } + + /** + * Register a slash command. + * + * @param command the command to register + */ + public void registerCommand(@NonNull SlashCommand command) { + commands.add(command); + } +} \ No newline at end of file diff --git a/DiscordBot/src/main/java/cc/restfulmc/bot/command/SlashCommand.java b/DiscordBot/src/main/java/cc/restfulmc/bot/command/SlashCommand.java new file mode 100644 index 0000000..97777a3 --- /dev/null +++ b/DiscordBot/src/main/java/cc/restfulmc/bot/command/SlashCommand.java @@ -0,0 +1,62 @@ +package cc.restfulmc.bot.command; + +import cc.restfulmc.sdk.exception.RESTfulMCAPIException; +import lombok.Getter; +import lombok.NonNull; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +/** + * A wrapper for slash commands. + * + * @author Braydon + */ +@Getter +public abstract class SlashCommand { + /** + * The name of this command. + */ + @NonNull private final String name; + + /** + * The description of this command. + */ + @NonNull private final String description; + + /** + * Optional options for this command. + */ + private final OptionData[] options; + + public SlashCommand(@NonNull String name, @NonNull String description, OptionData... options) { + this.name = name; + this.description = description; + this.options = options; + } + + /** + * Invoked when this command is executed. + * + * @param user the executing user + * @param member the executing member, null if in dms + * @param event the event that triggered this command + */ + public abstract void onExecute(@NonNull User user, Member member, @NonNull SlashCommandInteractionEvent event); + + /** + * Reply to an interaction with an API error. + * + * @param event the event to reply to + * @param apiError the api error to reply with + */ + protected final void replyWithApiError(@NonNull SlashCommandInteractionEvent event, @NonNull RESTfulMCAPIException apiError) { + event.getHook().sendMessageEmbeds(new EmbedBuilder() + .setColor(0xAA0000) + .setTitle(apiError.getCode() + " | API Error") + .setDescription(apiError.getMessage()) + .build()).queue(); + } +} \ No newline at end of file diff --git a/DiscordBot/src/main/java/cc/restfulmc/bot/command/impl/PlayerCommand.java b/DiscordBot/src/main/java/cc/restfulmc/bot/command/impl/PlayerCommand.java new file mode 100644 index 0000000..4b8a73a --- /dev/null +++ b/DiscordBot/src/main/java/cc/restfulmc/bot/command/impl/PlayerCommand.java @@ -0,0 +1,70 @@ +package cc.restfulmc.bot.command.impl; + +import cc.restfulmc.bot.command.SlashCommand; +import cc.restfulmc.sdk.client.RESTfulMCClient; +import cc.restfulmc.sdk.exception.RESTfulMCAPIException; +import cc.restfulmc.sdk.response.Player; +import lombok.NonNull; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +/** + * @author Braydon + */ +public final class PlayerCommand extends SlashCommand { + /** + * The API client to use for lookups. + */ + @NonNull private final RESTfulMCClient apiClient; + + public PlayerCommand(@NonNull RESTfulMCClient apiClient) { + super("player", "Lookup a player by their username or UUID", + new OptionData(OptionType.STRING, "query", "The player username or UUID").setRequired(true) + ); + this.apiClient = apiClient; + } + + /** + * Invoked when this command is executed. + * + * @param user the executing user + * @param member the executing member, null if in dms + * @param event the event that triggered this command + */ + @Override + public void onExecute(@NonNull User user, Member member, @NonNull SlashCommandInteractionEvent event) { + OptionMapping query = event.getOption("query"); // Get the query + assert query != null; + String queryValue = query.getAsString(); // Get the query value + + // Lookup the requested player by the given query + apiClient.async().getPlayer(queryValue).whenComplete((player, ex) -> { + // Failed to lookup the player, handle the error + if (ex != null) { + if (ex.getCause() instanceof RESTfulMCAPIException apiError) { + replyWithApiError(event, apiError); + } else { // Only print real errors + ex.printStackTrace(); + } + return; + } + // Respond with the player + long cached = player.getCached(); // The timestamp the player was cached + event.getHook().sendMessageEmbeds(new EmbedBuilder() + .setColor(0x55FF55) + .setTitle("<:grass_block:1232798337300828181> Player Response", "https://api.restfulmc.cc/player/" + queryValue) + .addField("Unique ID", player.getUniqueId().toString(), true) + .addField("Username", player.getUsername(), true) + .addField("Legacy", player.isLegacy() ? "Yes" : "No", true) + .addField("Cached", cached == -1L ? "No" : "Yes, ", true) + .setThumbnail(player.getSkin().getParts().get(Player.SkinPart.HEAD)) + .setFooter("Requested by " + user.getName() + " | " + user.getId(), user.getEffectiveAvatarUrl()) + .build()).queue(); + }); + } +} \ No newline at end of file