diff --git a/.gitignore b/.gitignore
index 5a53574..e07b5b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,4 +30,6 @@ build/
.vscode/
### Mac OS ###
-.DS_Store
\ No newline at end of file
+.DS_Store
+
+.env
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 0520e68..2fc9fef 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,6 +16,8 @@
4.2.17.Final
2.25.4
6.5.0
+ 3.2.0
+ 42.7.13
@@ -57,5 +59,17 @@
+
+
+ io.github.cdimascio
+ dotenv-java
+ ${dot.version}
+
+
+
+ org.postgresql
+ postgresql
+ ${postgresql.version}
+
\ No newline at end of file
diff --git a/src/main/java/Main.java b/src/main/java/Main.java
deleted file mode 100644
index ae8f65d..0000000
--- a/src/main/java/Main.java
+++ /dev/null
@@ -1,10 +0,0 @@
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-public class Main {
- static Logger logger = LogManager.getLogger(Main.class);
-
- public static void main(String[] args) {
-
- }
-}
diff --git a/src/main/java/tech/allydoes/Constants.java b/src/main/java/tech/allydoes/Constants.java
new file mode 100644
index 0000000..d9fbc49
--- /dev/null
+++ b/src/main/java/tech/allydoes/Constants.java
@@ -0,0 +1,6 @@
+package tech.allydoes;
+
+public class Constants {
+ public static final boolean PRODUCTION = false;
+ public static final String TESTING_GUILD = "";
+}
diff --git a/src/main/java/tech/allydoes/Main.java b/src/main/java/tech/allydoes/Main.java
new file mode 100644
index 0000000..1ce2c74
--- /dev/null
+++ b/src/main/java/tech/allydoes/Main.java
@@ -0,0 +1,73 @@
+package tech.allydoes;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.*;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import tech.allydoes.database.DatabaseManager;
+import tech.allydoes.discord.DiscordManager;
+import tech.allydoes.web.HttpServerHandler;
+
+public class Main {
+ private static DiscordManager discordManager;
+ private static DatabaseManager databaseManager;
+
+ static Logger logger = LogManager.getLogger(Main.class);
+ public final int port;
+ public Main(int port) {
+ this.port = port;
+ }
+
+ public static void main(String[] args) throws InterruptedException {
+ discordManager = new DiscordManager();
+ databaseManager = new DatabaseManager();
+
+ new Main(8080).start();
+ }
+
+ public void start() {
+ EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+ EventLoopGroup workerGroup = new NioEventLoopGroup();
+ try {
+ ServerBootstrap serverBootstrap = new ServerBootstrap();
+ serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
+ serverBootstrap.group(bossGroup, workerGroup)
+ .channel(NioServerSocketChannel.class)
+ .handler(new LoggingHandler(LogLevel.DEBUG))
+ .childHandler(new ChannelInitializer() {
+ @Override
+ protected void initChannel(SocketChannel socketChannel) {
+ ChannelPipeline channelPipeline = socketChannel.pipeline();
+ channelPipeline.addLast(new HttpResponseEncoder());
+ channelPipeline.addLast(new HttpRequestDecoder());
+ channelPipeline.addLast(new HttpObjectAggregator(1048576));
+ channelPipeline.addLast(new HttpServerHandler());
+ }
+ });
+ Channel ch = serverBootstrap.bind(this.port).sync().channel();
+ ch.closeFuture().sync();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } finally {
+ bossGroup.shutdownGracefully();
+ workerGroup.shutdownGracefully();
+ }
+ }
+
+ public static DiscordManager getDiscordManager() {
+ return discordManager;
+ }
+
+ public static DatabaseManager getDatabaseManager() {
+ return databaseManager;
+ }
+}
diff --git a/src/main/java/tech/allydoes/database/DatabaseManager.java b/src/main/java/tech/allydoes/database/DatabaseManager.java
new file mode 100644
index 0000000..fd3119a
--- /dev/null
+++ b/src/main/java/tech/allydoes/database/DatabaseManager.java
@@ -0,0 +1,113 @@
+package tech.allydoes.database;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.sql.*;
+
+public class DatabaseManager {
+ private final Logger LOGGER = LogManager.getLogger(DatabaseManager.class);
+ private final Dotenv dotenv = Dotenv.load();
+
+ public DatabaseManager() {
+ initializeDatabase();
+ }
+
+ private void initializeDatabase() {
+ String user = dotenv.get("DB_USER");
+ String password = dotenv.get("DB_PASSWORD");
+ String host = dotenv.get("DB_HOST");
+ String port = dotenv.get("DB_PORT");
+
+ String defaultDbUrl = String.format("jdbc:postgresql://%s:%s/postgres?sslmode=require", host, port);
+ try (Connection conn = DriverManager.getConnection(defaultDbUrl, user, password)) {
+ conn.setAutoCommit(true);
+
+ boolean dbExists = false;
+ try (Statement statement = conn.createStatement()) {
+ if (statement.execute(Queries.CHECK_IF_DATABASE_EXISTS)) {
+ ResultSet resultSet = statement.getResultSet();
+ resultSet.next();
+ dbExists = true;
+ }
+ }
+
+ if (!dbExists) {
+ LOGGER.debug("Database does not exist. Creating...");
+ try (Statement statement = conn.createStatement()) {
+ if (statement.execute(Queries.CREATE_DATABASE)) {
+ ResultSet resultSet = statement.getResultSet();
+ resultSet.next();
+ LOGGER.debug("Database created successfully.");
+ }
+ } catch (SQLException e) {
+ LOGGER.error("Failed to create database: {}", e.getMessage());
+ }
+
+ try (Statement statement = conn.createStatement()) {
+ if (statement.execute(Queries.CREATE_SERVER_TABLE)) {
+ ResultSet resultSet = statement.getResultSet();
+ resultSet.next();
+ LOGGER.debug("Server table created successfully.");
+ }
+ } catch (SQLException e) {
+ LOGGER.error("Failed to create server table: {}", e.getMessage());
+ }
+ } else {
+ LOGGER.debug("Database already exists.");
+ }
+
+ } catch (Exception e) {
+ LOGGER.error("Database initialization failed: {}", e.getMessage());
+ }
+ }
+
+ public void getAllNotifiers() {
+ String user = dotenv.get("DB_USER");
+ String password = dotenv.get("DB_PASSWORD");
+ String host = dotenv.get("DB_HOST");
+ String port = dotenv.get("DB_PORT");
+
+ String defaultDbUrl = String.format("jdbc:postgresql://%s:%s/postgres?sslmode=require", host, port);
+ try (Connection conn = DriverManager.getConnection(defaultDbUrl, user, password)) {
+ conn.setAutoCommit(true);
+
+ try (Statement statement = conn.createStatement()) {
+ statement.execute(Queries.SELECT_ALL_SERVER_CHANNELS);
+ ResultSet resultSet = statement.getResultSet();
+ while (resultSet.next()) {
+ String guildId = resultSet.getString("guild_id");
+ String channelId = resultSet.getString("channel_id");
+ String messageId = resultSet.getString("message_id");
+ LOGGER.debug("Notifier for guild {}, channel {}, message {}", guildId, channelId, messageId);
+ }
+ }
+ } catch (SQLException e) {
+ LOGGER.error("getAllNotifiers: Failed to connect: {}", e.getMessage());
+ }
+ }
+
+ public void putNotifier(String guildId, String channelId, String messageId) {
+ String user = dotenv.get("DB_USER");
+ String password = dotenv.get("DB_PASSWORD");
+ String host = dotenv.get("DB_HOST");
+ String port = dotenv.get("DB_PORT");
+
+ String defaultDbUrl = String.format("jdbc:postgresql://%s:%s/postgres?sslmode=require", host, port);
+ try (Connection conn = DriverManager.getConnection(defaultDbUrl, user, password)) {
+ conn.setAutoCommit(true);
+
+ try (PreparedStatement preparedStatement = conn.prepareStatement(Queries.INSERT_SERVER_CHANNEL)) {
+ preparedStatement.setString(1, guildId);
+ preparedStatement.setString(2, channelId);
+ preparedStatement.setString(3, messageId);
+ if (preparedStatement.execute()) {
+ LOGGER.debug("Notifier for guild {}, channel {}, message {} added successfully.", guildId, channelId, messageId);
+ }
+ }
+ } catch (SQLException e) {
+ LOGGER.error("putNotifier: Failed to connect: {}", e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/tech/allydoes/database/Queries.java b/src/main/java/tech/allydoes/database/Queries.java
new file mode 100644
index 0000000..c912d6a
--- /dev/null
+++ b/src/main/java/tech/allydoes/database/Queries.java
@@ -0,0 +1,11 @@
+package tech.allydoes.database;
+
+public class Queries {
+ public static final String CHECK_IF_DATABASE_EXISTS = "SELECT 1 FROM pg_database WHERE datname = notifier";
+ public static final String CREATE_DATABASE = "CREATE DATABASE IF NOT EXISTS notifier";
+ public static final String CREATE_SERVER_TABLE = "CREATE TABLE IF NOT EXISTS notifier_channels (guild_id VARCHAR(22) PRIMARY KEY, channel_id VARCHAR(22), message_id VARCHAR(22))";
+ public static final String INSERT_SERVER_CHANNEL = "INSERT INTO notifier_channels (guild_id, channel_id, message_id) VALUES (?, ?, ?)";
+ public static final String DELETE_SERVER_CHANNEL = "DELETE FROM notifier_channels WHERE message_id = ?";
+ public static final String SELECT_ALL_SERVER_CHANNELS = "SELECT guild_id, channel_id, message_id FROM notifier_channels";
+
+}
diff --git a/src/main/java/tech/allydoes/discord/Command.java b/src/main/java/tech/allydoes/discord/Command.java
new file mode 100644
index 0000000..52f9d2d
--- /dev/null
+++ b/src/main/java/tech/allydoes/discord/Command.java
@@ -0,0 +1,12 @@
+package tech.allydoes.discord;
+
+import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
+import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
+import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
+
+public interface Command {
+ SlashCommandData getCommandData();
+ boolean isPrivateCommand();
+ void processSlashCommandInteractionEvent(SlashCommandInteractionEvent event);
+ void processButtonInteractionEvent(ButtonInteractionEvent event);
+}
diff --git a/src/main/java/tech/allydoes/discord/DiscordManager.java b/src/main/java/tech/allydoes/discord/DiscordManager.java
new file mode 100644
index 0000000..3ba8de8
--- /dev/null
+++ b/src/main/java/tech/allydoes/discord/DiscordManager.java
@@ -0,0 +1,75 @@
+package tech.allydoes.discord;
+
+import io.github.cdimascio.dotenv.Dotenv;
+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.Guild;
+import net.dv8tion.jda.api.requests.GatewayIntent;
+import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import tech.allydoes.Constants;
+import tech.allydoes.discord.commands.SaveChannelCommand;
+
+import java.util.HashMap;
+
+public class DiscordManager {
+ public final Logger LOGGER;
+ private final JDA jda;
+ private final HashMap commands;
+ private final Dotenv dotenv = Dotenv.load();
+
+ public DiscordManager() throws InterruptedException {
+ LOGGER = LogManager.getLogger(DiscordManager.class);
+ commands = new HashMap<>();
+ commands.put("savechannel", new SaveChannelCommand());
+
+ String botToken = Constants.PRODUCTION ? dotenv.get("DISCORD_TOKEN") : dotenv.get("TESTING_TOKEN");
+ jda = JDABuilder
+ .create(botToken, GatewayIntent.GUILD_MESSAGES)
+ .build()
+ .awaitReady();
+ registerSlashCommands();
+ jda.getPresence().setActivity(Activity.competing("in robotics!"));
+ LOGGER.info("Logged in as {}", jda.getSelfUser().getName());
+ }
+
+ private void registerSlashCommands() {
+ if (Constants.PRODUCTION) {
+ registerSlashCommandsGlobal();
+ } else {
+ registerSlashCommandsGuild(Constants.TESTING_GUILD);
+ }
+ }
+
+ private void registerSlashCommandsGlobal() {
+ CommandListUpdateAction commandListUpdateAction = jda.updateCommands();
+ for (Command command : commands.values()) {
+ if (command.isPrivateCommand()) continue;
+ commandListUpdateAction = commandListUpdateAction.addCommands(command.getCommandData());
+ LOGGER.debug("Registered global command: {}", command.getCommandData().getName());
+ }
+ commandListUpdateAction.queue();
+ }
+
+ private void registerSlashCommandsGuild(String guildID) {
+ Guild guild = jda.getGuildById(guildID);
+ if (guild == null) return;
+
+ CommandListUpdateAction updateAction = guild.updateCommands();
+ for (Command command : commands.values()) {
+ updateAction = updateAction.addCommands(command.getCommandData());
+ LOGGER.debug("Registered guild command: {}", command.getCommandData().getName());
+ }
+ updateAction.queue();
+ }
+
+ public Command getCommand(String command) {
+ return commands.get(command);
+ }
+
+ public JDA getJDA() {
+ return jda;
+ }
+}
diff --git a/src/main/java/tech/allydoes/discord/Embeds.java b/src/main/java/tech/allydoes/discord/Embeds.java
new file mode 100644
index 0000000..704bcbd
--- /dev/null
+++ b/src/main/java/tech/allydoes/discord/Embeds.java
@@ -0,0 +1,17 @@
+package tech.allydoes.discord;
+
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.entities.MessageEmbed;
+
+import java.time.Instant;
+
+public class Embeds {
+ public static MessageEmbed createUnknownLabStatus() {
+ return new EmbedBuilder()
+ .setTitle("Unknown Status")
+ .setDescription("The status of the lab is unknown.")
+ .setColor(0xD3D3D3)
+ .setTimestamp(Instant.now())
+ .build();
+ }
+}
diff --git a/src/main/java/tech/allydoes/discord/commands/SaveChannelCommand.java b/src/main/java/tech/allydoes/discord/commands/SaveChannelCommand.java
new file mode 100644
index 0000000..b8d596a
--- /dev/null
+++ b/src/main/java/tech/allydoes/discord/commands/SaveChannelCommand.java
@@ -0,0 +1,42 @@
+package tech.allydoes.discord.commands;
+
+import net.dv8tion.jda.api.Permission;
+import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
+import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
+import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
+import net.dv8tion.jda.api.interactions.commands.build.Commands;
+import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
+import tech.allydoes.Main;
+import tech.allydoes.discord.Command;
+import tech.allydoes.discord.Embeds;
+
+public class SaveChannelCommand implements Command {
+ @Override
+ public SlashCommandData getCommandData() {
+ return Commands.slash("savechannel", "Saves the channel you are in to be used for the status updates")
+ .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.ADMINISTRATOR));
+ }
+
+ @Override
+ public boolean isPrivateCommand() {
+ return false;
+ }
+
+ @Override
+ public void processSlashCommandInteractionEvent(SlashCommandInteractionEvent event) {
+ event.deferReply(true).queue((interactionHook) -> {
+ interactionHook.getInteraction().getMessageChannel().sendMessageEmbeds(Embeds.createUnknownLabStatus()).queue((embedMessage) -> {
+ String guildId = event.getGuild().getId();
+ String channelId = event.getChannel().getId();
+ String messageId = embedMessage.getId();
+ Main.getDatabaseManager().putNotifier(guildId, channelId, messageId);
+ });
+ event.getHook().editOriginal("Channel saved!").queue();
+ });
+ }
+
+ @Override
+ public void processButtonInteractionEvent(ButtonInteractionEvent event) {
+
+ }
+}
diff --git a/src/main/java/tech/allydoes/web/HttpServerHandler.java b/src/main/java/tech/allydoes/web/HttpServerHandler.java
new file mode 100644
index 0000000..107b09b
--- /dev/null
+++ b/src/main/java/tech/allydoes/web/HttpServerHandler.java
@@ -0,0 +1,98 @@
+package tech.allydoes.web;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.*;
+import io.netty.util.CharsetUtil;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import tech.allydoes.leaderboards.handlers.GET.*;
+import tech.allydoes.leaderboards.handlers.POST.SetPlayerProfile;
+import tech.allydoes.leaderboards.handlers.POST.SetScore;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static io.netty.handler.codec.http.HttpHeaderNames.*;
+import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
+
+public class HttpServerHandler extends SimpleChannelInboundHandler {
+ private static final Logger LOGGER = LogManager.getLogger(HttpServerHandler.class);
+ private final HashMap requestHandlers = new HashMap<>();
+
+ public HttpServerHandler() {
+ RequestHandler[] handlers = {
+
+ };
+ for (RequestHandler handler : handlers) {
+ requestHandlers.put(handler.getRequestName().toLowerCase(), handler);
+ }
+ }
+
+ public static ChannelFuture sendContent(String content, FullHttpRequest request, ChannelHandlerContext channelHandlerContext) {
+ ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
+ DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.OK, byteBuf);
+ httpResponse.headers().set(CONTENT_TYPE, "application/json; charset=utf-8");
+ httpResponse.headers().set(CONTENT_LENGTH, byteBuf.readableBytes());
+ return channelHandlerContext.writeAndFlush(httpResponse);
+ }
+
+ @Override
+ public void channelReadComplete(ChannelHandlerContext channelHandlerContext) {
+ channelHandlerContext.flush();
+ }
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext channelHandlerContext, FullHttpRequest request) {
+ ChannelFuture future = processRequest(channelHandlerContext, request);
+
+ request.headers().set(CONNECTION, CLOSE);
+ future.addListener(ChannelFutureListener.CLOSE);
+ }
+
+ private ChannelFuture processRequest(ChannelHandlerContext channelHandlerContext, FullHttpRequest request) {
+ String path = request.uri().split("\\?", 15)[0].toLowerCase();
+ RequestHandler requestHandler = requestHandlers.get(path);
+ QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri());
+ Map> params = queryStringDecoder.parameters();
+
+ if (requestHandler != null && requestHandler.getRequestType().equalsIgnoreCase(request.method().name())) {
+ if (!hasRequiredParameters(params, requestHandler.getRequiredParameters())) {
+ return channelHandlerContext.writeAndFlush(new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.BAD_REQUEST));
+ }
+
+ return requestHandler.processRequest(channelHandlerContext, request, params);
+ } else {
+ return channelHandlerContext.writeAndFlush(new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.NOT_FOUND));
+ }
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable cause) {
+ LOGGER.info("Unhandled exception in pipeline", cause);
+
+ channelHandlerContext.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR));
+ channelHandlerContext.close();
+ }
+
+ private boolean hasRequiredParameters(Map> parameters, String[] requiredParameters) {
+ for (String requiredParameter : requiredParameters) {
+ if (!parameters.containsKey(requiredParameter)) {
+ return false;
+ }
+ }
+
+ for (List parameterArray: parameters.values()) {
+ String parameter = parameterArray.getFirst();
+ if (parameter.isEmpty()) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/tech/allydoes/web/POST/PostStatus.java b/src/main/java/tech/allydoes/web/POST/PostStatus.java
new file mode 100644
index 0000000..c38ee7a
--- /dev/null
+++ b/src/main/java/tech/allydoes/web/POST/PostStatus.java
@@ -0,0 +1,36 @@
+package tech.allydoes.web.POST;
+
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.FullHttpRequest;
+import tech.allydoes.web.RequestHandler;
+
+import java.util.List;
+import java.util.Map;
+
+public class PostStatus implements RequestHandler {
+ public static String STATUS = "status";
+
+ @Override
+ public ChannelFuture processRequest(ChannelHandlerContext channelHandlerContext, FullHttpRequest request, Map> parameters) {
+ int status = Integer.parseInt(parameters.get(STATUS).getFirst());
+
+ }
+
+ @Override
+ public String getRequestName() {
+ return "PostStatus";
+ }
+
+ @Override
+ public String getRequestType() {
+ return "POST";
+ }
+
+ @Override
+ public String[] getRequiredParameters() {
+ return new String[] {
+ STATUS
+ };
+ }
+}
diff --git a/src/main/java/tech/allydoes/web/RequestHandler.java b/src/main/java/tech/allydoes/web/RequestHandler.java
new file mode 100644
index 0000000..7fba8de
--- /dev/null
+++ b/src/main/java/tech/allydoes/web/RequestHandler.java
@@ -0,0 +1,15 @@
+package tech.allydoes.web;
+
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.FullHttpRequest;
+
+import java.util.List;
+import java.util.Map;
+
+public interface RequestHandler {
+ ChannelFuture processRequest(ChannelHandlerContext channelHandlerContext, FullHttpRequest request, Map> parameters);
+ String getRequestName();
+ String getRequestType();
+ String[] getRequiredParameters();
+}