Remove JpaUtils and refactor repository access
Eliminated the static `JpaUtils` utility and transitioned to explicit repository management for improved modularity and testability. Updated `GuildService`, `TaskManagerImpl`, and related classes to use dependency-injected repositories. Simplified configuration by aligning repository definitions with existing service patterns and ensuring proper session handling through `SessionManager`. Verified that all affected components function as expected after refactoring.
This commit is contained in:
1 parent
62de32ecfc
commit
677b3f8013
60 files changed
+2384
-780
No files matched your search
@@ -28,7 +28,10 @@ import me.bobert.netneutrality.core.thread.ExecutorProvider;
|
||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
||||
import me.bobert.netneutrality.utils.ChannelMessagePurger;
|
||||
import me.bobert.netneutrality.db.JpaUtils;
|
||||
import me.bobert.netneutrality.db.repository.BlacklistRepository;
|
||||
import me.bobert.netneutrality.db.repository.GuildRepository;
|
||||
import me.bobert.netneutrality.db.repository.GuildSettingRepository;
|
||||
import me.bobert.netneutrality.db.repository.TaskRepository;
|
||||
import me.bobert.netneutrality.utils.Settings;
|
||||
import me.bobert.netneutrality.http.WebModule;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
@@ -114,9 +117,11 @@ public class App {
|
||||
// Allow ApplicationContext itself to be injected into components that need it.
|
||||
context.register(context);
|
||||
|
||||
// Persistence: build the SessionFactory + run migrations; released last on shutdown.
|
||||
// SessionManager binds itself into JpaUtils for repository access.
|
||||
context.register(new SessionManager(settings));
|
||||
// Persistence: run migrations + build the SessionFactory; released last on shutdown.
|
||||
SessionManager sessions = context.register(new SessionManager(settings));
|
||||
GuildRepository guildRepository = new GuildRepository(sessions);
|
||||
BlacklistRepository blacklistRepository = new BlacklistRepository(sessions);
|
||||
GuildSettingRepository guildSettingRepository = new GuildSettingRepository(sessions);
|
||||
|
||||
// Executors: no explicit start today; shut down after music players are disconnected.
|
||||
ExecutorProvider executors = context.register(new ThreadManager());
|
||||
@@ -130,12 +135,12 @@ public class App {
|
||||
CommandRegistry commands = context.register(new CommandManager(context));
|
||||
|
||||
// Task registry (priority 30).
|
||||
TaskManager tasks = context.register(new TaskManagerImpl(context));
|
||||
TaskManager tasks = context.register(new TaskManagerImpl(context, new TaskRepository(sessions)));
|
||||
|
||||
// Per-guild service cache (not a Lifecycle component).
|
||||
GuildServiceProvider guildServices = context.register(new GuildServiceProviderImpl());
|
||||
GuildServiceProvider guildServices = context.register(
|
||||
new GuildServiceProviderImpl(guildRepository, blacklistRepository, guildSettingRepository));
|
||||
LogHandler.bind(guildServices);
|
||||
JpaUtils.bindGuildServiceLookup(guildServices::get);
|
||||
|
||||
// Music sources (priority 40): initialise the shared player manager; disconnect on shutdown.
|
||||
music = context.register(new MusicServiceImpl(settings, guildServices, tasks, executors));
|
||||
|
||||
@@ -42,7 +42,7 @@ public class BotState {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, GuildService> services = guildServices.getServices();
|
||||
Map<String, GuildService> services = guildServices.snapshot();
|
||||
|
||||
ObjectNode main = Json.object();
|
||||
ArrayNode guildsArray = Json.array();
|
||||
@@ -56,7 +56,7 @@ public class BotState {
|
||||
|
||||
ObjectNode guild = Json.object();
|
||||
ObjectNode music = Json.object();
|
||||
guild.put("id", service.getEntity().getGuildId());
|
||||
guild.put("id", service.getGuildId());
|
||||
|
||||
// MessageChannel currentChannel = trackScheduler.getTextChannel();
|
||||
// if (currentChannel == null) {
|
||||
|
||||
@@ -10,16 +10,20 @@ import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
||||
import me.bobert.netneutrality.core.command.impl.AdminCommand;
|
||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||
import me.bobert.netneutrality.core.context.CommandContext;
|
||||
import me.bobert.netneutrality.db.entities.OBlacklist;
|
||||
import me.bobert.netneutrality.db.service.BlacklistEntry;
|
||||
import me.bobert.netneutrality.db.service.BlacklistResult;
|
||||
import me.bobert.netneutrality.db.service.BlacklistService;
|
||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
||||
|
||||
public class BlacklistCommand extends AdminCommand {
|
||||
private static final Logger logger = LogManager.getLogger(BlacklistCommand.class);
|
||||
|
||||
private final GuildServiceProvider guildServices;
|
||||
|
||||
@@ -64,16 +68,16 @@ public class BlacklistCommand extends AdminCommand {
|
||||
public CommandExecutionResult execute(CommandContext context) {
|
||||
String sub = context.getSubcommand();
|
||||
String word = context.getOption("word", "", CommandOption::getAsString);
|
||||
BlacklistService service = guildServices.get(context.getGuild()).getBlacklistService();
|
||||
BlacklistService service = guildServices.get(context.getGuild().getId()).getBlacklistService();
|
||||
|
||||
switch (sub) {
|
||||
case "get" -> context.async("Retrieving list, please wait...", asyncMessage -> {
|
||||
List<OBlacklist> blacklists = service.getBlacklistForGuild();
|
||||
List<BlacklistEntry> blacklists = service.list();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("List of blacklisted words for this server: \n");
|
||||
for (OBlacklist bl : blacklists) {
|
||||
sb.append(bl.getWord()).append(" -> ").append(bl.getEnabled() ? "enabled" : "disabled").append("\n");
|
||||
for (BlacklistEntry bl : blacklists) {
|
||||
sb.append(bl.word()).append(" -> ").append(bl.enabled() ? "enabled" : "disabled").append("\n");
|
||||
}
|
||||
|
||||
if (blacklists.isEmpty()) {
|
||||
@@ -83,13 +87,32 @@ public class BlacklistCommand extends AdminCommand {
|
||||
asyncMessage.edit(sb.toString());
|
||||
});
|
||||
case "add" -> {
|
||||
return service.add(context.getGuildUser(), word);
|
||||
try {
|
||||
return toReply(service.add(context.getGuildUser().getId(), word), word);
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("Failed to add word to blacklist", e);
|
||||
return CommandExecutionResult.warn("Failed to add word to blacklist: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
case "remove" -> {
|
||||
return service.delete(word);
|
||||
try {
|
||||
return toReply(service.remove(word), word);
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("Failed to remove word from blacklist", e);
|
||||
return CommandExecutionResult.warn("Failed to remove word from blacklist: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CommandExecutionResult.none();
|
||||
}
|
||||
|
||||
static CommandExecutionResult toReply(BlacklistResult result, String word) {
|
||||
return switch (result) {
|
||||
case ADDED, REENABLED -> CommandExecutionResult.success("Added the word [%s] to the blacklist.", word);
|
||||
case ALREADY_EXISTS -> CommandExecutionResult.warn(String.format("The word [%s] is already in the blacklist.", word));
|
||||
case REMOVED -> CommandExecutionResult.success("Removed the word [%s] from the blacklist.", word);
|
||||
case NOT_FOUND -> CommandExecutionResult.warn(String.format("The word [%s] is not in the blacklist.", word));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,19 +12,23 @@ import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||
import me.bobert.netneutrality.core.context.CommandContext;
|
||||
import me.bobert.netneutrality.db.GSettings;
|
||||
import me.bobert.netneutrality.db.service.GuildService;
|
||||
import me.bobert.netneutrality.db.service.GuildSettingService;
|
||||
import net.dv8tion.jda.api.entities.Role;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
|
||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
||||
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
|
||||
|
||||
import java.util.Map;
|
||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
||||
import me.bobert.netneutrality.music.MusicService;
|
||||
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ConfigureCommand extends AdminCommand {
|
||||
private static final Logger logger = LogManager.getLogger(ConfigureCommand.class);
|
||||
|
||||
private final GuildServiceProvider guildServices;
|
||||
private final MusicService musicService;
|
||||
@@ -76,8 +80,18 @@ public class ConfigureCommand extends AdminCommand {
|
||||
|
||||
@Override
|
||||
public CommandExecutionResult execute(CommandContext context) {
|
||||
try {
|
||||
return configure(context);
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("Failed to update configuration", e);
|
||||
return CommandExecutionResult.warn("Failed to update configuration: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private CommandExecutionResult configure(CommandContext context) {
|
||||
String sub = context.getSubcommand();
|
||||
GuildService guildService = guildServices.get(context.getGuild().getId());
|
||||
GuildSettingService settingService = guildService.getGuildSettingService();
|
||||
context.ephemeral();
|
||||
|
||||
if ("music-role".equals(sub)) {
|
||||
@@ -85,13 +99,13 @@ public class ConfigureCommand extends AdminCommand {
|
||||
assert role != null;
|
||||
|
||||
if (role.getPosition() == -1) {
|
||||
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, null);
|
||||
settingService.set(GSettings.MUSIC_ROLE, null);
|
||||
|
||||
return CommandExecutionResult.success("Everyone can now use music commands");
|
||||
}
|
||||
|
||||
if (role.getGuild().getId().equals(context.getGuild().getId())) {
|
||||
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, role.getId());
|
||||
settingService.set(GSettings.MUSIC_ROLE, role.getId());
|
||||
|
||||
return CommandExecutionResult.success(String.format("Successfully set %s as the role needed for music command", role.getAsMention()));
|
||||
}
|
||||
@@ -101,14 +115,14 @@ public class ConfigureCommand extends AdminCommand {
|
||||
GuildChannelUnion channel = context.getOption("channel", CommandOption::getAsChannel);
|
||||
|
||||
if (channel == null) {
|
||||
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_CHANNEL, null);
|
||||
settingService.set(GSettings.MUSIC_CHANNEL, null);
|
||||
|
||||
return CommandExecutionResult.success("No channel restrictions for music commands");
|
||||
}
|
||||
|
||||
if (channel instanceof TextChannel) {
|
||||
if (channel.getGuild().getId().equals(context.getGuild().getId())) {
|
||||
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_CHANNEL, channel.getId());
|
||||
settingService.set(GSettings.MUSIC_CHANNEL, channel.getId());
|
||||
|
||||
return CommandExecutionResult.success(String.format("Successfully set %s as the channel required to execute music command", channel.getAsMention()));
|
||||
}
|
||||
@@ -118,7 +132,7 @@ public class ConfigureCommand extends AdminCommand {
|
||||
|
||||
if ("music-volume".equals(sub)) {
|
||||
int volume = context.getOption("volume", 30, CommandOption::getAsInt);
|
||||
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_VOLUME, String.valueOf(volume));
|
||||
settingService.set(GSettings.MUSIC_VOLUME, String.valueOf(volume));
|
||||
musicService.get(context.getGuild()).setVolume(volume);
|
||||
|
||||
return CommandExecutionResult.success(String.format("Default volume for music player is now %d", volume));
|
||||
@@ -132,7 +146,7 @@ public class ConfigureCommand extends AdminCommand {
|
||||
}
|
||||
|
||||
if ("show-all".equals(sub)) {
|
||||
Map<String, String> settings = guildService.getGuildSettingService().getSettings();
|
||||
Map<String, String> settings = settingService.snapshot();
|
||||
MessageCreateBuilder builder = new MessageCreateBuilder();
|
||||
|
||||
for (GSettings o : GSettings.values()) {
|
||||
|
||||
@@ -92,7 +92,7 @@ public class LogHandler {
|
||||
|
||||
if (context.getGuild() != null) {
|
||||
GuildServiceProvider provider = guildServices;
|
||||
if (provider != null && provider.get(context.getGuild()).isDebugEnabled()) {
|
||||
if (provider != null && provider.get(context.getGuild().getId()).isDebugEnabled()) {
|
||||
context.getGuild()
|
||||
.getTextChannels()
|
||||
.stream()
|
||||
|
||||
@@ -43,7 +43,7 @@ public abstract class MusicCommand extends AbstractCommand {
|
||||
* @return the guild service
|
||||
*/
|
||||
protected final GuildService guildService(Guild guild) {
|
||||
return guildServices.get(guild);
|
||||
return guildServices.get(guild.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +79,7 @@ public abstract class MusicCommand extends AbstractCommand {
|
||||
|
||||
Guild guild = member.getGuild();
|
||||
|
||||
String roleDb = guildService(guild.getId()).getGuildSettingService().getSetting(GSettings.MUSIC_ROLE);
|
||||
String roleDb = guildService(guild.getId()).getGuildSettingService().getOrDefault(GSettings.MUSIC_ROLE);
|
||||
|
||||
if (roleDb != null && !roleDb.isEmpty()) {
|
||||
if (UserFunction.isAdmin(member)) {
|
||||
@@ -89,7 +89,7 @@ public abstract class MusicCommand extends AbstractCommand {
|
||||
Role neededRole = guild.getRoleById(roleDb);
|
||||
if (neededRole == null) {
|
||||
// Role doesn't exist on guild anymore, remove from db
|
||||
guildService(guild.getId()).getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, null);
|
||||
guildService(guild.getId()).getGuildSettingService().set(GSettings.MUSIC_ROLE, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public abstract class MusicCommand extends AbstractCommand {
|
||||
return false;
|
||||
}
|
||||
|
||||
String channelDb = guildService(guildMessageChannel.getGuild()).getGuildSettingService().getSetting(GSettings.MUSIC_CHANNEL);
|
||||
String channelDb = guildService(guildMessageChannel.getGuild()).getGuildSettingService().getOrDefault(GSettings.MUSIC_CHANNEL);
|
||||
if (channelDb != null && !channelDb.isEmpty()) {
|
||||
return channelDb.equals(channel.getId());
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import me.bobert.netneutrality.core.tasks.trait.WithButton;
|
||||
import me.bobert.netneutrality.core.tasks.trait.WithChildren;
|
||||
import me.bobert.netneutrality.core.tasks.trait.WithParent;
|
||||
import me.bobert.netneutrality.core.tasks.persistence.PersistableTaskRegistry;
|
||||
import me.bobert.netneutrality.db.repository.TaskRepository;
|
||||
import me.bobert.netneutrality.db.service.TaskService;
|
||||
import me.bobert.netneutrality.exceptions.GracefulContextError;
|
||||
import me.bobert.netneutrality.exceptions.TaskExecutionException;
|
||||
@@ -52,9 +53,10 @@ public class TaskManagerImpl implements TaskManager {
|
||||
* Creates a task manager that can inject context-registered dependencies into reflectively
|
||||
* loaded tasks.
|
||||
*
|
||||
* @param context the application context; must not be {@code null}
|
||||
* @param context the application context; must not be {@code null}
|
||||
* @param taskRepository the repository persisting {@link PersistableTask}s
|
||||
*/
|
||||
public TaskManagerImpl(ApplicationContext context) {
|
||||
public TaskManagerImpl(ApplicationContext context, TaskRepository taskRepository) {
|
||||
Objects.requireNonNull(context, "ApplicationContext must not be null");
|
||||
this.tasks = new ConcurrentHashMap<>();
|
||||
this.pendingTasks = new ConcurrentHashMap<>();
|
||||
@@ -62,7 +64,7 @@ public class TaskManagerImpl implements TaskManager {
|
||||
this.tasksLock = new ConcurrentHashMap<>();
|
||||
this.context = context;
|
||||
this.persistableTaskRegistry = new PersistableTaskRegistry(context);
|
||||
this.taskService = new TaskService(this, persistableTaskRegistry);
|
||||
this.taskService = new TaskService(this, persistableTaskRegistry, taskRepository);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package me.bobert.netneutrality.db;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import org.hibernate.Session;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Generic, constructor-injected Hibernate repository.
|
||||
*
|
||||
* <p>All work runs through Hibernate's native {@code fromSession}/{@code fromTransaction}/
|
||||
* {@code inTransaction} helpers, which open and close the session and commit or roll back the
|
||||
* transaction. Exceptions are never swallowed: a failed write is rolled back and rethrown.
|
||||
*
|
||||
* @param <T> the entity type
|
||||
* @param <ID> the entity identifier type
|
||||
*/
|
||||
public abstract class AbstractRepository<T, ID> {
|
||||
|
||||
protected final SessionProvider sessions;
|
||||
private final Class<T> type;
|
||||
|
||||
protected AbstractRepository(SessionProvider sessions, Class<T> type) {
|
||||
this.sessions = Objects.requireNonNull(sessions, "sessions");
|
||||
this.type = Objects.requireNonNull(type, "type");
|
||||
}
|
||||
|
||||
protected Class<T> type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public List<T> findAll() {
|
||||
return read(session -> {
|
||||
CriteriaQuery<T> query = session.getCriteriaBuilder().createQuery(type);
|
||||
query.from(type);
|
||||
return session.createSelectionQuery(query).getResultList();
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<T> findById(ID id) {
|
||||
Objects.requireNonNull(id, "id");
|
||||
return read(session -> Optional.ofNullable(session.find(type, id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts or updates the entity.
|
||||
*
|
||||
* @param entity the entity to save
|
||||
* @return the persistent (merged) state, which callers should use from then on
|
||||
*/
|
||||
public T save(T entity) {
|
||||
Objects.requireNonNull(entity, "entity");
|
||||
return write(session -> session.merge(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entity with the given id, if it exists.
|
||||
*
|
||||
* @param id the identifier
|
||||
* @return {@code true} if a row was deleted
|
||||
*/
|
||||
public boolean deleteById(ID id) {
|
||||
Objects.requireNonNull(id, "id");
|
||||
return write(session -> {
|
||||
T entity = session.find(type, id);
|
||||
if (entity == null) {
|
||||
return false;
|
||||
}
|
||||
session.remove(entity);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
protected <R> R read(Function<Session, R> work) {
|
||||
return sessions.getSessionFactory().fromSession(work);
|
||||
}
|
||||
|
||||
protected <R> R write(Function<Session, R> work) {
|
||||
return sessions.getSessionFactory().fromTransaction(work);
|
||||
}
|
||||
|
||||
protected void writeVoid(Consumer<Session> work) {
|
||||
sessions.getSessionFactory().inTransaction(work);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package me.bobert.netneutrality.db;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* JDBC connection settings shared by Flyway ({@link DatabaseMigration}) and Hibernate
|
||||
* ({@link SessionManager}), so both always connect to the same database.
|
||||
*
|
||||
* @param url the JDBC url
|
||||
* @param user the database user
|
||||
* @param password the database password (never logged, see {@link #toString()})
|
||||
*/
|
||||
public record DatabaseConfig(String url, String user, String password) {
|
||||
|
||||
public DatabaseConfig {
|
||||
Objects.requireNonNull(url, "url");
|
||||
Objects.requireNonNull(user, "user");
|
||||
password = password == null ? "" : password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the config from the {@code MYSQL_*} settings.
|
||||
*
|
||||
* @param settings the bot settings
|
||||
* @return the database config
|
||||
*/
|
||||
public static DatabaseConfig from(SettingsProvider settings) {
|
||||
String dbName = settings.get("MYSQL_DATABASE", "netneutrality");
|
||||
String dbUser = settings.get("MYSQL_USER", "root");
|
||||
String dbPass = settings.get("MYSQL_PASSWORD", "");
|
||||
String dbHost = settings.get("MYSQL_HOST", "localhost");
|
||||
String dbPort = settings.get("MYSQL_PORT", "3306");
|
||||
return new DatabaseConfig(String.format("jdbc:mysql://%s:%s/%s", dbHost, dbPort, dbName), dbUser, dbPass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DatabaseConfig[url=" + url + ", user=" + user + ", password=***]";
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,38 @@
|
||||
package me.bobert.netneutrality.db;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.api.output.MigrateResult;
|
||||
|
||||
public class DatabaseMigration {
|
||||
private final static Logger logger = LogManager.getLogger(DatabaseMigration.class);
|
||||
public final class DatabaseMigration {
|
||||
private static final Logger logger = LogManager.getLogger(DatabaseMigration.class);
|
||||
|
||||
public static void migrate(SettingsProvider settings) {
|
||||
String dbName = settings.get("MYSQL_DATABASE", "netneutrality");
|
||||
String dbUser = settings.get("MYSQL_USER", "root");
|
||||
String dbPass = settings.get("MYSQL_PASSWORD", "");
|
||||
String dbHost = settings.get("MYSQL_HOST", "localhost");
|
||||
String dbPort = settings.get("MYSQL_PORT", "3306");
|
||||
String dbUrl = String.format("jdbc:mysql://%s:%s/%s", dbHost, dbPort, dbName);
|
||||
private DatabaseMigration() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all pending Flyway migrations to the configured database.
|
||||
*
|
||||
* @param config the database connection settings
|
||||
* @throws IllegalStateException if the migration fails
|
||||
*/
|
||||
public static void migrate(DatabaseConfig config) {
|
||||
try {
|
||||
logger.info("Running database migrations..");
|
||||
Flyway flyway = Flyway.configure()
|
||||
.dataSource(dbUrl, dbUser, dbPass)
|
||||
.dataSource(config.url(), config.user(), config.password())
|
||||
.load();
|
||||
|
||||
MigrateResult result = flyway.migrate();
|
||||
logger.info("Database migrations finished successfully! (total: {})", result.migrationsExecuted);
|
||||
} catch (Exception e) {
|
||||
logger.error(e);
|
||||
throw new IllegalStateException("Database migration failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
package me.bobert.netneutrality.db;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Per-guild settings stored in the {@code guild_settings} table.
|
||||
*
|
||||
* <p>Only values that differ from the default are stored; setting a value back to its default (or
|
||||
* {@code null}) deletes the row. {@link #getName()} is the key persisted in the {@code name}
|
||||
* column and must never change for an existing setting.
|
||||
*/
|
||||
public enum GSettings {
|
||||
//TODO: description
|
||||
/** Id of the role required to use music commands; empty means everyone may use them. */
|
||||
MUSIC_ROLE("MUSIC_ROLE", ""),
|
||||
/** Id of the only text channel music commands may be used in; empty means any channel. */
|
||||
MUSIC_CHANNEL("MUSIC_CHANNEL", ""),
|
||||
/** Default playback volume (0-100) applied when a music player is created. */
|
||||
MUSIC_VOLUME("MUSIC_VOLUME", "30"),
|
||||
/** Playlist size above which the bot asks for confirmation before queueing every track. */
|
||||
MUSIC_MAXPLAYLISTSIZE("MUSIC_MAXPLAYLISTSIZE", "20");
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
package me.bobert.netneutrality.db;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import me.bobert.netneutrality.db.entities.OGuild;
|
||||
import me.bobert.netneutrality.db.service.GuildService;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.hibernate.HibernateException;
|
||||
import org.hibernate.ObjectNotFoundException;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.Transaction;
|
||||
import org.hibernate.exception.ConstraintViolationException;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Static Hibernate helpers used by repositories and domain services.
|
||||
*
|
||||
* <p>Because repositories are created deep in the object graph without constructor injection,
|
||||
* this class holds a single {@link SessionProvider} bound once during bootstrap via
|
||||
* {@link #bind(SessionProvider)}. That is the only global access point for sessions — there is no
|
||||
* {@code SessionManager.instance()} shim.
|
||||
*/
|
||||
public class JpaUtils {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(JpaUtils.class);
|
||||
private static volatile SessionProvider sessions;
|
||||
private static volatile Function<String, GuildService> guildServiceLookup;
|
||||
|
||||
private JpaUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the application {@link SessionProvider}. Called once from {@code SessionManager}'s
|
||||
* constructor during context registration.
|
||||
*
|
||||
* @param provider the live session provider
|
||||
*/
|
||||
public static void bind(SessionProvider provider) {
|
||||
sessions = Objects.requireNonNull(provider, "SessionProvider");
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional guild-service lookup used when recovering a missing {@link OGuild} entity. Bound from
|
||||
* bootstrap once the {@code GuildServiceProvider} is available.
|
||||
*
|
||||
* @param lookup guild-id → service resolver
|
||||
*/
|
||||
public static void bindGuildServiceLookup(Function<String, GuildService> lookup) {
|
||||
guildServiceLookup = Objects.requireNonNull(lookup, "guildServiceLookup");
|
||||
}
|
||||
|
||||
private static SessionFactory factory() {
|
||||
SessionProvider provider = sessions;
|
||||
if (provider == null) {
|
||||
throw new IllegalStateException("JpaUtils has not been bound to a SessionProvider");
|
||||
}
|
||||
return provider.getSessionFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to read the persistent state into the object.
|
||||
* <p>
|
||||
* If the object is not found in the database and is of type {@link OGuild}, tries to recreate entity and throws
|
||||
* an exception to block execution.
|
||||
* <p>
|
||||
*
|
||||
* @param session The session in which to load the persistent object
|
||||
* @param object The persistent object
|
||||
* @param id Identifier
|
||||
*/
|
||||
public static void tryLoad(Session session, Object object, Serializable id) {
|
||||
try {
|
||||
session.load(object, id);
|
||||
} catch (HibernateException exception) {
|
||||
if (exception instanceof ObjectNotFoundException) {
|
||||
if (object instanceof OGuild oGuild) {
|
||||
String cachedId = oGuild.getGuildId();
|
||||
logger.error("Trying to load Guild[{}] while entity doesn't exist in database, reloading..", cachedId);
|
||||
|
||||
Function<String, GuildService> lookup = guildServiceLookup;
|
||||
if (lookup == null) {
|
||||
session.close();
|
||||
throw exception;
|
||||
}
|
||||
GuildService service = lookup.apply(cachedId);
|
||||
service.reloadEntity();
|
||||
service.getBlacklistService().removeGuildBlacklist();
|
||||
logger.info("Guild[{}] reloaded", cachedId);
|
||||
}
|
||||
}
|
||||
|
||||
session.close();
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
public static Session openSession() {
|
||||
return factory().openSession();
|
||||
}
|
||||
|
||||
public static void runWithSession(Consumer<Session> consumer) {
|
||||
try (Session session = factory().openSession()) {
|
||||
consumer.accept(session);
|
||||
}
|
||||
}
|
||||
|
||||
public static void runWithTransaction(Consumer<Session> consumer) {
|
||||
try (Session session = factory().openSession()) {
|
||||
session.beginTransaction();
|
||||
consumer.accept(session);
|
||||
session.getTransaction().commit();
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateWithTransaction(Object persistantEntity, Runnable runnable) {
|
||||
try (Session session = factory().openSession()) {
|
||||
Transaction transaction = session.beginTransaction();
|
||||
|
||||
try {
|
||||
runnable.run();
|
||||
|
||||
session.merge(persistantEntity);
|
||||
transaction.commit();
|
||||
} catch (ConstraintViolationException e) {
|
||||
logger.error(e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.error(e);
|
||||
transaction.rollback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the entity and relations from the database and executes the given consumer.
|
||||
* This will load all Lazy relationships defined in the entity.
|
||||
*
|
||||
* @param persistantEntity The entity to load
|
||||
* @param id The identifier for the entity
|
||||
* @param consumer The comsumer, which exposes the session
|
||||
*/
|
||||
public static void getWithSession(Object persistantEntity, Serializable id, Consumer<Session> consumer) {
|
||||
try (Session session = factory().openSession()) {
|
||||
tryLoad(session, persistantEntity, id);
|
||||
consumer.accept(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package me.bobert.netneutrality.db;
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.Transaction;
|
||||
import org.hibernate.exception.ConstraintViolationException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface Repository<O> {
|
||||
Logger logger = LogManager.getLogger(Repository.class);
|
||||
|
||||
List<O> findAll();
|
||||
|
||||
O findById(String id);
|
||||
|
||||
default void save(O entity) {
|
||||
Transaction transaction = null;
|
||||
|
||||
try (Session session = JpaUtils.openSession()) {
|
||||
transaction = session.beginTransaction();
|
||||
session.merge(entity);
|
||||
transaction.commit();
|
||||
} catch (ConstraintViolationException e) {
|
||||
logger.error(e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.error(e);
|
||||
|
||||
if (transaction != null) {
|
||||
transaction.rollback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void deleteBy(String id);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
package me.bobert.netneutrality.db;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
@@ -10,7 +11,6 @@ import me.bobert.netneutrality.utils.SettingsProvider;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.boot.Metadata;
|
||||
import org.hibernate.boot.MetadataSources;
|
||||
import org.hibernate.boot.registry.StandardServiceRegistry;
|
||||
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
|
||||
@@ -18,24 +18,34 @@ import org.hibernate.cfg.Environment;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Instance-based {@link SessionProvider} owning the Hibernate {@link SessionFactory} and its
|
||||
* service registry.
|
||||
*
|
||||
* <p>On construction the instance is bound into {@link JpaUtils} so repositories and other static
|
||||
* persistence helpers can open sessions without a global {@code SessionManager} singleton.
|
||||
* <p>{@link #start()} first applies the Flyway migrations and only then builds the session factory,
|
||||
* which validates the mapped entities against the migrated schema ({@code hbm2ddl.auto=validate}).
|
||||
*/
|
||||
public class SessionManager implements SessionProvider {
|
||||
private static final Logger logger = LogManager.getLogger(SessionManager.class);
|
||||
|
||||
private final SettingsProvider settings;
|
||||
private final DatabaseConfig config;
|
||||
private final boolean reportErrors;
|
||||
private StandardServiceRegistry registry;
|
||||
private SessionFactory sessionFactory;
|
||||
private volatile SessionFactory sessionFactory;
|
||||
|
||||
public SessionManager(SettingsProvider settings) {
|
||||
this.settings = settings;
|
||||
JpaUtils.bind(this);
|
||||
this(DatabaseConfig.from(settings), !"dev".equals(settings.get("NETNEUTRALITY_BOT_ENV", "dev")));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param config the database connection settings
|
||||
* @param reportErrors whether startup failures are reported to Sentry
|
||||
*/
|
||||
public SessionManager(DatabaseConfig config, boolean reportErrors) {
|
||||
this.config = Objects.requireNonNull(config, "config");
|
||||
this.reportErrors = reportErrors;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,55 +54,58 @@ public class SessionManager implements SessionProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
getSessionFactory();
|
||||
DatabaseMigration.migrate(settings);
|
||||
public synchronized void start() {
|
||||
if (sessionFactory != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info("Connecting to database '{}' with user '{}'", config.url(), config.user());
|
||||
DatabaseMigration.migrate(config);
|
||||
sessionFactory = buildSessionFactory();
|
||||
} catch (RuntimeException e) {
|
||||
if (reportErrors) {
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
|
||||
stop();
|
||||
logger.error("Error connecting to database. Shutting down..", e);
|
||||
throw new IllegalStateException("Unable to connect to the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
private SessionFactory buildSessionFactory() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put(Environment.JAKARTA_JDBC_URL, config.url());
|
||||
settings.put(Environment.JAKARTA_JDBC_USER, config.user());
|
||||
settings.put(Environment.JAKARTA_JDBC_PASSWORD, config.password());
|
||||
|
||||
registry = new StandardServiceRegistryBuilder()
|
||||
.configure()
|
||||
.applySettings(settings)
|
||||
.build();
|
||||
return new MetadataSources(registry).buildMetadata().buildSessionFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
public synchronized void stop() {
|
||||
SessionFactory factory = sessionFactory;
|
||||
sessionFactory = null;
|
||||
if (factory != null) {
|
||||
factory.close();
|
||||
}
|
||||
if (registry != null) {
|
||||
StandardServiceRegistryBuilder.destroy(registry);
|
||||
registry = null;
|
||||
sessionFactory = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionFactory getSessionFactory() {
|
||||
if (sessionFactory == null) {
|
||||
try {
|
||||
StandardServiceRegistryBuilder registryBuilder = new StandardServiceRegistryBuilder().configure();
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
String dbName = settings.get("MYSQL_DATABASE", "netneutrality");
|
||||
String dbUser = settings.get("MYSQL_USER", "root");
|
||||
String dbPass = settings.get("MYSQL_PASSWORD", "");
|
||||
String dbHost = settings.get("MYSQL_HOST", "localhost");
|
||||
String dbPort = settings.get("MYSQL_PORT", "3306");
|
||||
logger.info("Connecting to database '{}' at {}:{} with user '{}'", dbName, dbHost, dbPort, dbUser);
|
||||
|
||||
config.put(Environment.JAKARTA_JDBC_URL, String.format("jdbc:mysql://%s:%s/%s", dbHost, dbPort, dbName));
|
||||
config.put(Environment.JAKARTA_JDBC_USER, dbUser);
|
||||
config.put(Environment.JAKARTA_JDBC_PASSWORD, dbPass);
|
||||
registryBuilder.applySettings(config);
|
||||
|
||||
registry = registryBuilder.build();
|
||||
MetadataSources sources = new MetadataSources(registry);
|
||||
Metadata metadata = sources.getMetadataBuilder().build();
|
||||
sessionFactory = metadata.getSessionFactoryBuilder().build();
|
||||
|
||||
} catch (Exception e) {
|
||||
String env = settings.get("NETNEUTRALITY_BOT_ENV", "dev");
|
||||
if (!"dev".equals(env)) {
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
|
||||
stop();
|
||||
logger.error("Error connecting to database. Shutting down..");
|
||||
throw new IllegalStateException("Unable to connect to the database", e);
|
||||
}
|
||||
SessionFactory factory = sessionFactory;
|
||||
if (factory == null) {
|
||||
throw new IllegalStateException("SessionManager has not been started");
|
||||
}
|
||||
|
||||
return sessionFactory;
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Table(name = "blacklist")
|
||||
@@ -76,4 +79,26 @@ public class OBlacklist {
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity is the database id: two instances are equal when they have the same non-null id,
|
||||
* regardless of Hibernate proxies. Transient instances (no id yet) are only equal to themselves.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) {
|
||||
return false;
|
||||
}
|
||||
OBlacklist other = (OBlacklist) o;
|
||||
return getId() != null && Objects.equals(getId(), other.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Constant per class so the hash never changes when the id is assigned on persist.
|
||||
return Hibernate.getClass(this).hashCode();
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,10 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@@ -43,14 +44,14 @@ public class OGuild {
|
||||
private Set<OLogMessage> logMessages = new LinkedHashSet<>();
|
||||
|
||||
@OneToMany(mappedBy = "guild")
|
||||
private Set<OLogging> OLoggings = new LinkedHashSet<>();
|
||||
private Set<OLogging> loggings = new LinkedHashSet<>();
|
||||
|
||||
public Set<OLogging> getLoggings() {
|
||||
return OLoggings;
|
||||
return loggings;
|
||||
}
|
||||
|
||||
public void setLoggings(Set<OLogging> OLoggings) {
|
||||
this.OLoggings = OLoggings;
|
||||
public void setLoggings(Set<OLogging> loggings) {
|
||||
this.loggings = loggings;
|
||||
}
|
||||
|
||||
public Set<OLogMessage> getLogMessages() {
|
||||
@@ -110,13 +111,25 @@ public class OGuild {
|
||||
this.guildId = guildId;
|
||||
}
|
||||
|
||||
public OGuildSetting getSettingByName(String name) {
|
||||
Optional<OGuildSetting> setting = getGuildSettings()
|
||||
.stream()
|
||||
.filter(setting1 -> setting1.getName().equalsIgnoreCase(name))
|
||||
.findFirst();
|
||||
|
||||
return setting.orElse(null);
|
||||
/**
|
||||
* Identity is the database id: two instances are equal when they have the same non-null id,
|
||||
* regardless of Hibernate proxies. Transient instances (no id yet) are only equal to themselves.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) {
|
||||
return false;
|
||||
}
|
||||
OGuild other = (OGuild) o;
|
||||
return getGuildId() != null && Objects.equals(getGuildId(), other.getGuildId());
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Constant per class so the hash never changes when the id is assigned on persist.
|
||||
return Hibernate.getClass(this).hashCode();
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,13 @@ import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.NamedQueries;
|
||||
import jakarta.persistence.NamedQuery;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Table(name = "guild_settings")
|
||||
@@ -33,8 +35,7 @@ public class OGuildSetting {
|
||||
@Column(name = "name", nullable = false)
|
||||
private String name;
|
||||
|
||||
@Lob
|
||||
@Column(name = "value", nullable = false)
|
||||
@Column(name = "value", nullable = false, columnDefinition = "text")
|
||||
private String value;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@@ -72,4 +73,26 @@ public class OGuildSetting {
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity is the database id: two instances are equal when they have the same non-null id,
|
||||
* regardless of Hibernate proxies. Transient instances (no id yet) are only equal to themselves.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) {
|
||||
return false;
|
||||
}
|
||||
OGuildSetting other = (OGuildSetting) o;
|
||||
return getId() != null && Objects.equals(getId(), other.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Constant per class so the hash never changes when the id is assigned on persist.
|
||||
return Hibernate.getClass(this).hashCode();
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,10 @@ import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Table(name = "log_message")
|
||||
@@ -113,4 +115,26 @@ public class OLogMessage {
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity is the database id: two instances are equal when they have the same non-null id,
|
||||
* regardless of Hibernate proxies. Transient instances (no id yet) are only equal to themselves.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) {
|
||||
return false;
|
||||
}
|
||||
OLogMessage other = (OLogMessage) o;
|
||||
return getId() != null && Objects.equals(getId(), other.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Constant per class so the hash never changes when the id is assigned on persist.
|
||||
return Hibernate.getClass(this).hashCode();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,9 @@ import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.MapsId;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Table(name = "logging")
|
||||
@@ -52,4 +55,26 @@ public class OLogging {
|
||||
public void setId(OLoggingId id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity is the database id: two instances are equal when they have the same non-null id,
|
||||
* regardless of Hibernate proxies. Transient instances (no id yet) are only equal to themselves.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) {
|
||||
return false;
|
||||
}
|
||||
OLogging other = (OLogging) o;
|
||||
return getId() != null && Objects.equals(getId(), other.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Constant per class so the hash never changes when the id is assigned on persist.
|
||||
return Hibernate.getClass(this).hashCode();
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,11 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Table(name = "tasks", uniqueConstraints = {@UniqueConstraint(columnNames = {"task_id"})})
|
||||
@@ -31,8 +33,7 @@ public class OTask {
|
||||
@Column(name = "task_class", nullable = false)
|
||||
private String taskClass;
|
||||
|
||||
@Lob
|
||||
@Column(name = "payload")
|
||||
@Column(name = "payload", columnDefinition = "text")
|
||||
private String payload;
|
||||
|
||||
public Integer getId() {
|
||||
@@ -67,4 +68,25 @@ public class OTask {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Identity is the database id: two instances are equal when they have the same non-null id,
|
||||
* regardless of Hibernate proxies. Transient instances (no id yet) are only equal to themselves.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) {
|
||||
return false;
|
||||
}
|
||||
OTask other = (OTask) o;
|
||||
return getId() != null && Objects.equals(getId(), other.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Constant per class so the hash never changes when the id is assigned on persist.
|
||||
return Hibernate.getClass(this).hashCode();
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,12 @@ package me.bobert.netneutrality.db.entities;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Table(name = "web_users")
|
||||
@@ -22,8 +23,7 @@ public class OWebUser {
|
||||
@Column(name = "id", nullable = false)
|
||||
private String id;
|
||||
|
||||
@Lob
|
||||
@Column(name = "email", nullable = false)
|
||||
@Column(name = "email", nullable = false, columnDefinition = "mediumtext")
|
||||
private String email;
|
||||
|
||||
@Column(name = "token", nullable = false)
|
||||
@@ -87,4 +87,25 @@ public class OWebUser {
|
||||
this.rememberToken = rememberToken;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Identity is the database id: two instances are equal when they have the same non-null id,
|
||||
* regardless of Hibernate proxies. Transient instances (no id yet) are only equal to themselves.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) {
|
||||
return false;
|
||||
}
|
||||
OWebUser other = (OWebUser) o;
|
||||
return getId() != null && Objects.equals(getId(), other.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Constant per class so the hash never changes when the id is assigned on persist.
|
||||
return Hibernate.getClass(this).hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,104 +1,107 @@
|
||||
package me.bobert.netneutrality.db.repository;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import me.bobert.netneutrality.db.Repository;
|
||||
import me.bobert.netneutrality.db.AbstractRepository;
|
||||
import me.bobert.netneutrality.db.SessionProvider;
|
||||
import me.bobert.netneutrality.db.entities.OBlacklist;
|
||||
import me.bobert.netneutrality.db.entities.OGuild;
|
||||
import me.bobert.netneutrality.db.JpaUtils;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.query.Query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BlacklistRepository implements Repository<OBlacklist> {
|
||||
public class BlacklistRepository extends AbstractRepository<OBlacklist, Integer> {
|
||||
|
||||
@Override
|
||||
public List<OBlacklist> findAll() {
|
||||
List<OBlacklist> blacklists = new ArrayList<>();
|
||||
|
||||
JpaUtils.runWithSession(session -> {
|
||||
Query<OBlacklist> query = session.createQuery("FROM OBlacklist", OBlacklist.class);
|
||||
|
||||
blacklists.addAll(query.list());
|
||||
});
|
||||
|
||||
return blacklists;
|
||||
/**
|
||||
* Outcome of {@link #enableWord(String, String, String)}.
|
||||
*/
|
||||
public enum EnableOutcome {
|
||||
/** A new row was inserted. */
|
||||
CREATED,
|
||||
/** An existing, disabled row was re-enabled. */
|
||||
ENABLED,
|
||||
/** The word was already enabled. */
|
||||
UNCHANGED
|
||||
}
|
||||
|
||||
@Override
|
||||
public OBlacklist findById(String id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBy(String id) {
|
||||
JpaUtils.runWithTransaction(session -> {
|
||||
Query<OBlacklist> query = session.createQuery("SELECT o FROM OBlacklist o WHERE o.id = :blacklistId", OBlacklist.class);
|
||||
query.setParameter("blacklistId", id);
|
||||
|
||||
OBlacklist oTask = query.uniqueResult();
|
||||
if (oTask != null) {
|
||||
session.remove(oTask);
|
||||
}
|
||||
});
|
||||
public BlacklistRepository(SessionProvider sessions) {
|
||||
super(sessions, OBlacklist.class);
|
||||
}
|
||||
|
||||
public List<OBlacklist> findByGuildId(String guildId) {
|
||||
List<OBlacklist> blacklists = new ArrayList<>();
|
||||
|
||||
JpaUtils.runWithSession(session -> {
|
||||
Query<OBlacklist> query = session.createQuery("SELECT o FROM OBlacklist o WHERE o.guild.guildId = :guildId", OBlacklist.class);
|
||||
query.setParameter("guildId", guildId);
|
||||
|
||||
blacklists.addAll(query.list());
|
||||
});
|
||||
|
||||
return blacklists;
|
||||
return read(session -> session
|
||||
.createSelectionQuery("FROM OBlacklist o WHERE o.guild.guildId = :guildId ORDER BY o.id", OBlacklist.class)
|
||||
.setParameter("guildId", guildId)
|
||||
.getResultList());
|
||||
}
|
||||
|
||||
public void add(OGuild oGuild, Member member, String word) {
|
||||
JpaUtils.runWithTransaction(session -> {
|
||||
OBlacklist bl = getWordFromDb(oGuild.getGuildId(), word);
|
||||
public Set<String> findEnabledWords(String guildId) {
|
||||
return read(session -> session
|
||||
.createSelectionQuery("SELECT o.word FROM OBlacklist o WHERE o.guild.guildId = :guildId AND o.enabled = true", String.class)
|
||||
.setParameter("guildId", guildId)
|
||||
.getResultStream()
|
||||
.collect(Collectors.toUnmodifiableSet()));
|
||||
}
|
||||
|
||||
if (bl == null) {
|
||||
bl = new OBlacklist();
|
||||
bl.setGuild(oGuild);
|
||||
bl.setCreatorId(member.getId());
|
||||
bl.setWord(word);
|
||||
bl.setEnabled(true);
|
||||
|
||||
session.persist(bl);
|
||||
} else {
|
||||
bl.setEnabled(true);
|
||||
session.merge(bl);
|
||||
/**
|
||||
* Adds the word to the guild's blacklist, or re-enables it if it already exists. Lookup and
|
||||
* write happen in the same transaction.
|
||||
*
|
||||
* @param guildId the guild id (the guild row must exist)
|
||||
* @param creatorId the user adding the word
|
||||
* @param word the word
|
||||
* @return what changed
|
||||
*/
|
||||
public EnableOutcome enableWord(String guildId, String creatorId, String word) {
|
||||
return write(session -> {
|
||||
List<OBlacklist> existing = findWord(session, guildId, word);
|
||||
if (existing.isEmpty()) {
|
||||
OBlacklist blacklist = new OBlacklist();
|
||||
blacklist.setGuild(session.getReference(OGuild.class, guildId));
|
||||
blacklist.setCreatorId(creatorId);
|
||||
blacklist.setWord(word);
|
||||
blacklist.setEnabled(true);
|
||||
session.persist(blacklist);
|
||||
return EnableOutcome.CREATED;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void delete(OGuild oGuild, String word) {
|
||||
JpaUtils.runWithTransaction(session -> {
|
||||
OBlacklist bl = getWordFromDb(oGuild.getGuildId(), word);
|
||||
if (bl != null) {
|
||||
session.remove(bl);
|
||||
boolean changed = false;
|
||||
for (OBlacklist blacklist : existing) {
|
||||
if (!Boolean.TRUE.equals(blacklist.getEnabled())) {
|
||||
blacklist.setEnabled(true);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? EnableOutcome.ENABLED : EnableOutcome.UNCHANGED;
|
||||
});
|
||||
}
|
||||
|
||||
private OBlacklist getWordFromDb(String guildId, String word) {
|
||||
try (Session session = JpaUtils.openSession()) {
|
||||
Query<OBlacklist> query = session.createQuery("SELECT o FROM OBlacklist o WHERE upper(o.word) = upper(:word) AND o.guild.guildId = :guildId", OBlacklist.class);
|
||||
query.setParameter("guildId", guildId);
|
||||
query.setParameter("word", word);
|
||||
OBlacklist blacklist = query.uniqueResult();
|
||||
/**
|
||||
* Deletes the word (case-insensitive) from the guild's blacklist.
|
||||
*
|
||||
* @param guildId the guild id
|
||||
* @param word the word
|
||||
* @return {@code true} if at least one row was deleted
|
||||
*/
|
||||
public boolean deleteWord(String guildId, String word) {
|
||||
return write(session -> {
|
||||
List<OBlacklist> existing = findWord(session, guildId, word);
|
||||
existing.forEach(session::remove);
|
||||
return !existing.isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
session.close();
|
||||
return blacklist;
|
||||
}
|
||||
private static List<OBlacklist> findWord(Session session, String guildId, String word) {
|
||||
return session
|
||||
.createSelectionQuery("FROM OBlacklist o WHERE upper(o.word) = upper(:word) AND o.guild.guildId = :guildId", OBlacklist.class)
|
||||
.setParameter("guildId", guildId)
|
||||
.setParameter("word", word)
|
||||
.getResultList();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
package me.bobert.netneutrality.db.repository;/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
package me.bobert.netneutrality.db.repository;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
@@ -10,49 +6,31 @@ package me.bobert.netneutrality.db.repository;/*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import me.bobert.netneutrality.db.Repository;
|
||||
import me.bobert.netneutrality.db.AbstractRepository;
|
||||
import me.bobert.netneutrality.db.SessionProvider;
|
||||
import me.bobert.netneutrality.db.entities.OGuild;
|
||||
import me.bobert.netneutrality.db.JpaUtils;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.query.Query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
public class GuildRepository extends AbstractRepository<OGuild, String> {
|
||||
|
||||
public class GuildRepository implements Repository<OGuild> {
|
||||
@Override
|
||||
public List<OGuild> findAll() {
|
||||
List<OGuild> guilds = new ArrayList<>();
|
||||
|
||||
JpaUtils.runWithSession(session -> {
|
||||
Query<OGuild> query = session.createQuery("FROM OGuild", OGuild.class);
|
||||
|
||||
guilds.addAll(query.list());
|
||||
});
|
||||
|
||||
return guilds;
|
||||
public GuildRepository(SessionProvider sessions) {
|
||||
super(sessions, OGuild.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OGuild findById(String id) {
|
||||
try (Session session = JpaUtils.openSession()) {
|
||||
Query<OGuild> query = session.createQuery("SELECT o FROM OGuild o WHERE o.guildId = :guildId", OGuild.class);
|
||||
query.setParameter("guildId", id);
|
||||
|
||||
return query.uniqueResult();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBy(String id) {
|
||||
JpaUtils.runWithTransaction(session -> {
|
||||
Query<OGuild> query = session.createQuery("SELECT o FROM OGuild o WHERE o.guildId = :guildId", OGuild.class);
|
||||
query.setParameter("guildId", id);
|
||||
|
||||
OGuild oGuild = query.uniqueResult();
|
||||
if (oGuild != null) {
|
||||
session.remove(oGuild);
|
||||
/**
|
||||
* Returns the guild row, inserting it first if it does not exist yet.
|
||||
*
|
||||
* @param guildId the Discord guild id
|
||||
* @return the persistent guild
|
||||
*/
|
||||
public OGuild findOrCreate(String guildId) {
|
||||
return write(session -> {
|
||||
OGuild guild = session.find(OGuild.class, guildId);
|
||||
if (guild == null) {
|
||||
guild = new OGuild();
|
||||
guild.setGuildId(guildId);
|
||||
session.persist(guild);
|
||||
}
|
||||
return guild;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
package me.bobert.netneutrality.db.repository;/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
package me.bobert.netneutrality.db.repository;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
@@ -10,88 +6,56 @@ package me.bobert.netneutrality.db.repository;/*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import me.bobert.netneutrality.db.AbstractRepository;
|
||||
import me.bobert.netneutrality.db.GSettings;
|
||||
import me.bobert.netneutrality.db.Repository;
|
||||
import me.bobert.netneutrality.db.SessionProvider;
|
||||
import me.bobert.netneutrality.db.entities.OGuild;
|
||||
import me.bobert.netneutrality.db.entities.OGuildSetting;
|
||||
import me.bobert.netneutrality.db.JpaUtils;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.query.Query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GuildSettingRepository implements Repository<OGuildSetting> {
|
||||
@Override
|
||||
public List<OGuildSetting> findAll() {
|
||||
List<OGuildSetting> settings = new ArrayList<>();
|
||||
public class GuildSettingRepository extends AbstractRepository<OGuildSetting, Integer> {
|
||||
|
||||
JpaUtils.runWithSession(session -> {
|
||||
Query<OGuildSetting> query = session.createQuery("FROM OGuildSetting", OGuildSetting.class);
|
||||
|
||||
settings.addAll(query.list());
|
||||
});
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OGuildSetting findById(String id) {
|
||||
try (Session session = JpaUtils.openSession()) {
|
||||
Query<OGuildSetting> query = session.createQuery("SELECT o FROM OGuildSetting o WHERE o.id = :id", OGuildSetting.class);
|
||||
query.setParameter("id", id);
|
||||
|
||||
return query.uniqueResult();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBy(String id) {
|
||||
JpaUtils.runWithTransaction(session -> {
|
||||
Query<OGuildSetting> query = session.createQuery("SELECT o FROM OGuildSetting o WHERE o.id = :id", OGuildSetting.class);
|
||||
query.setParameter("id", id);
|
||||
|
||||
OGuildSetting setting = query.uniqueResult();
|
||||
if (setting != null) {
|
||||
session.remove(setting);
|
||||
}
|
||||
});
|
||||
public GuildSettingRepository(SessionProvider sessions) {
|
||||
super(sessions, OGuildSetting.class);
|
||||
}
|
||||
|
||||
public List<OGuildSetting> findByGuild(String guildId) {
|
||||
List<OGuildSetting> settings = new ArrayList<>();
|
||||
|
||||
JpaUtils.runWithSession(session -> {
|
||||
Query<OGuildSetting> query = session.createQuery("SELECT o FROM OGuildSetting o WHERE o.guild.guildId = :guildId", OGuildSetting.class);
|
||||
query.setParameter("guildId", guildId);
|
||||
|
||||
settings.addAll(query.list());
|
||||
});
|
||||
|
||||
return settings;
|
||||
return read(session -> session
|
||||
.createSelectionQuery("FROM OGuildSetting o WHERE o.guild.guildId = :guildId", OGuildSetting.class)
|
||||
.setParameter("guildId", guildId)
|
||||
.getResultList());
|
||||
}
|
||||
|
||||
public void setSettingValue(OGuild oGuild, GSettings setting, String value) {
|
||||
JpaUtils.getWithSession(oGuild, oGuild.getGuildId(), session -> {
|
||||
OGuildSetting oGuildSetting = oGuild.getSettingByName(setting.getName());
|
||||
/**
|
||||
* Stores a non-default value, or deletes the row when the value is {@code null} or equal to the
|
||||
* setting's default. Runs in a single transaction.
|
||||
*
|
||||
* @param guildId the guild id (the guild row must exist)
|
||||
* @param setting the setting
|
||||
* @param value the new value, or {@code null} to reset
|
||||
*/
|
||||
public void upsertOrDelete(String guildId, GSettings setting, String value) {
|
||||
writeVoid(session -> {
|
||||
List<OGuildSetting> existing = session
|
||||
.createNamedSelectionQuery("settings_findByName", OGuildSetting.class)
|
||||
.setParameter("id", guildId)
|
||||
.setParameter("name", setting.getName())
|
||||
.getResultList();
|
||||
|
||||
if (value == null || setting.getDefaultValue().equals(value)) {
|
||||
if (oGuildSetting != null) {
|
||||
session.remove(oGuildSetting);
|
||||
}
|
||||
existing.forEach(session::remove);
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing.isEmpty()) {
|
||||
OGuildSetting guildSetting = new OGuildSetting();
|
||||
guildSetting.setGuild(session.getReference(OGuild.class, guildId));
|
||||
guildSetting.setName(setting.getName());
|
||||
guildSetting.setValue(value);
|
||||
session.persist(guildSetting);
|
||||
} else {
|
||||
if (oGuildSetting == null) {
|
||||
oGuildSetting = new OGuildSetting();
|
||||
oGuildSetting.setGuild(oGuild);
|
||||
oGuildSetting.setName(setting.getName());
|
||||
oGuildSetting.setValue(value);
|
||||
|
||||
session.persist(oGuildSetting);
|
||||
} else {
|
||||
oGuildSetting.setValue(value);
|
||||
|
||||
session.merge(oGuildSetting);
|
||||
}
|
||||
existing.forEach(guildSetting -> guildSetting.setValue(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,105 +1,104 @@
|
||||
package me.bobert.netneutrality.db.repository;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import me.bobert.netneutrality.db.Repository;
|
||||
import me.bobert.netneutrality.db.AbstractRepository;
|
||||
import me.bobert.netneutrality.db.SessionProvider;
|
||||
import me.bobert.netneutrality.db.entities.OGuild;
|
||||
import me.bobert.netneutrality.db.entities.OLogMessage;
|
||||
import me.bobert.netneutrality.db.JpaUtils;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.Message;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.query.Query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MessageRepository implements Repository<OLogMessage> {
|
||||
@Override
|
||||
public List<OLogMessage> findAll() {
|
||||
List<OLogMessage> messages = new ArrayList<>();
|
||||
public class MessageRepository extends AbstractRepository<OLogMessage, Integer> {
|
||||
|
||||
JpaUtils.runWithSession(session -> {
|
||||
Query<OLogMessage> query = session.createQuery("FROM OLogMessage", OLogMessage.class);
|
||||
|
||||
messages.addAll(query.list());
|
||||
});
|
||||
|
||||
return messages;
|
||||
public MessageRepository(SessionProvider sessions) {
|
||||
super(sessions, OLogMessage.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OLogMessage findById(String id) {
|
||||
try (Session session = JpaUtils.openSession()) {
|
||||
Query<OLogMessage> query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.messageId = :messageId", OLogMessage.class);
|
||||
query.setParameter("messageId", id);
|
||||
|
||||
return query.uniqueResult();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBy(String id) {
|
||||
JpaUtils.runWithTransaction(session -> {
|
||||
Query<OLogMessage> query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.messageId = :messageId", OLogMessage.class);
|
||||
query.setParameter("messageId", id);
|
||||
|
||||
OLogMessage oLogMessage = query.uniqueResult();
|
||||
if (oLogMessage != null) {
|
||||
session.remove(oLogMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//TODO: move to a service
|
||||
public void log(OGuild oGuild, MessageChannel channel, Member author, Message message) {
|
||||
JpaUtils.runWithTransaction(session -> {
|
||||
/**
|
||||
* Stores a logged message.
|
||||
*
|
||||
* @param guildId the guild id (the guild row must exist)
|
||||
* @param channelId the channel id
|
||||
* @param userId the author id
|
||||
* @param messageId the Discord message id
|
||||
* @param content the raw message content
|
||||
* @param sentAt when the message was sent
|
||||
* @return the persistent log entry
|
||||
*/
|
||||
public OLogMessage log(String guildId, String channelId, String userId, String messageId, String content, Instant sentAt) {
|
||||
return write(session -> {
|
||||
OLogMessage logMessage = new OLogMessage();
|
||||
logMessage.setGuild(oGuild);
|
||||
logMessage.setMessageId(message.getId());
|
||||
logMessage.setMessage(message.getContentRaw().trim());
|
||||
logMessage.setChannelId(channel.getId());
|
||||
logMessage.setUserId(author.getId());
|
||||
logMessage.setDateSent(message.getTimeCreated().toInstant());
|
||||
|
||||
logMessage.setGuild(session.getReference(OGuild.class, guildId));
|
||||
logMessage.setChannelId(channelId);
|
||||
logMessage.setUserId(userId);
|
||||
logMessage.setMessageId(messageId);
|
||||
logMessage.setMessage(content == null ? null : content.trim());
|
||||
logMessage.setDateSent(sentAt);
|
||||
session.persist(logMessage);
|
||||
return logMessage;
|
||||
});
|
||||
}
|
||||
|
||||
public void logEdit(String messageId, String newMessage) {
|
||||
JpaUtils.runWithTransaction(session -> {
|
||||
Query<OLogMessage> query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.messageId = :messageId", OLogMessage.class);
|
||||
query.setParameter("messageId", messageId);
|
||||
public Optional<OLogMessage> findByMessageId(String messageId) {
|
||||
return read(session -> findByMessageId(session, messageId));
|
||||
}
|
||||
|
||||
OLogMessage message = query.uniqueResult();
|
||||
if (message != null) {
|
||||
public Optional<String> findMessageContent(String messageId) {
|
||||
return findByMessageId(messageId).map(OLogMessage::getMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the edited content of a logged message.
|
||||
*
|
||||
* @param messageId the Discord message id
|
||||
* @param newMessage the edited content
|
||||
* @return {@code true} if the message was found and updated
|
||||
*/
|
||||
public boolean updateEditedMessage(String messageId, String newMessage) {
|
||||
return write(session -> findByMessageId(session, messageId)
|
||||
.map(message -> {
|
||||
message.setEditedMessage(newMessage);
|
||||
|
||||
session.merge(message);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
})
|
||||
.orElse(false));
|
||||
}
|
||||
|
||||
public List<OLogMessage> getMessagesFromUser(String userId, String guildId, int max) {
|
||||
List<OLogMessage> messages = new ArrayList<>();
|
||||
|
||||
JpaUtils.runWithSession(session -> {
|
||||
Query<OLogMessage> query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.guild.guildId = :guildId AND o.userId = :userId", OLogMessage.class);
|
||||
query.setParameter("guildId", guildId);
|
||||
query.setParameter("userId", userId);
|
||||
query.setMaxResults(max);
|
||||
|
||||
messages.addAll(query.list());
|
||||
});
|
||||
|
||||
return messages;
|
||||
public boolean deleteByMessageId(String messageId) {
|
||||
return write(session -> session
|
||||
.createMutationQuery("DELETE FROM OLogMessage o WHERE o.messageId = :messageId")
|
||||
.setParameter("messageId", messageId)
|
||||
.executeUpdate() > 0);
|
||||
}
|
||||
|
||||
public String getMessageString(String messageId) {
|
||||
return findById(messageId).getMessage();
|
||||
/**
|
||||
* Returns the most recent messages of a user in a guild, newest first.
|
||||
*
|
||||
* @param userId the user id
|
||||
* @param guildId the guild id
|
||||
* @param max the maximum number of messages
|
||||
* @return the messages
|
||||
*/
|
||||
public List<OLogMessage> findByUser(String userId, String guildId, int max) {
|
||||
return read(session -> session
|
||||
.createSelectionQuery("FROM OLogMessage o WHERE o.guild.guildId = :guildId AND o.userId = :userId ORDER BY o.dateSent DESC", OLogMessage.class)
|
||||
.setParameter("guildId", guildId)
|
||||
.setParameter("userId", userId)
|
||||
.setMaxResults(max)
|
||||
.getResultList());
|
||||
}
|
||||
|
||||
private static Optional<OLogMessage> findByMessageId(Session session, String messageId) {
|
||||
return session
|
||||
.createSelectionQuery("FROM OLogMessage o WHERE o.messageId = :messageId", OLogMessage.class)
|
||||
.setParameter("messageId", messageId)
|
||||
.uniqueResultOptional();
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,63 @@
|
||||
package me.bobert.netneutrality.db.repository;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
|
||||
import me.bobert.netneutrality.db.Repository;
|
||||
import me.bobert.netneutrality.db.AbstractRepository;
|
||||
import me.bobert.netneutrality.db.SessionProvider;
|
||||
import me.bobert.netneutrality.db.entities.OTask;
|
||||
import me.bobert.netneutrality.db.JpaUtils;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.query.Query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class TaskRepository implements Repository<OTask> {
|
||||
public class TaskRepository extends AbstractRepository<OTask, Integer> {
|
||||
|
||||
@Override
|
||||
public List<OTask> findAll() {
|
||||
List<OTask> tasks = new ArrayList<>();
|
||||
|
||||
JpaUtils.runWithSession(session -> {
|
||||
Query<OTask> query = session.createQuery("FROM OTask", OTask.class);
|
||||
|
||||
tasks.addAll(query.list());
|
||||
});
|
||||
|
||||
return tasks;
|
||||
public TaskRepository(SessionProvider sessions) {
|
||||
super(sessions, OTask.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OTask findById(String id) {
|
||||
try (Session session = JpaUtils.openSession()) {
|
||||
Query<OTask> query = session.createQuery("SELECT o FROM OTask o WHERE o.taskId = :taskId", OTask.class);
|
||||
query.setParameter("taskId", id);
|
||||
|
||||
return query.uniqueResult();
|
||||
}
|
||||
public Optional<OTask> findByTaskId(String taskId) {
|
||||
return read(session -> findByTaskId(session, taskId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBy(String id) {
|
||||
JpaUtils.runWithTransaction(session -> {
|
||||
Query<OTask> query = session.createQuery("SELECT o FROM OTask o WHERE o.taskId = :taskId", OTask.class);
|
||||
query.setParameter("taskId", id);
|
||||
|
||||
OTask oTask = query.uniqueResult();
|
||||
if (oTask != null) {
|
||||
session.remove(oTask);
|
||||
/**
|
||||
* Inserts the task or updates its payload, in a single transaction.
|
||||
*
|
||||
* @param taskId the logical task id
|
||||
* @param typeId the persistable task type id
|
||||
* @param payload the serialized payload
|
||||
* @return the persistent task
|
||||
*/
|
||||
public OTask upsert(String taskId, String typeId, String payload) {
|
||||
return write(session -> {
|
||||
OTask task = findByTaskId(session, taskId).orElse(null);
|
||||
if (task == null) {
|
||||
task = new OTask();
|
||||
task.setTaskId(taskId);
|
||||
task.setTaskClass(typeId);
|
||||
task.setPayload(payload);
|
||||
session.persist(task);
|
||||
} else {
|
||||
task.setPayload(payload);
|
||||
}
|
||||
return task;
|
||||
});
|
||||
}
|
||||
|
||||
public boolean deleteByTaskId(String taskId) {
|
||||
return write(session -> session
|
||||
.createMutationQuery("DELETE FROM OTask o WHERE o.taskId = :taskId")
|
||||
.setParameter("taskId", taskId)
|
||||
.executeUpdate() > 0);
|
||||
}
|
||||
|
||||
private static Optional<OTask> findByTaskId(Session session, String taskId) {
|
||||
return session
|
||||
.createSelectionQuery("FROM OTask o WHERE o.taskId = :taskId", OTask.class)
|
||||
.setParameter("taskId", taskId)
|
||||
.uniqueResultOptional();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.bobert.netneutrality.db.service;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Read-only view of a blacklisted word.
|
||||
*
|
||||
* @param word the word
|
||||
* @param enabled whether the word is currently enforced
|
||||
*/
|
||||
public record BlacklistEntry(String word, boolean enabled) {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package me.bobert.netneutrality.db.service;
|
||||
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Outcome of a blacklist change made through {@link BlacklistService}.
|
||||
*/
|
||||
public enum BlacklistResult {
|
||||
/** The word was added to the blacklist. */
|
||||
ADDED,
|
||||
/** The word existed but was disabled, and is now enabled again. */
|
||||
REENABLED,
|
||||
/** The word was already blacklisted; nothing changed. */
|
||||
ALREADY_EXISTS,
|
||||
/** The word was removed from the blacklist. */
|
||||
REMOVED,
|
||||
/** The word was not blacklisted; nothing changed. */
|
||||
NOT_FOUND
|
||||
}
|
||||
@@ -6,93 +6,108 @@ package me.bobert.netneutrality.db.service;
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||
import me.bobert.netneutrality.db.entities.OBlacklist;
|
||||
import me.bobert.netneutrality.db.entities.OGuild;
|
||||
import me.bobert.netneutrality.db.repository.BlacklistRepository;
|
||||
import me.bobert.netneutrality.db.JpaUtils;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Per-guild blacklist, backed by the {@code blacklist} table and an in-memory cache of the enabled
|
||||
* words. Persistence failures propagate to the caller.
|
||||
*/
|
||||
public class BlacklistService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(BlacklistService.class);
|
||||
private static final Map<String, Set<String>> blacklistCache = new ConcurrentHashMap<>();
|
||||
private static final Logger logger = LogManager.getLogger(BlacklistService.class);
|
||||
private static final Pattern WORD_SEPARATOR = Pattern.compile("[^\\p{L}\\p{N}]+");
|
||||
|
||||
private final BlacklistRepository blacklistRepository;
|
||||
private final GuildService guildService;
|
||||
private volatile Set<String> enabledWords = Set.of();
|
||||
|
||||
public BlacklistService(GuildService guildService) {
|
||||
this.blacklistRepository = new BlacklistRepository();
|
||||
this.guildService = guildService;
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
public BlacklistService(GuildService guildService, BlacklistRepository blacklistRepository) {
|
||||
this.guildService = Objects.requireNonNull(guildService, "guildService");
|
||||
this.blacklistRepository = Objects.requireNonNull(blacklistRepository, "blacklistRepository");
|
||||
reloadBlacklistCache();
|
||||
}
|
||||
|
||||
public void reloadBlacklistCache() {
|
||||
OGuild oGuild = guildService.getEntity();
|
||||
logger.debug("Reloading blacklist cache for guild: {}", oGuild.getGuildId());
|
||||
|
||||
JpaUtils.getWithSession(oGuild, oGuild.getGuildId(), session -> {
|
||||
Set<String> blacklistWords = oGuild.getBlacklists().stream()
|
||||
.filter(OBlacklist::getEnabled)
|
||||
.map(OBlacklist::getWord)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
blacklistCache.put(oGuild.getGuildId(), blacklistWords);
|
||||
});
|
||||
logger.debug("Reloading blacklist cache for guild: {}", guildService.getGuildId());
|
||||
enabledWords = blacklistRepository.findEnabledWords(guildService.getGuildId()).stream()
|
||||
.map(BlacklistService::normalize)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
public CommandExecutionResult add(Member member, String word) {
|
||||
try {
|
||||
blacklistRepository.add(guildService.getEntity(), member, word);
|
||||
reloadBlacklistCache();
|
||||
/**
|
||||
* Adds the word to the blacklist, or re-enables it.
|
||||
*
|
||||
* @param creatorUserId the id of the user adding the word
|
||||
* @param word the word
|
||||
* @return {@link BlacklistResult#ADDED}, {@link BlacklistResult#REENABLED} or
|
||||
* {@link BlacklistResult#ALREADY_EXISTS}
|
||||
*/
|
||||
public BlacklistResult add(String creatorUserId, String word) {
|
||||
guildService.ensureEntity();
|
||||
BlacklistRepository.EnableOutcome outcome =
|
||||
blacklistRepository.enableWord(guildService.getGuildId(), creatorUserId, word);
|
||||
reloadBlacklistCache();
|
||||
|
||||
return CommandExecutionResult.success("Added the word [%s] to the blacklist.", word);
|
||||
} catch (Exception e) {
|
||||
return CommandExecutionResult.warn("Failed to add word to blacklist: " + e.getMessage());
|
||||
}
|
||||
return switch (outcome) {
|
||||
case CREATED -> BlacklistResult.ADDED;
|
||||
case ENABLED -> BlacklistResult.REENABLED;
|
||||
case UNCHANGED -> BlacklistResult.ALREADY_EXISTS;
|
||||
};
|
||||
}
|
||||
|
||||
public CommandExecutionResult delete(String word) {
|
||||
try {
|
||||
blacklistRepository.delete(guildService.getEntity(), word);
|
||||
reloadBlacklistCache();
|
||||
/**
|
||||
* Removes the word from the blacklist.
|
||||
*
|
||||
* @param word the word
|
||||
* @return {@link BlacklistResult#REMOVED} or {@link BlacklistResult#NOT_FOUND}
|
||||
*/
|
||||
public BlacklistResult remove(String word) {
|
||||
boolean deleted = blacklistRepository.deleteWord(guildService.getGuildId(), word);
|
||||
reloadBlacklistCache();
|
||||
|
||||
return CommandExecutionResult.success("Removed the word [%s] to the blacklist.", word);
|
||||
} catch (Exception e) {
|
||||
return CommandExecutionResult.warn("Failed to remove word from blacklist: " + e.getMessage());
|
||||
}
|
||||
return deleted ? BlacklistResult.REMOVED : BlacklistResult.NOT_FOUND;
|
||||
}
|
||||
|
||||
public List<OBlacklist> getBlacklistForGuild() {
|
||||
try {
|
||||
return blacklistRepository.findByGuildId(guildService.getEntity().getGuildId());
|
||||
} catch (Exception e) {
|
||||
return List.of();
|
||||
public List<BlacklistEntry> list() {
|
||||
return blacklistRepository.findByGuildId(guildService.getGuildId()).stream()
|
||||
.map(blacklist -> new BlacklistEntry(blacklist.getWord(), Boolean.TRUE.equals(blacklist.getEnabled())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any word of the text is an enabled blacklisted word (case-insensitive).
|
||||
*
|
||||
* @param text the text to check
|
||||
* @return {@code true} if the text contains a blacklisted word
|
||||
*/
|
||||
public boolean containsBlacklistedWord(String text) {
|
||||
Set<String> words = enabledWords;
|
||||
if (text == null || words.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String token : WORD_SEPARATOR.split(text)) {
|
||||
if (!token.isEmpty() && words.contains(normalize(token))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void removeGuildBlacklist() {
|
||||
logger.info("Clearing blacklist cache for guild: {}", guildService.getEntity().getGuildId());
|
||||
blacklistCache.remove(guildService.getEntity().getGuildId());
|
||||
logger.info("Clearing blacklist cache for guild: {}", guildService.getGuildId());
|
||||
enabledWords = Set.of();
|
||||
}
|
||||
|
||||
public static boolean isWordBlacklisted(String guildId, String word) {
|
||||
Set<String> blacklistWords = blacklistCache.get(guildId);
|
||||
return blacklistWords != null && blacklistWords.contains(word);
|
||||
}
|
||||
|
||||
public static Set<String> getBlacklist(String guildId) {
|
||||
return blacklistCache.getOrDefault(guildId, Collections.emptySet());
|
||||
private static String normalize(String word) {
|
||||
return word.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,13 @@ package me.bobert.netneutrality.db.service;
|
||||
*/
|
||||
|
||||
import me.bobert.netneutrality.db.entities.OGuild;
|
||||
import me.bobert.netneutrality.db.repository.BlacklistRepository;
|
||||
import me.bobert.netneutrality.db.repository.GuildRepository;
|
||||
import me.bobert.netneutrality.db.JpaUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import me.bobert.netneutrality.db.repository.GuildSettingRepository;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Per-guild domain service (blacklist, settings, entity access).
|
||||
@@ -19,16 +22,24 @@ import org.slf4j.LoggerFactory;
|
||||
* {@link me.bobert.netneutrality.db.service.GuildServiceProvider}.
|
||||
*/
|
||||
public class GuildService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GuildService.class);
|
||||
private final GuildRepository guildRepository = new GuildRepository();
|
||||
private static final Logger logger = LogManager.getLogger(GuildService.class);
|
||||
private final String guildId;
|
||||
private final GuildRepository guildRepository;
|
||||
private final BlacklistService blacklistService;
|
||||
private final GuildSettingService guildSettingService;
|
||||
private OGuild oGuild;
|
||||
private volatile OGuild oGuild;
|
||||
|
||||
GuildService(String guildId) {
|
||||
public GuildService(String guildId, GuildRepository guildRepository, BlacklistRepository blacklistRepository,
|
||||
GuildSettingRepository guildSettingRepository) {
|
||||
this.guildId = Objects.requireNonNull(guildId, "guildId");
|
||||
this.guildRepository = Objects.requireNonNull(guildRepository, "guildRepository");
|
||||
this.oGuild = findOrCreateGuild(guildId);
|
||||
this.blacklistService = new BlacklistService(this);
|
||||
this.guildSettingService = new GuildSettingService(this);
|
||||
this.blacklistService = new BlacklistService(this, blacklistRepository);
|
||||
this.guildSettingService = new GuildSettingService(this, guildSettingRepository);
|
||||
}
|
||||
|
||||
public String getGuildId() {
|
||||
return guildId;
|
||||
}
|
||||
|
||||
public OGuild getEntity() {
|
||||
@@ -43,17 +54,23 @@ public class GuildService {
|
||||
return guildSettingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the guild row, creating it when it does not exist (for example after it was deleted
|
||||
* while the service was cached).
|
||||
*
|
||||
* @param guildId the guild id
|
||||
* @return the persistent guild
|
||||
*/
|
||||
public OGuild findOrCreateGuild(String guildId) {
|
||||
OGuild guildEntity = guildRepository.findById(guildId);
|
||||
if (guildEntity == null) {
|
||||
logger.debug("Guild[{}] doesn't exist in DB, creating entity..", guildId);
|
||||
logger.debug("Loading Guild[{}]", guildId);
|
||||
return guildRepository.findOrCreate(guildId);
|
||||
}
|
||||
|
||||
guildEntity = new OGuild();
|
||||
guildEntity.setGuildId(guildId);
|
||||
guildRepository.save(guildEntity);
|
||||
}
|
||||
|
||||
return guildEntity;
|
||||
/**
|
||||
* Makes sure the guild row exists before writing rows that reference it.
|
||||
*/
|
||||
void ensureEntity() {
|
||||
this.oGuild = findOrCreateGuild(guildId);
|
||||
}
|
||||
|
||||
public boolean isDebugEnabled() {
|
||||
@@ -61,14 +78,17 @@ public class GuildService {
|
||||
}
|
||||
|
||||
public void updateDebugMode(boolean debug) {
|
||||
JpaUtils.updateWithTransaction(oGuild, () -> oGuild.setDebug(debug));
|
||||
OGuild current = findOrCreateGuild(guildId);
|
||||
current.setDebug(debug);
|
||||
this.oGuild = guildRepository.save(current);
|
||||
}
|
||||
|
||||
public void deleteGuild() {
|
||||
guildRepository.deleteBy(oGuild.getGuildId());
|
||||
guildRepository.deleteById(guildId);
|
||||
blacklistService.removeGuildBlacklist();
|
||||
}
|
||||
|
||||
public void reloadEntity() {
|
||||
this.oGuild = findOrCreateGuild(oGuild.getGuildId());
|
||||
ensureEntity();
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ package me.bobert.netneutrality.db.service;
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -24,17 +22,16 @@ public interface GuildServiceProvider {
|
||||
GuildService get(String guildId);
|
||||
|
||||
/**
|
||||
* Returns (creating on first use) the service for the given guild.
|
||||
* Drops the cached service for the guild; the next {@link #get(String)} builds a new one.
|
||||
*
|
||||
* @param guild the Discord guild
|
||||
* @return the guild service
|
||||
* @param guildId the Discord guild id
|
||||
*/
|
||||
GuildService get(Guild guild);
|
||||
void evict(String guildId);
|
||||
|
||||
/**
|
||||
* Returns the live cache of guild services.
|
||||
* Returns a read-only snapshot of the cached guild services.
|
||||
*
|
||||
* @return the services keyed by guild id
|
||||
*/
|
||||
Map<String, GuildService> getServices();
|
||||
Map<String, GuildService> snapshot();
|
||||
}
|
||||
@@ -6,9 +6,12 @@ package me.bobert.netneutrality.db.service;
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import me.bobert.netneutrality.db.repository.BlacklistRepository;
|
||||
import me.bobert.netneutrality.db.repository.GuildRepository;
|
||||
import me.bobert.netneutrality.db.repository.GuildSettingRepository;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
@@ -17,22 +20,30 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
public class GuildServiceProviderImpl implements GuildServiceProvider {
|
||||
|
||||
private final Map<String, GuildService> services = new ConcurrentHashMap<>();
|
||||
private final GuildRepository guildRepository;
|
||||
private final BlacklistRepository blacklistRepository;
|
||||
private final GuildSettingRepository guildSettingRepository;
|
||||
|
||||
public GuildServiceProviderImpl() {
|
||||
public GuildServiceProviderImpl(GuildRepository guildRepository, BlacklistRepository blacklistRepository,
|
||||
GuildSettingRepository guildSettingRepository) {
|
||||
this.guildRepository = Objects.requireNonNull(guildRepository, "guildRepository");
|
||||
this.blacklistRepository = Objects.requireNonNull(blacklistRepository, "blacklistRepository");
|
||||
this.guildSettingRepository = Objects.requireNonNull(guildSettingRepository, "guildSettingRepository");
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuildService get(String guildId) {
|
||||
return services.computeIfAbsent(guildId, GuildService::new);
|
||||
return services.computeIfAbsent(guildId,
|
||||
id -> new GuildService(id, guildRepository, blacklistRepository, guildSettingRepository));
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuildService get(Guild guild) {
|
||||
return get(guild.getId());
|
||||
public void evict(String guildId) {
|
||||
services.remove(guildId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, GuildService> getServices() {
|
||||
return services;
|
||||
public Map<String, GuildService> snapshot() {
|
||||
return Map.copyOf(services);
|
||||
}
|
||||
}
|
||||
@@ -9,52 +9,73 @@ package me.bobert.netneutrality.db.service;
|
||||
import me.bobert.netneutrality.db.GSettings;
|
||||
import me.bobert.netneutrality.db.entities.OGuildSetting;
|
||||
import me.bobert.netneutrality.db.repository.GuildSettingRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Per-guild settings, backed by the {@code guild_settings} table. Only non-default values are
|
||||
* stored; reads come from an immutable snapshot that is swapped atomically on reload.
|
||||
*/
|
||||
public class GuildSettingService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GuildSettingService.class);
|
||||
private final GuildSettingRepository guildSettingRepository = new GuildSettingRepository();
|
||||
private static final Logger logger = LogManager.getLogger(GuildSettingService.class);
|
||||
private final GuildSettingRepository guildSettingRepository;
|
||||
private final GuildService guildService;
|
||||
private final Map<String, String> settingsCache = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, String> settingsCache = Map.of();
|
||||
|
||||
public GuildSettingService(GuildService guildService) {
|
||||
this.guildService = guildService;
|
||||
public GuildSettingService(GuildService guildService, GuildSettingRepository guildSettingRepository) {
|
||||
this.guildService = Objects.requireNonNull(guildService, "guildService");
|
||||
this.guildSettingRepository = Objects.requireNonNull(guildSettingRepository, "guildSettingRepository");
|
||||
reloadSettingsCache();
|
||||
}
|
||||
|
||||
public Map<String, String> getSettings() {
|
||||
/**
|
||||
* @return an unmodifiable snapshot of the stored (non-default) settings, keyed by setting name
|
||||
*/
|
||||
public Map<String, String> snapshot() {
|
||||
return settingsCache;
|
||||
}
|
||||
|
||||
public String getSetting(GSettings setting) {
|
||||
String value = settingsCache.get(setting.getName());
|
||||
|
||||
if (value == null) {
|
||||
value = setting.getDefaultValue();
|
||||
}
|
||||
|
||||
return value;
|
||||
/**
|
||||
* @param key the setting
|
||||
* @return the stored value, or empty when the setting uses its default
|
||||
*/
|
||||
public Optional<String> get(GSettings key) {
|
||||
return Optional.ofNullable(settingsCache.get(key.getName()));
|
||||
}
|
||||
|
||||
public void setSettingValue(GSettings setting, String value) {
|
||||
guildSettingRepository.setSettingValue(guildService.getEntity(), setting, value);
|
||||
/**
|
||||
* @param key the setting
|
||||
* @return the stored value, or the setting's default
|
||||
*/
|
||||
public String getOrDefault(GSettings key) {
|
||||
return get(key).orElse(key.getDefaultValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the value; {@code null} or the default value deletes the stored setting.
|
||||
*
|
||||
* @param key the setting
|
||||
* @param value the new value, or {@code null} to reset it
|
||||
*/
|
||||
public void set(GSettings key, String value) {
|
||||
guildService.ensureEntity();
|
||||
guildSettingRepository.upsertOrDelete(guildService.getGuildId(), key, value);
|
||||
|
||||
reloadSettingsCache();
|
||||
}
|
||||
|
||||
public void reloadSettingsCache() {
|
||||
logger.debug("Reloading settings cache for guild: {}", guildService.getEntity().getGuildId());
|
||||
settingsCache.clear();
|
||||
logger.debug("Reloading settings cache for guild: {}", guildService.getGuildId());
|
||||
|
||||
|
||||
List<OGuildSetting> settings = guildSettingRepository.findByGuild(guildService.getEntity().getGuildId());
|
||||
for (OGuildSetting setting : settings) {
|
||||
settingsCache.put(setting.getName(), setting.getValue());
|
||||
Map<String, String> fresh = new HashMap<>();
|
||||
for (OGuildSetting setting : guildSettingRepository.findByGuild(guildService.getGuildId())) {
|
||||
fresh.put(setting.getName(), setting.getValue());
|
||||
}
|
||||
settingsCache = Map.copyOf(fresh);
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,10 @@ public class TaskService {
|
||||
private final TaskRepository taskRepository;
|
||||
private final PersistableTaskRegistry registry;
|
||||
|
||||
public TaskService(TaskManager taskManager, PersistableTaskRegistry registry) {
|
||||
this.taskManager = taskManager;
|
||||
this.taskRepository = new TaskRepository();
|
||||
this.registry = registry;
|
||||
public TaskService(TaskManager taskManager, PersistableTaskRegistry registry, TaskRepository taskRepository) {
|
||||
this.taskManager = Objects.requireNonNull(taskManager, "taskManager");
|
||||
this.registry = Objects.requireNonNull(registry, "registry");
|
||||
this.taskRepository = Objects.requireNonNull(taskRepository, "taskRepository");
|
||||
}
|
||||
|
||||
public void loadTasks() {
|
||||
@@ -40,13 +40,11 @@ public class TaskService {
|
||||
String typeId = oTask.getTaskClass();
|
||||
logger.debug("Trying to load task with ID: {} and type: {}", taskId, typeId);
|
||||
|
||||
String payload = oTask.getPayload();
|
||||
|
||||
try {
|
||||
ObjectNode json = Json.readObject(payload);
|
||||
ObjectNode json = Json.readObject(oTask.getPayload());
|
||||
PersistableTask task = registry.create(typeId, json);
|
||||
|
||||
validateTask(task, oTask);
|
||||
validateTask(task, taskId, json);
|
||||
taskManager.insertPersistableTask(task);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error loading task with ID {}: {}", taskId, e.getMessage());
|
||||
@@ -54,15 +52,11 @@ public class TaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateTask(PersistableTask task, OTask oTask) throws IllegalArgumentException {
|
||||
String taskId = oTask.getTaskId();
|
||||
String payload = oTask.getPayload();
|
||||
|
||||
private static void validateTask(PersistableTask task, String taskId, ObjectNode json) throws IllegalArgumentException {
|
||||
if (!Objects.equals(taskId, task.getId())) {
|
||||
throw new IllegalArgumentException("Task ID mismatch, expected: " + task.getId());
|
||||
}
|
||||
|
||||
ObjectNode json = Json.readObject(payload);
|
||||
String payloadId = json.path("id").asText(null);
|
||||
if (!Objects.equals(payloadId, task.getId())) {
|
||||
throw new IllegalArgumentException("Task payload ID mismatch, expected: " + task.getId());
|
||||
@@ -81,24 +75,12 @@ public class TaskService {
|
||||
return;
|
||||
}
|
||||
|
||||
TaskRepository taskRepository = new TaskRepository();
|
||||
OTask oTask = taskRepository.findById(task.getId());
|
||||
if (oTask == null) {
|
||||
oTask = new OTask();
|
||||
oTask.setTaskId(task.getId());
|
||||
oTask.setTaskClass(typeId);
|
||||
}
|
||||
|
||||
oTask.setPayload(task.toPayload());
|
||||
|
||||
logger.info("Persisting task: {}", oTask.getTaskId());
|
||||
taskRepository.save(oTask);
|
||||
logger.info("Persisting task: {}", task.getId());
|
||||
taskRepository.upsert(task.getId(), typeId, task.toPayload());
|
||||
}
|
||||
|
||||
public void unpersistTask(PersistableTask task) {
|
||||
logger.debug("Unpersisting task: {}", task.getId());
|
||||
TaskRepository taskRepository = new TaskRepository();
|
||||
|
||||
taskRepository.deleteBy(task.getId());
|
||||
taskRepository.deleteByTaskId(task.getId());
|
||||
}
|
||||
}
|
||||
@@ -95,13 +95,29 @@ public class EventsHandler implements EventListener {
|
||||
addListener(ButtonInteractionEvent.class, ButtonTaskContext::new);
|
||||
addListener(MessageReactionAddEvent.class, EmojiTaskContext::new);
|
||||
addGenericListener(GuildJoinEvent.class, event -> {
|
||||
GuildService service = guildServices.get(event.getGuild());
|
||||
GuildService service = guildServices.get(event.getGuild().getId());
|
||||
|
||||
if (service.isDebugEnabled()) {
|
||||
LogHandler.sendMessageToDebugChannel(event.getGuild(), "Error logging enabled on this channel");
|
||||
}
|
||||
});
|
||||
addGenericListener(GuildLeaveEvent.class, event -> guildServices.get(event.getGuild()).deleteGuild());
|
||||
addGenericListener(GuildLeaveEvent.class, event -> onGuildLeave(event.getGuild().getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the guild's data and drops its cached service. The service is evicted even when the
|
||||
* delete fails, so a stale cache entry never outlives the guild membership.
|
||||
*
|
||||
* @param guildId the id of the guild the bot left
|
||||
*/
|
||||
void onGuildLeave(String guildId) {
|
||||
try {
|
||||
guildServices.get(guildId).deleteGuild();
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("Failed to delete data of guild {}", guildId, e);
|
||||
} finally {
|
||||
guildServices.evict(guildId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,14 +52,14 @@ public final class ActionRoutes {
|
||||
|
||||
routes.get("/actions/settings/current/{guildId}", ctx -> {
|
||||
Guild guild = GuildRoutes.guild(jdaSupplier, ctx);
|
||||
Map<String, String> settings = guildServices.get(guild).getGuildSettingService().getSettings();
|
||||
Map<String, String> settings = guildServices.get(guild.getId()).getGuildSettingService().snapshot();
|
||||
|
||||
ctx.json(Map.of("status", "ok", "settings", settings));
|
||||
});
|
||||
|
||||
routes.post("/actions/settings/reload/{guildId}", ctx -> {
|
||||
Guild guild = GuildRoutes.guild(jdaSupplier, ctx);
|
||||
guildServices.get(guild).getGuildSettingService().reloadSettingsCache();
|
||||
guildServices.get(guild.getId()).getGuildSettingService().reloadSettingsCache();
|
||||
|
||||
ctx.json(Map.of("status", "true"));
|
||||
});
|
||||
|
||||
@@ -259,7 +259,7 @@ public class MusicManager implements MusicManagerCallback {
|
||||
|
||||
private static int parseBaseVolume(GuildService guildService) {
|
||||
try {
|
||||
return Integer.parseInt(guildService.getGuildSettingService().getSetting(GSettings.MUSIC_VOLUME));
|
||||
return Integer.parseInt(guildService.getGuildSettingService().getOrDefault(GSettings.MUSIC_VOLUME));
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Invalid MUSIC_VOLUME setting, falling back to default ({})", DEFAULT_VOLUME, e);
|
||||
return DEFAULT_VOLUME;
|
||||
|
||||
@@ -105,7 +105,7 @@ public class MusicServiceImpl implements MusicService {
|
||||
public MusicManager get(Guild guild) {
|
||||
return managers.computeIfAbsent(
|
||||
guild.getId(),
|
||||
id -> new MusicManager(guild, client, guildServices.get(guild), taskManager)
|
||||
id -> new MusicManager(guild, client, guildServices.get(id), taskManager)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta
|
||||
int searchResultsLimit = 5; // Max number of buttons per row
|
||||
int maxPlaylistSizeBeforeAsking = Integer.parseInt(musicManager.getGuildService()
|
||||
.getGuildSettingService()
|
||||
.getSetting(GSettings.MUSIC_MAXPLAYLISTSIZE));
|
||||
.getOrDefault(GSettings.MUSIC_MAXPLAYLISTSIZE));
|
||||
|
||||
// Search results
|
||||
if (searchResult) {
|
||||
|
||||
Reference in new issue
Block a user