diff --git a/src/main/java/creator/loki/core/ListenerAggregator.java b/src/main/java/creator/loki/core/ListenerAggregator.java index 6e861b7..5a420b9 100644 --- a/src/main/java/creator/loki/core/ListenerAggregator.java +++ b/src/main/java/creator/loki/core/ListenerAggregator.java @@ -6,6 +6,7 @@ import creator.loki.bootstrap.ReadyListener; import creator.loki.feature.counter.service.MemberCounterListener; import creator.loki.feature.economy.FishMessageListener; import creator.loki.feature.help.HelpMenuHandler; +import creator.loki.feature.reactroles.ReactionRoleListener; import creator.loki.feature.image.ImageNavigationHandler; import creator.loki.feature.leaderboard.LeaderboardButtonListener; import creator.loki.feature.level.listener.MessageListener; @@ -13,8 +14,10 @@ import creator.loki.feature.level.listener.ResetLevelsButtonHandler; import creator.loki.feature.level.service.VoiceTracker; import creator.loki.feature.moderation.commands.utils.ClearSlashCommands; import creator.loki.feature.moderation.log.ModerationLogger; -import creator.loki.feature.music.listener.MusicSearchButtonHandler; +import creator.loki.feature.config.SetupSlashHandler; import creator.loki.feature.music.MusicSlashCommands; +import creator.loki.feature.music.listener.PlaySelectHandler; +import creator.loki.feature.ticket.TicketListener; import creator.loki.feature.roast.AndrertListener; import creator.loki.feature.roast.HelperListener; import creator.loki.feature.voice.handler.VoiceButtonHandler; @@ -43,13 +46,16 @@ public class ListenerAggregator { ResetLevelsButtonHandler resetLevelsButtonHandler, LeaderboardButtonListener leaderboardButtonListener, MessageCache messageCache, - MusicSearchButtonHandler musicSearchButtonHandler, MusicSlashCommands musicSlashCommands, + PlaySelectHandler playSelectHandler, FishMessageListener fishMessageListener, AndrertListener andrertListener, ClearSlashCommands clearSlashCommands, HelperListener helperListener, - ImageNavigationHandler imageNavigationHandler) { + ImageNavigationHandler imageNavigationHandler, + SetupSlashHandler setupSlashHandler, + TicketListener ticketListener, + ReactionRoleListener reactionRoleListener) { this.listeners = new Object[] { readyListener, @@ -66,13 +72,16 @@ public class ListenerAggregator { resetLevelsButtonHandler, leaderboardButtonListener, messageCache, - musicSearchButtonHandler, musicSlashCommands, + playSelectHandler, fishMessageListener, andrertListener, clearSlashCommands, helperListener, - imageNavigationHandler + imageNavigationHandler, + setupSlashHandler, + ticketListener, + reactionRoleListener }; } diff --git a/src/main/java/creator/loki/database/schema/FeatureTables.java b/src/main/java/creator/loki/database/schema/FeatureTables.java index 6d32c4b..ce867b2 100644 --- a/src/main/java/creator/loki/database/schema/FeatureTables.java +++ b/src/main/java/creator/loki/database/schema/FeatureTables.java @@ -103,6 +103,27 @@ public final class FeatureTables { left_count INTEGER DEFAULT 0, difference INTEGER DEFAULT 0 )"""; + private static final String REACTION_ROLES = + """ + CREATE TABLE IF NOT EXISTS reaction_roles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + message_id TEXT NOT NULL, + emoji TEXT NOT NULL, + role_id TEXT NOT NULL, + UNIQUE(message_id, emoji) + )"""; + private static final String TICKETS = + """ + CREATE TABLE IF NOT EXISTS tickets ( + channel_id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL, + creator_id TEXT NOT NULL, + created_at INTEGER NOT NULL, + closed INTEGER DEFAULT 0, + claimed_by TEXT + )"""; private static final String RATINGS = """ CREATE TABLE IF NOT EXISTS ratings ( @@ -123,7 +144,9 @@ public final class FeatureTables { database.execute(VOICE_CHANNELS); database.execute(VOICE_PRESETS); database.execute(COUNTER_STATS); + database.execute(TICKETS); database.execute(RATINGS); + database.execute(REACTION_ROLES); createIndexes(database); } diff --git a/src/main/java/creator/loki/di/BotModule.java b/src/main/java/creator/loki/di/BotModule.java index 958a349..349231a 100644 --- a/src/main/java/creator/loki/di/BotModule.java +++ b/src/main/java/creator/loki/di/BotModule.java @@ -14,6 +14,7 @@ import creator.loki.core.registry.PrefixCommandRegistrar; import creator.loki.core.security.CooldownManager; import creator.loki.core.security.RateLimiter; import creator.loki.database.DatabaseModule; +import creator.loki.feature.config.ConfigModule; import creator.loki.feature.counter.CounterModule; import creator.loki.feature.economy.EconomyModule; import creator.loki.feature.entertainment.EntertainmentModule; @@ -29,6 +30,8 @@ import creator.loki.feature.radio.RadioModule; import creator.loki.feature.roast.RoastModule; import creator.loki.feature.role.RoleModule; import creator.loki.feature.tiktok.TikTokModule; +import creator.loki.feature.ticket.TicketModule; +import creator.loki.feature.reactroles.ReactionRolesModule; import creator.loki.feature.utility.UtilityModule; import creator.loki.feature.voice.VoiceModule; import creator.loki.feature.welcome.WelcomeModule; @@ -47,6 +50,7 @@ public class BotModule extends AbstractModule { bind(PrefixCommandRegistrar.class).in(Singleton.class); install(new DatabaseModule()); + install(new ConfigModule()); install(new VoiceModule()); install(new MusicModule()); install(new LevelModule()); @@ -62,7 +66,9 @@ public class BotModule extends AbstractModule { install(new ImageModule()); install(new TikTokModule()); install(new EntertainmentModule()); + install(new TicketModule()); install(new UtilityModule()); + install(new ReactionRolesModule()); install(new LifecycleModule()); } diff --git a/src/main/java/creator/loki/feature/config/ConfigModule.java b/src/main/java/creator/loki/feature/config/ConfigModule.java new file mode 100644 index 0000000..387251d --- /dev/null +++ b/src/main/java/creator/loki/feature/config/ConfigModule.java @@ -0,0 +1,11 @@ +package creator.loki.feature.config; + +import com.google.inject.AbstractModule; +import com.google.inject.Singleton; + +public class ConfigModule extends AbstractModule { + @Override + protected void configure() { + bind(SetupSlashHandler.class).in(Singleton.class); + } +} diff --git a/src/main/java/creator/loki/feature/config/SetupSlashHandler.java b/src/main/java/creator/loki/feature/config/SetupSlashHandler.java new file mode 100644 index 0000000..8d16874 --- /dev/null +++ b/src/main/java/creator/loki/feature/config/SetupSlashHandler.java @@ -0,0 +1,432 @@ +package creator.loki.feature.config; + +import java.awt.Color; +import java.util.Map; +import java.util.stream.Collectors; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import creator.loki.feature.config.repository.ConfigRepository; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.components.actionrow.ActionRow; +import net.dv8tion.jda.api.components.buttons.Button; +import net.dv8tion.jda.api.components.label.Label; +import net.dv8tion.jda.api.components.selections.EntitySelectMenu; +import net.dv8tion.jda.api.components.selections.StringSelectMenu; +import net.dv8tion.jda.api.components.textinput.TextInput; +import net.dv8tion.jda.api.components.textinput.TextInputStyle; +import net.dv8tion.jda.api.events.guild.GuildReadyEvent; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.EntitySelectInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.modals.Modal; +import org.jetbrains.annotations.NotNull; + +@Singleton +public class SetupSlashHandler extends ListenerAdapter { + private static final Color BLURPLE = new Color(88, 101, 242); + + private final ConfigRepository configRepo; + private volatile boolean registered = false; + + @Inject + public SetupSlashHandler(ConfigRepository configRepo) { + this.configRepo = configRepo; + } + + @Override + public void onGuildReady(@NotNull GuildReadyEvent event) { + if (registered) return; + registered = true; + event.getJDA().updateCommands() + .addCommands(Commands.slash("setup", "Панель управления сервером")) + .queue(); + } + + @Override + public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) { + if (!event.getName().equals("setup")) return; + event.deferReply().queue(); + showDashboard(event.getGuild(), event.getHook()); + } + + @Override + public void onButtonInteraction(@NotNull ButtonInteractionEvent event) { + String id = event.getComponentId(); + if (!id.startsWith("setup_")) return; + event.deferEdit().queue(); + + switch (id) { + case "setup_voice" -> showVoicePanel(event.getGuild(), event.getHook()); + case "setup_counter" -> showCounterPanel(event.getGuild(), event.getHook()); + case "setup_logs" -> showLoggingPanel(event.getGuild(), event.getHook()); + case "setup_levelroles" -> showLevelRolesPanel(event.getGuild(), event.getHook()); + case "setup_back" -> showDashboard(event.getGuild(), event.getHook()); + case "setup_counter_format" -> openCounterFormatModal(event); + case "setup_levelrole_add" -> openLevelRoleAddModal(event); + case "setup_levelrole_remove" -> openLevelRoleRemoveSelect(event); + } + } + + @Override + public void onEntitySelectInteraction(@NotNull EntitySelectInteractionEvent event) { + String id = event.getComponentId(); + if (!id.startsWith("setup_")) return; + event.deferEdit().queue(); + + String value = event.getValues().get(0).getId(); + + switch (id) { + case "setup_voice_trigger" -> { + configRepo.set("voice.trigger_channel_id", value); + showVoicePanel(event.getGuild(), event.getHook()); + } + case "setup_voice_category" -> { + configRepo.set("voice.category_id", value); + showVoicePanel(event.getGuild(), event.getHook()); + } + case "setup_counter_channel" -> { + configRepo.set("counter.channel_id", value); + updateCounterChannelName(event.getGuild()); + showCounterPanel(event.getGuild(), event.getHook()); + } + case "setup_logs_moderation" -> { + configRepo.set("moderation.log_channel_id", value); + showLoggingPanel(event.getGuild(), event.getHook()); + } + case "setup_logs_levelup" -> { + configRepo.set("levelup.notification_channel_id", value); + showLoggingPanel(event.getGuild(), event.getHook()); + } + } + } + + @Override + public void onStringSelectInteraction(@NotNull StringSelectInteractionEvent event) { + String id = event.getComponentId(); + if (!id.startsWith("setup_")) return; + event.deferEdit().queue(); + + if (id.equals("setup_levelrole_remove_select")) { + String selected = event.getValues().get(0); + configRepo.delete("level_role_" + selected); + showLevelRolesPanel(event.getGuild(), event.getHook()); + } + } + + @Override + public void onModalInteraction(@NotNull ModalInteractionEvent event) { + String id = event.getModalId(); + if (!id.startsWith("setup_")) return; + event.deferEdit().queue(); + + switch (id) { + case "setup_counter_format_modal" -> { + String format = event.getValue("format").getAsString(); + configRepo.set("counter.format", format); + updateCounterChannelName(event.getGuild()); + showCounterPanel(event.getGuild(), event.getHook()); + } + case "setup_levelrole_add_modal" -> { + String levelStr = event.getValue("level").getAsString(); + String role = event.getValue("role").getAsString(); + configRepo.set("level_role_" + levelStr, role); + showLevelRolesPanel(event.getGuild(), event.getHook()); + } + } + } + + private void showDashboard(Guild guild, InteractionHook hook) { + hook.editOriginalEmbeds(buildDashboardEmbed(guild).build()) + .setComponents(buildDashboardButtons()) + .queue(); + } + + private void showVoicePanel(Guild guild, InteractionHook hook) { + EmbedBuilder embed = new EmbedBuilder() + .setTitle("🎤 Настройка голосовых каналов") + .setColor(BLURPLE); + + String triggerId = configRepo.get("voice.trigger_channel_id"); + if (triggerId != null) { + embed.addField("Канал-триггер", "<#" + triggerId + ">", false); + } else { + embed.addField("Канал-триггер", "❌ Не настроено", false); + } + + String categoryId = configRepo.get("voice.category_id"); + if (categoryId != null) { + var cat = guild.getCategoryById(categoryId); + embed.addField("Категория", cat != null ? cat.getName() : "❌ Не найдена", false); + } else { + embed.addField("Категория", "❌ Не настроено", false); + } + + hook.editOriginalEmbeds(embed.build()) + .setComponents( + ActionRow.of(EntitySelectMenu.create("setup_voice_trigger", EntitySelectMenu.SelectTarget.CHANNEL) + .setChannelTypes(ChannelType.VOICE) + .setPlaceholder("Выбери канал-триггер") + .build()), + ActionRow.of(EntitySelectMenu.create("setup_voice_category", EntitySelectMenu.SelectTarget.CHANNEL) + .setChannelTypes(ChannelType.CATEGORY) + .setPlaceholder("Выбери категорию") + .build()), + ActionRow.of(Button.secondary("setup_back", "🔙 Назад"))) + .queue(); + } + + private void showCounterPanel(Guild guild, InteractionHook hook) { + EmbedBuilder embed = new EmbedBuilder() + .setTitle("📊 Настройка счётчика участников") + .setColor(BLURPLE); + + String channelId = configRepo.get("counter.channel_id"); + if (channelId != null) { + embed.addField("Канал", "<#" + channelId + ">", false); + } else { + embed.addField("Канал", "❌ Не настроено", false); + } + + String format = configRepo.get("counter.format"); + embed.addField("Формат", format != null ? "`" + format + "`" : "❌ Не настроено", false); + + hook.editOriginalEmbeds(embed.build()) + .setComponents( + ActionRow.of(EntitySelectMenu.create("setup_counter_channel", EntitySelectMenu.SelectTarget.CHANNEL) + .setChannelTypes(ChannelType.VOICE) + .setPlaceholder("Выбери канал") + .build()), + ActionRow.of( + Button.secondary("setup_counter_format", "✏️ Формат"), + Button.secondary("setup_back", "🔙 Назад"))) + .queue(); + } + + private void showLoggingPanel(Guild guild, InteractionHook hook) { + EmbedBuilder embed = new EmbedBuilder() + .setTitle("📝 Настройка логирования") + .setColor(BLURPLE); + + String modId = configRepo.get("moderation.log_channel_id"); + embed.addField("Логи модерации", modId != null ? "<#" + modId + ">" : "❌ Не настроено", false); + + String lvlId = configRepo.get("levelup.notification_channel_id"); + embed.addField("Уведомления level-up", lvlId != null ? "<#" + lvlId + ">" : "❌ Не настроено", false); + + hook.editOriginalEmbeds(embed.build()) + .setComponents( + ActionRow.of(EntitySelectMenu.create("setup_logs_moderation", EntitySelectMenu.SelectTarget.CHANNEL) + .setChannelTypes(ChannelType.TEXT) + .setPlaceholder("Канал логов модерации") + .build()), + ActionRow.of(EntitySelectMenu.create("setup_logs_levelup", EntitySelectMenu.SelectTarget.CHANNEL) + .setChannelTypes(ChannelType.TEXT) + .setPlaceholder("Канал level-up уведомлений") + .build()), + ActionRow.of(Button.secondary("setup_back", "🔙 Назад"))) + .queue(); + } + + private void showLevelRolesPanel(Guild guild, InteractionHook hook) { + Map allConfig = configRepo.getAll(); + var levelRoles = allConfig.entrySet().stream() + .filter(e -> e.getKey().startsWith("level_role_")) + .collect(Collectors.toMap( + e -> Integer.parseInt(e.getKey().substring("level_role_".length())), + Map.Entry::getValue)); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("🏆 Роли за уровни") + .setColor(BLURPLE); + + if (levelRoles.isEmpty()) { + embed.setDescription("❌ Роли не настроены"); + } else { + StringBuilder sb = new StringBuilder(); + levelRoles.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(e -> sb.append("**Уровень ").append(e.getKey()).append(":** `").append(e.getValue()).append("`\n")); + embed.setDescription(sb.toString()); + } + + var buttons = new java.util.ArrayList