From 3b816c4bde5ff9f76d8b2c645c1706aa7e1736ad Mon Sep 17 00:00:00 2001 From: allison <67673392+Allybe@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:06:21 -0500 Subject: [PATCH] things might work --- .gitignore | 4 +- pom.xml | 14 +++ src/main/java/Main.java | 10 -- src/main/java/tech/allydoes/Constants.java | 6 + src/main/java/tech/allydoes/Main.java | 73 +++++++++++ .../allydoes/database/DatabaseManager.java | 113 ++++++++++++++++++ .../java/tech/allydoes/database/Queries.java | 11 ++ .../java/tech/allydoes/discord/Command.java | 12 ++ .../tech/allydoes/discord/DiscordManager.java | 75 ++++++++++++ .../java/tech/allydoes/discord/Embeds.java | 17 +++ .../discord/commands/SaveChannelCommand.java | 42 +++++++ .../tech/allydoes/web/HttpServerHandler.java | 98 +++++++++++++++ .../tech/allydoes/web/POST/PostStatus.java | 36 ++++++ .../tech/allydoes/web/RequestHandler.java | 15 +++ 14 files changed, 515 insertions(+), 11 deletions(-) delete mode 100644 src/main/java/Main.java create mode 100644 src/main/java/tech/allydoes/Constants.java create mode 100644 src/main/java/tech/allydoes/Main.java create mode 100644 src/main/java/tech/allydoes/database/DatabaseManager.java create mode 100644 src/main/java/tech/allydoes/database/Queries.java create mode 100644 src/main/java/tech/allydoes/discord/Command.java create mode 100644 src/main/java/tech/allydoes/discord/DiscordManager.java create mode 100644 src/main/java/tech/allydoes/discord/Embeds.java create mode 100644 src/main/java/tech/allydoes/discord/commands/SaveChannelCommand.java create mode 100644 src/main/java/tech/allydoes/web/HttpServerHandler.java create mode 100644 src/main/java/tech/allydoes/web/POST/PostStatus.java create mode 100644 src/main/java/tech/allydoes/web/RequestHandler.java 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(); +}