diff --git a/.gitignore b/.gitignore index 0f3cac9..4fb2032 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ target/ !**/src/main/**/target/ !**/src/test/**/target/ logs/ +data/ ### IntelliJ IDEA ### .idea/modules.xml diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..61012ce --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,23 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/data/data.db + + + + $ProjectFileDir$ + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar + + + + + \ No newline at end of file diff --git a/src/main/java/me/youhavetrouble/inviter/Main.java b/src/main/java/me/youhavetrouble/inviter/Main.java index 4f5597a..d00064e 100644 --- a/src/main/java/me/youhavetrouble/inviter/Main.java +++ b/src/main/java/me/youhavetrouble/inviter/Main.java @@ -1,7 +1,8 @@ package me.youhavetrouble.inviter; import me.youhavetrouble.inviter.http.ApiServer; -import me.youhavetrouble.inviter.storage.MemoryStorage; +import me.youhavetrouble.inviter.discord.DiscordInviteManager; +import me.youhavetrouble.inviter.storage.SqliteStorage; import me.youhavetrouble.inviter.storage.Storage; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; @@ -18,8 +19,9 @@ public class Main { public static final Logger LOGGER = LoggerFactory.getLogger("Inviter"); private static JDA jda; - private static Storage storage; + private static DiscordInviteManager discordInviteManager; private static ApiServer apiServer; + private static Storage storage; public static void main(String[] args) throws InterruptedException { @@ -69,6 +71,8 @@ public class Main { } } + storage = new SqliteStorage(); + jda = JDABuilder.create( token, Set.of(GatewayIntent.GUILD_INVITES) @@ -86,7 +90,10 @@ public class Main { jda.awaitReady(); - storage = new MemoryStorage(jda); + jda.getGuilds().parallelStream().forEach(guild -> storage.saveDefaultGuildSettings(guild.getIdLong())); + // TODO make sure to save default settings for guilds bot joins on runtime + + discordInviteManager = new DiscordInviteManager(jda); try { apiServer = new ApiServer(hostname, port); @@ -98,8 +105,8 @@ public class Main { LOGGER.info("Welcome to the Inviter Application!"); } - public static Storage getStorage() { - return storage; + public static DiscordInviteManager getDiscordInviteMenager() { + return discordInviteManager; } } diff --git a/src/main/java/me/youhavetrouble/inviter/DiscordInvite.java b/src/main/java/me/youhavetrouble/inviter/discord/DiscordInvite.java similarity index 94% rename from src/main/java/me/youhavetrouble/inviter/DiscordInvite.java rename to src/main/java/me/youhavetrouble/inviter/discord/DiscordInvite.java index 2c2e746..b193387 100644 --- a/src/main/java/me/youhavetrouble/inviter/DiscordInvite.java +++ b/src/main/java/me/youhavetrouble/inviter/discord/DiscordInvite.java @@ -1,4 +1,4 @@ -package me.youhavetrouble.inviter; +package me.youhavetrouble.inviter.discord; public record DiscordInvite( String code, diff --git a/src/main/java/me/youhavetrouble/inviter/storage/MemoryStorage.java b/src/main/java/me/youhavetrouble/inviter/discord/DiscordInviteManager.java similarity index 65% rename from src/main/java/me/youhavetrouble/inviter/storage/MemoryStorage.java rename to src/main/java/me/youhavetrouble/inviter/discord/DiscordInviteManager.java index 8557ff5..7ff76f3 100644 --- a/src/main/java/me/youhavetrouble/inviter/storage/MemoryStorage.java +++ b/src/main/java/me/youhavetrouble/inviter/discord/DiscordInviteManager.java @@ -1,19 +1,17 @@ -package me.youhavetrouble.inviter.storage; +package me.youhavetrouble.inviter.discord; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; -import me.youhavetrouble.inviter.DiscordInvite; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Invite; import net.dv8tion.jda.api.entities.channel.unions.DefaultGuildChannelUnion; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.time.Duration; import java.time.temporal.ChronoUnit; -public class MemoryStorage implements Storage { +public class DiscordInviteManager { private final Cache cache = Caffeine.newBuilder() .expireAfterWrite(Duration.of(60, ChronoUnit.SECONDS)) @@ -21,13 +19,11 @@ public class MemoryStorage implements Storage { private final JDA jda; - public MemoryStorage(JDA jda) { + public DiscordInviteManager(JDA jda) { this.jda = jda; } - @Nullable - @Override public DiscordInvite getInvite(long guildId) { DiscordInvite discordInvite = cache.getIfPresent(String.valueOf(guildId)); if (discordInvite == null || discordInvite.isExpired()) { @@ -52,24 +48,4 @@ public class MemoryStorage implements Storage { return discordInvite; } - @NotNull - @Override - public DiscordInvite saveInvite(Invite invite) { - - if (invite == null) { - throw new IllegalArgumentException("Invite cannot be null"); - } - if (invite.getGuild() == null) { - throw new IllegalArgumentException("Invite must be associated with a guild"); - } - - DiscordInvite discordInvite = new DiscordInvite( - invite.getCode(), - invite.getGuild().getIdLong(), - invite.getTimeCreated().toEpochSecond() - ); - - cache.put(invite.getGuild().getId(), discordInvite); - return discordInvite; - } } diff --git a/src/main/java/me/youhavetrouble/inviter/discord/GuildSettings.java b/src/main/java/me/youhavetrouble/inviter/discord/GuildSettings.java new file mode 100644 index 0000000..eb10d21 --- /dev/null +++ b/src/main/java/me/youhavetrouble/inviter/discord/GuildSettings.java @@ -0,0 +1,10 @@ +package me.youhavetrouble.inviter.discord; + +import org.jetbrains.annotations.Nullable; + +public record GuildSettings( + boolean apiEnabled, + @Nullable String apiHostname +) { + +} diff --git a/src/main/java/me/youhavetrouble/inviter/http/endpoints/GetDiscordInviteByGuildId.java b/src/main/java/me/youhavetrouble/inviter/http/endpoints/GetDiscordInviteByGuildId.java index e01d67f..247cbdf 100644 --- a/src/main/java/me/youhavetrouble/inviter/http/endpoints/GetDiscordInviteByGuildId.java +++ b/src/main/java/me/youhavetrouble/inviter/http/endpoints/GetDiscordInviteByGuildId.java @@ -1,9 +1,9 @@ package me.youhavetrouble.inviter.http.endpoints; import com.sun.net.httpserver.HttpExchange; -import me.youhavetrouble.inviter.DiscordInvite; +import me.youhavetrouble.inviter.discord.DiscordInvite; import me.youhavetrouble.inviter.Main; -import me.youhavetrouble.inviter.storage.Storage; +import me.youhavetrouble.inviter.discord.DiscordInviteManager; import org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -38,7 +38,7 @@ public class GetDiscordInviteByGuildId implements EndpointHandler { return; } - Storage storage = Main.getStorage(); + DiscordInviteManager storage = Main.getDiscordInviteMenager(); DiscordInvite invite = storage.getInvite(guildIdLong); if (invite == null) { diff --git a/src/main/java/me/youhavetrouble/inviter/storage/SqliteStorage.java b/src/main/java/me/youhavetrouble/inviter/storage/SqliteStorage.java new file mode 100644 index 0000000..a273ebb --- /dev/null +++ b/src/main/java/me/youhavetrouble/inviter/storage/SqliteStorage.java @@ -0,0 +1,121 @@ +package me.youhavetrouble.inviter.storage; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import me.youhavetrouble.inviter.discord.GuildSettings; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.sql.DataSource; +import java.io.File; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class SqliteStorage implements Storage { + + private final DataSource dataSource; + + public SqliteStorage() { + File dataFolder = new File("data"); + if (!dataFolder.exists()) { + if (!dataFolder.mkdirs()) { + throw new RuntimeException("Failed to create data folder"); + } + } + + HikariConfig config = new HikariConfig(); + config.setPoolName("DataSQLitePool"); + config.setDriverClassName("org.sqlite.JDBC"); + config.setJdbcUrl("jdbc:sqlite:data/data.db"); + config.setConnectionTestQuery("PRAGMA journal_mode=WAL;"); + config.setMaxLifetime(60000); // 60 Sec + config.setMaximumPoolSize(Math.min(4, Runtime.getRuntime().availableProcessors() / 4)); + dataSource = new HikariDataSource(config); + + try (Connection connection = dataSource.getConnection()) { + // Initialize the database schema if necessary + // For example, you might want to create a table for guild settings + connection.createStatement().execute(""" + CREATE TABLE IF NOT EXISTS guild_settings ( + guild_id LONG PRIMARY KEY, + api_enabled BOOLEAN NOT NULL DEFAULT FALSE, + api_hostname VARCHAR(256) DEFAULT NULL + ); + """ + ); + } catch (SQLException e) { + throw new RuntimeException("Failed to initialize database", e); + } + + } + + + @NotNull + @Override + public GuildSettings getGuildSettings(long guildId) { + try (Connection connection = dataSource.getConnection()) { + var statement = connection.prepareStatement( + "SELECT * FROM guild_settings WHERE guild_id = ?" + ); + statement.setLong(1, guildId); + ResultSet resultSet = statement.executeQuery(); + + if (resultSet.next()) { + boolean apiEnabled = resultSet.getBoolean("api_enabled"); + String apiHostname = resultSet.getString("api_hostname"); + return new GuildSettings(apiEnabled, apiHostname); + } + return new GuildSettings(false, null); + } catch (SQLException e) { + throw new RuntimeException("Failed to retrieve guild settings", e); + + } + } + + @Override + public void saveDefaultGuildSettings(long guildId) { + try (Connection connection = dataSource.getConnection()) { + var statement = connection.prepareStatement( + "INSERT OR IGNORE INTO guild_settings (guild_id) VALUES (?)" + ); + statement.setLong(1, guildId); + statement.executeUpdate(); + } catch (Exception e) { + throw new RuntimeException("Failed to save default guild settings", e); + } + } + + @Override + public void updateDiscordApiEnabled(long guildId, boolean enabled) { + try (Connection connection = dataSource.getConnection()) { + var statement = connection.prepareStatement( + "UPDATE guild_settings SET api_enabled = ? WHERE guild_id = ?" + ); + statement.setBoolean(1, enabled); + statement.setLong(2, guildId); + statement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Failed to update Discord API enabled status", e); + } + + } + + @Override + public void updateDiscordApiHostname(long guildId, @Nullable String hostname) { + try (Connection connection = dataSource.getConnection()) { + var statement = connection.prepareStatement( + "UPDATE guild_settings SET api_hostname = ? WHERE guild_id = ?" + ); + if (hostname == null || hostname.isEmpty()) { + statement.setNull(1, java.sql.Types.VARCHAR); + } else { + statement.setString(1, hostname); + } + statement.setLong(2, guildId); + statement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Failed to update Discord API hostname", e); + } + } +} diff --git a/src/main/java/me/youhavetrouble/inviter/storage/Storage.java b/src/main/java/me/youhavetrouble/inviter/storage/Storage.java index e6f74b9..72e8322 100644 --- a/src/main/java/me/youhavetrouble/inviter/storage/Storage.java +++ b/src/main/java/me/youhavetrouble/inviter/storage/Storage.java @@ -1,19 +1,18 @@ package me.youhavetrouble.inviter.storage; -import me.youhavetrouble.inviter.DiscordInvite; -import net.dv8tion.jda.api.entities.Invite; + +import me.youhavetrouble.inviter.discord.GuildSettings; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public interface Storage { - @Nullable DiscordInvite getInvite(long guildId); + @NotNull GuildSettings getGuildSettings(long guildId); - /** - * Saves the invite to the storage and returns the saved invite. - * @param invite JDA invite object to save - * @return the saved DiscordInvite object - */ - @NotNull DiscordInvite saveInvite(Invite invite); + void saveDefaultGuildSettings(long guildId); + + void updateDiscordApiEnabled(long guildId, boolean enabled); + + void updateDiscordApiHostname(long guildId, @Nullable String hostname); }