From b8cd1a67c7eeb9932041b211a747a2a3b800e684 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Sat, 24 Aug 2024 17:01:05 +0300 Subject: [PATCH] Move thread pings and logging channels to a separate db --- .../java/net/neoforged/camelot/Database.java | 99 +++++++++++++++++-- .../commands/utility/ThreadPingsCommand.java | 39 +++----- .../camelot/listener/ThreadPingsListener.java | 4 +- .../neoforged/camelot/log/ChannelLogging.java | 4 +- .../camelot/module/BuiltInModule.java | 12 ++- .../camelot/module/LoggingModule.java | 4 +- .../camelot/module/ThreadPingsModule.java | 8 +- .../db/config/V1__logging_threadpings.sql | 13 +++ .../resources/db/main/V14__move_configs.sql | 1 + .../resources/db/pings/V3__move_configs.sql | 1 + 10 files changed, 138 insertions(+), 47 deletions(-) create mode 100644 src/main/resources/db/config/V1__logging_threadpings.sql create mode 100644 src/main/resources/db/main/V14__move_configs.sql create mode 100644 src/main/resources/db/pings/V3__move_configs.sql diff --git a/src/main/java/net/neoforged/camelot/Database.java b/src/main/java/net/neoforged/camelot/Database.java index a67ed8b..736ec3c 100644 --- a/src/main/java/net/neoforged/camelot/Database.java +++ b/src/main/java/net/neoforged/camelot/Database.java @@ -1,14 +1,20 @@ package net.neoforged.camelot; +import net.neoforged.camelot.configuration.Common; import net.neoforged.camelot.db.api.CallbackConfig; import net.neoforged.camelot.db.api.StringSearch; import net.neoforged.camelot.db.impl.PostCallbackDecorator; +import net.neoforged.camelot.db.transactionals.LoggingChannelsDAO; +import net.neoforged.camelot.db.transactionals.ThreadPingsDAO; import net.neoforged.camelot.listener.CustomPingListener; import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.callback.Context; +import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.configuration.FluentConfiguration; import org.jdbi.v3.core.Jdbi; import org.jdbi.v3.core.argument.AbstractArgumentFactory; import org.jdbi.v3.core.argument.Argument; -import org.jdbi.v3.core.argument.ArgumentFactory; import org.jdbi.v3.core.argument.Arguments; import org.jdbi.v3.core.config.ConfigRegistry; import org.jdbi.v3.sqlobject.HandlerDecorators; @@ -16,13 +22,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sqlite.SQLiteDataSource; -import net.neoforged.camelot.configuration.Common; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.sql.SQLType; +import java.sql.Connection; +import java.sql.SQLException; import java.sql.Types; +import java.util.function.UnaryOperator; /** * The class where the bot databases are stored. @@ -30,6 +37,18 @@ public class Database { public static final Logger LOGGER = LoggerFactory.getLogger(Common.NAME + " database"); + /** + * Static JDBI config instance. Can be accessed via {@link #config()}. + */ + private static Jdbi config; + + /** + * {@return the static config JDBI instance} + */ + public static Jdbi config() { + return config; + } + /** * Static JDBI main instance. Can be accessed via {@link #main()}. */ @@ -89,19 +108,47 @@ static void init() throws IOException { } } - main = createDatabaseConnection(mainDb, "main"); - pings = createDatabaseConnection(dir.resolve("pings.db"), "pings"); + config = createDatabaseConnection(dir.resolve("configuration.db"), "config"); + + main = createDatabaseConnection(mainDb, "main", flyway -> flyway + .callbacks(schemaMigrationCallback(14, connection -> { + LOGGER.info("Migrating logging channels from main.db to configuration.db"); + try (var stmt = connection.createStatement()) { + var rs = stmt.executeQuery("select type, channel from logging_channels"); + config.useExtension(LoggingChannelsDAO.class, extension -> { + while (rs.next()) { + extension.insert(rs.getLong(2), LoggingChannelsDAO.Type.values()[rs.getInt(1)]); + } + }); + } + }))); + pings = createDatabaseConnection(dir.resolve("pings.db"), "pings", flyway -> flyway + .callbacks(schemaMigrationCallback(3, connection -> { + LOGGER.info("Migrating thread pings from pings.db to configuration.db"); + try (var stmt = connection.createStatement()) { + var rs = stmt.executeQuery("select channel, role from thread_pings"); + config.useExtension(ThreadPingsDAO.class, extension -> { + while (rs.next()) { + extension.add(rs.getLong(1), rs.getLong(2)); + } + }); + } + }))); appeals = createDatabaseConnection(dir.resolve("appeals.db"), "appeals"); stats = createDatabaseConnection(dir.resolve("stats.db"), "stats"); CustomPingListener.requestRefresh(); } + public static Jdbi createDatabaseConnection(Path dbPath, String flywayLocation) { + return createDatabaseConnection(dbPath, flywayLocation, UnaryOperator.identity()); + } + /** * Sets up a connection to the SQLite database located at the {@code dbPath}, migrating it, if necessary. * * @return a JDBI connection to the database */ - public static Jdbi createDatabaseConnection(Path dbPath, String flywayLocation) { + public static Jdbi createDatabaseConnection(Path dbPath, String flywayLocation, UnaryOperator flywayConfig) { dbPath = dbPath.toAbsolutePath(); if (!Files.exists(dbPath)) { try { @@ -119,9 +166,9 @@ public static Jdbi createDatabaseConnection(Path dbPath, String flywayLocation) dataSource.setCaseSensitiveLike(false); LOGGER.info("Initiating SQLite database connection at {}.", url); - final var flyway = Flyway.configure() - .dataSource(dataSource) - .locations("classpath:db/" + flywayLocation) + final var flyway = flywayConfig.apply(Flyway.configure() + .dataSource(dataSource) + .locations("classpath:db/" + flywayLocation)) .load(); flyway.migrate(); @@ -137,4 +184,38 @@ protected Argument build(StringSearch value, ConfigRegistry config) { return jdbi; } + private static Callback schemaMigrationCallback(int version, BeforeMigrationHandler consumer) { + return new Callback() { + @Override + public boolean supports(Event event, Context context) { + return event == Event.BEFORE_EACH_MIGRATE; + } + + @Override + public boolean canHandleInTransaction(Event event, Context context) { + return true; + } + + @Override + public void handle(Event event, Context context) { + if (context.getMigrationInfo().getVersion().getMajor().intValue() == version) { + try { + consumer.handle(context.getConnection()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public String getCallbackName() { + return "before_migrate_schema_v" + version; + } + }; + } + + @FunctionalInterface + private interface BeforeMigrationHandler { + void handle(Connection connection) throws SQLException; + } } diff --git a/src/main/java/net/neoforged/camelot/commands/utility/ThreadPingsCommand.java b/src/main/java/net/neoforged/camelot/commands/utility/ThreadPingsCommand.java index 46203b1..849c63a 100644 --- a/src/main/java/net/neoforged/camelot/commands/utility/ThreadPingsCommand.java +++ b/src/main/java/net/neoforged/camelot/commands/utility/ThreadPingsCommand.java @@ -2,7 +2,6 @@ import com.jagrosh.jdautilities.command.SlashCommand; import com.jagrosh.jdautilities.command.SlashCommandEvent; -import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.IMentionable; import net.dv8tion.jda.api.entities.ISnowflake; import net.dv8tion.jda.api.entities.Role; @@ -15,6 +14,7 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu; import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.SelectTarget; @@ -31,20 +31,12 @@ import java.util.Objects; import java.util.stream.Collectors; -public class ThreadPingsCommand extends InteractiveCommand { +public abstract class ThreadPingsCommand extends InteractiveCommand { private static final Logger LOGGER = LoggerFactory.getLogger(ThreadPingsCommand.class); + private static final SubcommandGroupData GROUP_DATA = new SubcommandGroupData("thread-pings", "Commands related to thread pings configuration"); public ThreadPingsCommand() { - this.name = "thread-pings"; - this.guildOnly = true; - this.children = new SlashCommand[]{ - new ConfigureChannel(), - new ConfigureGuild(), - new View(), - }; - this.userPermissions = new Permission[] { - Permission.MESSAGE_MANAGE - }; + this.subcommandGroup = GROUP_DATA; } @Override @@ -62,7 +54,7 @@ protected void onEntitySelect(EntitySelectInteractionEvent event, String[] argum final GuildChannel channel = event.getJDA().getGuildChannelById(channelId); if (!isGuildId && channel == null) { LOGGER.info("Received interaction for non-existent channel {}; deleting associated pings from database", channelId); - Database.pings().useExtension(ThreadPingsDAO.class, threadPings -> threadPings.clearChannel(channelId)); + Database.config().useExtension(ThreadPingsDAO.class, threadPings -> threadPings.clearChannel(channelId)); return; } @@ -73,7 +65,7 @@ protected void onEntitySelect(EntitySelectInteractionEvent event, String[] argum .map(ISnowflake::getIdLong) .toList(); - Database.pings().useExtension(ThreadPingsDAO.class, threadPings -> { + Database.config().useExtension(ThreadPingsDAO.class, threadPings -> { final List existingRoles = threadPings.query(channelId); for (Long existingRoleId : existingRoles) { @@ -92,7 +84,7 @@ protected void onEntitySelect(EntitySelectInteractionEvent event, String[] argum event.getInteraction().editMessage(buildMessage(isGuildId ? "this guild" : channel.getAsMention(), roles)).queue(); } - public class ConfigureChannel extends SlashCommand { + public static class ConfigureChannel extends ThreadPingsCommand { public ConfigureChannel() { this.name = "configure-channel"; this.help = "Configure roles to be pinged in threads made under a channel"; @@ -107,7 +99,7 @@ protected void execute(SlashCommandEvent event) { if (result == null) return; result.interaction.getHook().editOriginal(buildMessage(result.channel.getAsMention(), result.roles)) .setComponents(ActionRow.of( - EntitySelectMenu.create(ThreadPingsCommand.super.getComponentId(result.channel.getId()), + EntitySelectMenu.create(getComponentId(result.channel.getId()), SelectTarget.ROLE) .setMinValues(0) .setMaxValues(SelectMenu.OPTIONS_MAX_AMOUNT) @@ -117,7 +109,7 @@ protected void execute(SlashCommandEvent event) { } } - public class ConfigureGuild extends SlashCommand { + public static class ConfigureGuild extends ThreadPingsCommand { public ConfigureGuild() { this.name = "configure-guild"; this.help = "Configure roles to be pinged in threads made under this guild"; @@ -129,7 +121,7 @@ protected void execute(SlashCommandEvent event) { assert event.getGuild() != null; final long guildId = event.getGuild().getIdLong(); - final List roles = Database.pings().withExtension(ThreadPingsDAO.class, + final List roles = Database.config().withExtension(ThreadPingsDAO.class, threadPings -> threadPings.query(guildId)) .stream() .map(id -> event.getJDA().getRoleById(id)) @@ -138,8 +130,7 @@ protected void execute(SlashCommandEvent event) { event.getInteraction().getHook().editOriginal(buildMessage("this guild", roles)) .setComponents(ActionRow.of( - EntitySelectMenu.create(ThreadPingsCommand.super.getComponentId(guildId), - SelectTarget.ROLE) + EntitySelectMenu.create(getComponentId(guildId), SelectTarget.ROLE) .setMinValues(0) .setMaxValues(SelectMenu.OPTIONS_MAX_AMOUNT) .build() @@ -151,7 +142,7 @@ protected void execute(SlashCommandEvent event) { public static class View extends SlashCommand { public View() { this.name = "view"; - this.help = "View roles to be pinged in threads made under a channel"; + this.help = "View roles to be pinged in threads made under a channel"; this.options = List.of( new OptionData(OptionType.CHANNEL, "channel", "The channel", true) ); @@ -182,7 +173,7 @@ protected void execute(SlashCommandEvent event) { if (result.channel instanceof StandardGuildChannel guildChannel && guildChannel.getParentCategory() != null) { final var parentCategory = guildChannel.getParentCategory(); - final List categoryRoles = Database.pings().withExtension(ThreadPingsDAO.class, + final List categoryRoles = Database.config().withExtension(ThreadPingsDAO.class, threadPings -> threadPings.query(parentCategory.getIdLong())) .stream() .map(id -> event.getJDA().getRoleById(id)) @@ -200,7 +191,7 @@ protected void execute(SlashCommandEvent event) { } } - final List guildRoles = Database.pings().withExtension(ThreadPingsDAO.class, + final List guildRoles = Database.config().withExtension(ThreadPingsDAO.class, threadPings -> threadPings.query(result.channel.getGuild().getIdLong())) .stream() .map(id -> event.getJDA().getRoleById(id)) @@ -235,7 +226,7 @@ private static CommonResult executeCommon(SlashCommandEvent event) { return null; } - final List roles = Database.pings().withExtension(ThreadPingsDAO.class, + final List roles = Database.config().withExtension(ThreadPingsDAO.class, threadPings -> threadPings.query(channel.getIdLong())) .stream() .map(id -> event.getJDA().getRoleById(id)) diff --git a/src/main/java/net/neoforged/camelot/listener/ThreadPingsListener.java b/src/main/java/net/neoforged/camelot/listener/ThreadPingsListener.java index a6ad28f..3317f66 100644 --- a/src/main/java/net/neoforged/camelot/listener/ThreadPingsListener.java +++ b/src/main/java/net/neoforged/camelot/listener/ThreadPingsListener.java @@ -41,7 +41,7 @@ public void onEvent(@NotNull GenericEvent gevent) { final ThreadChannel thread = event.getChannel().asThreadChannel(); final List roleIds = new ArrayList<>(); - Database.pings().useExtension(ThreadPingsDAO.class, threadPings -> { + Database.config().useExtension(ThreadPingsDAO.class, threadPings -> { // Check the thread's parent channel final IThreadContainerUnion parentChannel = thread.getParentChannel(); roleIds.addAll(threadPings.query(parentChannel.getIdLong())); @@ -63,7 +63,7 @@ public void onEvent(@NotNull GenericEvent gevent) { final Role role = thread.getGuild().getRoleById(roleId); if (role == null) { LOGGER.info("Role {} does not exist; deleting role from database", roleId); - Database.pings().useExtension(ThreadPingsDAO.class, threadPings -> threadPings.clearRole(roleId)); + Database.config().useExtension(ThreadPingsDAO.class, threadPings -> threadPings.clearRole(roleId)); continue; } roles.add(role); diff --git a/src/main/java/net/neoforged/camelot/log/ChannelLogging.java b/src/main/java/net/neoforged/camelot/log/ChannelLogging.java index 27aece5..aa2abdf 100644 --- a/src/main/java/net/neoforged/camelot/log/ChannelLogging.java +++ b/src/main/java/net/neoforged/camelot/log/ChannelLogging.java @@ -59,7 +59,7 @@ public void withChannel(Consumer consumer) { } else if (!acnowledgedUnknownChannel) { acnowledgedUnknownChannel = true; BotMain.LOGGER.warn("Unknown logging channel with id '{}'", channelId); - Database.main().useExtension(LoggingChannelsDAO.class, db -> db.removeAll(channelId)); + Database.config().useExtension(LoggingChannelsDAO.class, db -> db.removeAll(channelId)); } }); } @@ -68,6 +68,6 @@ public void withChannel(Consumer consumer) { * {@return the channels associated with this logging type} */ public List getChannels() { - return Database.main().withExtension(LoggingChannelsDAO.class, db -> db.getChannelsForType(type)); + return Database.config().withExtension(LoggingChannelsDAO.class, db -> db.getChannelsForType(type)); } } diff --git a/src/main/java/net/neoforged/camelot/module/BuiltInModule.java b/src/main/java/net/neoforged/camelot/module/BuiltInModule.java index 12fdaa1..35fbb1e 100644 --- a/src/main/java/net/neoforged/camelot/module/BuiltInModule.java +++ b/src/main/java/net/neoforged/camelot/module/BuiltInModule.java @@ -14,6 +14,7 @@ import net.neoforged.camelot.util.Emojis; import java.util.ArrayList; +import java.util.Arrays; /** * A module that provides builtin objects and arguments. @@ -29,7 +30,13 @@ public BuiltInModule() { @Override public void registerCommands(CommandClientBuilder builder) { var kids = new ArrayList(); - BotMain.propagateParameter(CONFIGURATION_COMMANDS, kids::add); + BotMain.propagateParameter(CONFIGURATION_COMMANDS, new ConfigCommandBuilder() { + @Override + public ConfigCommandBuilder accept(SlashCommand... child) { + kids.addAll(Arrays.asList(child)); + return this; + } + }); if (!kids.isEmpty()) { builder.addSlashCommand(new SlashCommand() { { @@ -38,6 +45,7 @@ public void registerCommands(CommandClientBuilder builder) { this.userPermissions = new Permission[] { Permission.MANAGE_SERVER }; + this.guildOnly = true; this.children = kids.toArray(SlashCommand[]::new); } @@ -60,6 +68,6 @@ public String id() { } public interface ConfigCommandBuilder { - void accept(SlashCommand child); + ConfigCommandBuilder accept(SlashCommand... child); } } diff --git a/src/main/java/net/neoforged/camelot/module/LoggingModule.java b/src/main/java/net/neoforged/camelot/module/LoggingModule.java index eeab777..aa0eeff 100644 --- a/src/main/java/net/neoforged/camelot/module/LoggingModule.java +++ b/src/main/java/net/neoforged/camelot/module/LoggingModule.java @@ -40,7 +40,7 @@ public LoggingModule() { @Override protected void execute(SlashCommandEvent event) { - var types = Database.main().withExtension(LoggingChannelsDAO.class, db -> db.getTypesForChannel(event.getChannel().getIdLong())); + var types = Database.config().withExtension(LoggingChannelsDAO.class, db -> db.getTypesForChannel(event.getChannel().getIdLong())); var builder = StringSelectMenu.create(getComponentId()) .setMaxValues(LoggingChannelsDAO.Type.values().length) .setMinValues(0); @@ -59,7 +59,7 @@ protected void execute(SlashCommandEvent event) { @Override protected void onStringSelect(StringSelectInteractionEvent event, String[] arguments) { - Database.main().useExtension(LoggingChannelsDAO.class, db -> { + Database.config().useExtension(LoggingChannelsDAO.class, db -> { db.removeAll(event.getChannelIdLong()); event.getValues().stream() .map(LoggingChannelsDAO.Type::valueOf) diff --git a/src/main/java/net/neoforged/camelot/module/ThreadPingsModule.java b/src/main/java/net/neoforged/camelot/module/ThreadPingsModule.java index 542ece5..89a843e 100644 --- a/src/main/java/net/neoforged/camelot/module/ThreadPingsModule.java +++ b/src/main/java/net/neoforged/camelot/module/ThreadPingsModule.java @@ -1,7 +1,6 @@ package net.neoforged.camelot.module; import com.google.auto.service.AutoService; -import com.jagrosh.jdautilities.command.CommandClientBuilder; import net.dv8tion.jda.api.JDABuilder; import net.neoforged.camelot.commands.utility.ThreadPingsCommand; import net.neoforged.camelot.config.module.ThreadPings; @@ -20,6 +19,8 @@ public class ThreadPingsModule extends CamelotModule.Base { public ThreadPingsModule() { super(ThreadPings.class); + accept(BuiltInModule.CONFIGURATION_COMMANDS, configCommandBuilder -> configCommandBuilder + .accept(new ThreadPingsCommand.ConfigureChannel(), new ThreadPingsCommand.ConfigureGuild(), new ThreadPingsCommand.View())); } @Override @@ -27,11 +28,6 @@ public String id() { return "thread-pings"; } - @Override - public void registerCommands(CommandClientBuilder builder) { - builder.addSlashCommand(new ThreadPingsCommand()); - } - @Override public void registerListeners(JDABuilder builder) { builder.addEventListeners(new ThreadPingsListener()); diff --git a/src/main/resources/db/config/V1__logging_threadpings.sql b/src/main/resources/db/config/V1__logging_threadpings.sql new file mode 100644 index 0000000..322a73a --- /dev/null +++ b/src/main/resources/db/config/V1__logging_threadpings.sql @@ -0,0 +1,13 @@ +create table logging_channels +( + type tinyint not null, + channel unsigned big int not null, + constraint logging_channels_keys primary key (type, channel) +); + +create table thread_pings +( + channel unsigned big int not null, + role unsigned big int not null, + constraint thread_pings_pk primary key (channel, role) +); diff --git a/src/main/resources/db/main/V14__move_configs.sql b/src/main/resources/db/main/V14__move_configs.sql new file mode 100644 index 0000000..685488d --- /dev/null +++ b/src/main/resources/db/main/V14__move_configs.sql @@ -0,0 +1 @@ +drop table logging_channels; diff --git a/src/main/resources/db/pings/V3__move_configs.sql b/src/main/resources/db/pings/V3__move_configs.sql new file mode 100644 index 0000000..fb4a60a --- /dev/null +++ b/src/main/resources/db/pings/V3__move_configs.sql @@ -0,0 +1 @@ +drop table thread_pings;