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