refactors and persistent server settings

This commit is contained in:
2025-07-10 21:44:39 +02:00
parent 2dabb1885c
commit add142864e
9 changed files with 182 additions and 45 deletions
+1
View File
@@ -3,6 +3,7 @@ target/
!**/src/main/**/target/ !**/src/main/**/target/
!**/src/test/**/target/ !**/src/test/**/target/
logs/ logs/
data/
### IntelliJ IDEA ### ### IntelliJ IDEA ###
.idea/modules.xml .idea/modules.xml
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="data" uuid="360fa9b4-f0d4-411d-8b92-fb946dc416e7">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/data/data.db</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>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</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
</libraries>
</data-source>
</component>
</project>
@@ -1,7 +1,8 @@
package me.youhavetrouble.inviter; package me.youhavetrouble.inviter;
import me.youhavetrouble.inviter.http.ApiServer; 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 me.youhavetrouble.inviter.storage.Storage;
import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.JDABuilder;
@@ -18,8 +19,9 @@ public class Main {
public static final Logger LOGGER = LoggerFactory.getLogger("Inviter"); public static final Logger LOGGER = LoggerFactory.getLogger("Inviter");
private static JDA jda; private static JDA jda;
private static Storage storage; private static DiscordInviteManager discordInviteManager;
private static ApiServer apiServer; private static ApiServer apiServer;
private static Storage storage;
public static void main(String[] args) throws InterruptedException { public static void main(String[] args) throws InterruptedException {
@@ -69,6 +71,8 @@ public class Main {
} }
} }
storage = new SqliteStorage();
jda = JDABuilder.create( jda = JDABuilder.create(
token, token,
Set.of(GatewayIntent.GUILD_INVITES) Set.of(GatewayIntent.GUILD_INVITES)
@@ -86,7 +90,10 @@ public class Main {
jda.awaitReady(); 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 { try {
apiServer = new ApiServer(hostname, port); apiServer = new ApiServer(hostname, port);
@@ -98,8 +105,8 @@ public class Main {
LOGGER.info("Welcome to the Inviter Application!"); LOGGER.info("Welcome to the Inviter Application!");
} }
public static Storage getStorage() { public static DiscordInviteManager getDiscordInviteMenager() {
return storage; return discordInviteManager;
} }
} }
@@ -1,4 +1,4 @@
package me.youhavetrouble.inviter; package me.youhavetrouble.inviter.discord;
public record DiscordInvite( public record DiscordInvite(
String code, String code,
@@ -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.Cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import me.youhavetrouble.inviter.DiscordInvite;
import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Invite; import net.dv8tion.jda.api.entities.Invite;
import net.dv8tion.jda.api.entities.channel.unions.DefaultGuildChannelUnion; import net.dv8tion.jda.api.entities.channel.unions.DefaultGuildChannelUnion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.Duration; import java.time.Duration;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
public class MemoryStorage implements Storage { public class DiscordInviteManager {
private final Cache<String, DiscordInvite> cache = Caffeine.newBuilder() private final Cache<String, DiscordInvite> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.of(60, ChronoUnit.SECONDS)) .expireAfterWrite(Duration.of(60, ChronoUnit.SECONDS))
@@ -21,13 +19,11 @@ public class MemoryStorage implements Storage {
private final JDA jda; private final JDA jda;
public MemoryStorage(JDA jda) { public DiscordInviteManager(JDA jda) {
this.jda = jda; this.jda = jda;
} }
@Nullable @Nullable
@Override
public DiscordInvite getInvite(long guildId) { public DiscordInvite getInvite(long guildId) {
DiscordInvite discordInvite = cache.getIfPresent(String.valueOf(guildId)); DiscordInvite discordInvite = cache.getIfPresent(String.valueOf(guildId));
if (discordInvite == null || discordInvite.isExpired()) { if (discordInvite == null || discordInvite.isExpired()) {
@@ -52,24 +48,4 @@ public class MemoryStorage implements Storage {
return discordInvite; 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;
}
} }
@@ -0,0 +1,10 @@
package me.youhavetrouble.inviter.discord;
import org.jetbrains.annotations.Nullable;
public record GuildSettings(
boolean apiEnabled,
@Nullable String apiHostname
) {
}
@@ -1,9 +1,9 @@
package me.youhavetrouble.inviter.http.endpoints; package me.youhavetrouble.inviter.http.endpoints;
import com.sun.net.httpserver.HttpExchange; 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.Main;
import me.youhavetrouble.inviter.storage.Storage; import me.youhavetrouble.inviter.discord.DiscordInviteManager;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
@@ -38,7 +38,7 @@ public class GetDiscordInviteByGuildId implements EndpointHandler {
return; return;
} }
Storage storage = Main.getStorage(); DiscordInviteManager storage = Main.getDiscordInviteMenager();
DiscordInvite invite = storage.getInvite(guildIdLong); DiscordInvite invite = storage.getInvite(guildIdLong);
if (invite == null) { if (invite == null) {
@@ -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);
}
}
}
@@ -1,19 +1,18 @@
package me.youhavetrouble.inviter.storage; 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.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
public interface Storage { public interface Storage {
@Nullable DiscordInvite getInvite(long guildId); @NotNull GuildSettings getGuildSettings(long guildId);
/** void saveDefaultGuildSettings(long guildId);
* Saves the invite to the storage and returns the saved invite.
* @param invite JDA invite object to save void updateDiscordApiEnabled(long guildId, boolean enabled);
* @return the saved DiscordInvite object
*/ void updateDiscordApiHostname(long guildId, @Nullable String hostname);
@NotNull DiscordInvite saveInvite(Invite invite);
} }