diff --git a/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java b/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java new file mode 100644 index 0000000..9c105a6 --- /dev/null +++ b/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java @@ -0,0 +1,104 @@ +package creator.loki.feature.level; + +import creator.loki.embed.EmbedFactory; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; + +/** + * Обработчик кнопок подтверждения сброса уровней + */ +public class ResetLevelsButtonHandler extends ListenerAdapter { + private static final Logger logger = LoggerFactory.getLogger(ResetLevelsButtonHandler.class); + private final ResetLevelsCommand resetCommand; + + public ResetLevelsButtonHandler(ResetLevelsCommand resetCommand) { + this.resetCommand = resetCommand; + } + + @Override + public void onButtonInteraction(ButtonInteractionEvent event) { + String buttonId = event.getComponentId(); + + // Проверяем что это наша кнопка + if (!buttonId.startsWith("reset_levels_")) { + return; + } + + // Извлекаем ID пользователя из кнопки + String[] parts = buttonId.split(":"); + if (parts.length != 2) { + return; + } + + String authorId = parts[1]; + + // Проверяем что кнопку нажал тот же пользователь + if (!event.getUser().getId().equals(authorId)) { + event.reply("❌ Только инициатор команды может подтвердить действие!") + .setEphemeral(true) + .queue(); + return; + } + + if (buttonId.startsWith("reset_levels_confirm:")) { + // Подтверждение + event.deferReply().queue(); + + try { + String guildId = event.getGuild().getId(); + String userId = event.getUser().getId(); + + // Выполняем сброс + resetCommand.executeReset(guildId, userId); + + // Удаляем исходное сообщение с кнопками + event.getMessage().delete().queue(); + + // Отправляем подтверждение + event.getHook().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("✅ Сброс уровней выполнен") + .setDescription( + "Уровни и XP всех пользователей успешно сброшены!\n\n" + + "**Сохранено:**\n" + + "• Время в голосовых каналах\n" + + "• Количество сообщений\n" + + "• Репутация\n\n" + + "**Сброшено:**\n" + + "• Уровень → 0\n" + + "• XP → 0\n\n" + + "Теперь используется новая система уровней!" + ) + .setColor(Color.GREEN) + .setFooter("Инициатор: " + event.getUser().getName(), event.getUser().getAvatarUrl()) + .build() + ).queue(); + + logger.info("Сброс уровней подтверждён пользователем: {}", event.getUser().getName()); + + } catch (Exception e) { + logger.error("Ошибка при выполнении сброса", e); + event.getHook().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("❌ Ошибка") + .setDescription("Произошла ошибка при сбросе уровней: " + e.getMessage()) + .setColor(Color.RED) + .build() + ).queue(); + } + + } else if (buttonId.startsWith("reset_levels_cancel:")) { + // Отмена + event.getMessage().delete().queue(); + event.reply("❌ Сброс уровней отменён.") + .setEphemeral(true) + .queue(); + + logger.info("Сброс уровней отменён пользователем: {}", event.getUser().getName()); + } + } +} diff --git a/src/main/java/creator/loki/feature/level/ResetLevelsCommand.java b/src/main/java/creator/loki/feature/level/ResetLevelsCommand.java new file mode 100644 index 0000000..fba00b9 --- /dev/null +++ b/src/main/java/creator/loki/feature/level/ResetLevelsCommand.java @@ -0,0 +1,152 @@ +package creator.loki.feature.level; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.database.ConfigRepository; +import creator.loki.embed.EmbedFactory; +import creator.loki.repository.UserStatsRepository; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; + +/** + * Одноразовая команда для сброса уровней и XP всех пользователей + * Сохраняет время в войсе и количество сообщений + */ +public class ResetLevelsCommand implements Command { + private static final Logger logger = LoggerFactory.getLogger(ResetLevelsCommand.class); + private static final String RESET_KEY = "levels_reset_executed"; + + private final UserStatsRepository repository; + private final ConfigRepository configRepo; + + public ResetLevelsCommand(UserStatsRepository repository, ConfigRepository configRepo) { + this.repository = repository; + this.configRepo = configRepo; + } + + @Override + public String getName() { + return "resetlevels"; + } + + @Override + public String getDescription() { + return "Сбросить уровни и XP всех пользователей (одноразовая команда, только для админов)"; + } + + @Override + public String getUsage() { + return "resetlevels"; + } + + @Override + public String getCategory() { + return "Настройка"; + } + + @Override + public void execute(CommandContext context) { + // Проверка прав администратора + if (context.getAuthor() == null || !context.getAuthor().hasPermission(Permission.ADMINISTRATOR)) { + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("❌ Недостаточно прав") + .setDescription("Эта команда доступна только администраторам.") + .setColor(Color.RED) + .build() + ).queue(); + return; + } + + // Проверка, была ли команда уже выполнена + String guildId = context.getGuild().getId(); + String resetKey = guildId + ":" + RESET_KEY; + String resetValue = configRepo.get(resetKey); + boolean alreadyReset = "true".equals(resetValue); + + if (alreadyReset) { + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("⚠️ Команда уже выполнена") + .setDescription("Сброс уровней уже был выполнен ранее. Эта команда одноразовая и не может быть использована повторно.") + .setColor(Color.ORANGE) + .addField("💡 Информация", "Если нужно сбросить уровни снова, обратитесь к разработчику бота.", false) + .build() + ).queue(); + return; + } + + // Показываем предупреждение с кнопками подтверждения + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("⚠️ ВНИМАНИЕ: Сброс уровней") + .setDescription( + "Вы собираетесь сбросить **уровни и XP** всех пользователей на сервере.\n\n" + + "**Что будет сброшено:**\n" + + "• Уровень → 0\n" + + "• XP → 0\n\n" + + "**Что НЕ будет затронуто:**\n" + + "• Время в голосовых каналах\n" + + "• Количество сообщений\n" + + "• Репутация\n\n" + + "**⚠️ Это действие НЕОБРАТИМО и может быть выполнено только ОДИН РАЗ!**" + ) + .setColor(Color.RED) + .addField("📊 Новая система уровней", + "После сброса будет использоваться новая формула:\n" + + "• XP для уровня N = 200 × N\n" + + "• Пример: для 15 уровня нужно 22,800 XP", + false) + .setFooter("Подтвердите действие нажатием кнопки ниже") + .build() + ).setActionRow( + Button.success("reset_levels_confirm:" + context.getAuthor().getId(), "✅ Подтвердить сброс"), + Button.danger("reset_levels_cancel:" + context.getAuthor().getId(), "❌ Отмена") + ).queue(); + } + + /** + * Выполняет сброс уровней (вызывается из ButtonHandler) + */ + public void executeReset(String guildId, String userId) { + try { + logger.info("========================================"); + logger.info("Начало сброса уровней для гильдии: {}", guildId); + logger.info("Инициатор: {}", userId); + logger.info("========================================"); + + // Получаем всех пользователей (без guildId - метод возвращает всех) + var allStats = repository.getAllStats(); + int resetCount = 0; + + // Фильтруем по guildId и сбрасываем только XP и уровень + for (var entry : allStats.entrySet()) { + String key = entry.getKey(); + // Ключ формата "guildId:userId" + if (key.startsWith(guildId + ":")) { + var stats = entry.getValue(); + stats.setTotalXP(0); + stats.setLevel(0); + // Сохраняем обратно + repository.save(stats); + resetCount++; + } + } + + // Помечаем что команда выполнена + String resetKey = guildId + ":" + RESET_KEY; + configRepo.set(resetKey, "true"); + + logger.info("Сброс завершён. Обработано пользователей: {}", resetCount); + logger.info("========================================"); + + } catch (Exception e) { + logger.error("Ошибка при сбросе уровней", e); + throw new RuntimeException("Ошибка при сбросе уровней: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/creator/loki/feature/voice/VoiceChannelListener.java b/src/main/java/creator/loki/feature/voice/VoiceChannelListener.java index d812159..b649582 100644 --- a/src/main/java/creator/loki/feature/voice/VoiceChannelListener.java +++ b/src/main/java/creator/loki/feature/voice/VoiceChannelListener.java @@ -42,9 +42,12 @@ public class VoiceChannelListener extends ListenerAdapter { VoiceChannel personalChannel = voiceManager.createPersonalChannel(member, guild); if (personalChannel != null) { + // Применяем сохранённый пресет пользователя + voiceManager.applyUserPreset(personalChannel, member.getId()); + // Перемещаем пользователя в созданный канал guild.moveVoiceMember(member, personalChannel).queue( - success -> logger.info("Пользователь {} перемещен в личный канал", member.getEffectiveName()), + success -> logger.info("Пользователь {} перемещен в личный канал с применённым пресетом", member.getEffectiveName()), error -> logger.error("Ошибка при перемещении пользователя", error) ); } diff --git a/src/main/java/creator/loki/feature/voice/VoiceChannelManager.java b/src/main/java/creator/loki/feature/voice/VoiceChannelManager.java index c6b3d05..ef43e6a 100644 --- a/src/main/java/creator/loki/feature/voice/VoiceChannelManager.java +++ b/src/main/java/creator/loki/feature/voice/VoiceChannelManager.java @@ -59,7 +59,7 @@ public class VoiceChannelManager { */ public VoiceChannel createPersonalChannel(Member member, Guild guild) { try { - String channelName = member.getEffectiveName() + "'s Channel"; + String channelName = "Канал: " + member.getEffectiveName(); VoiceChannel channel = guild.createVoiceChannel(channelName) .setParent(guild.getCategoryById(categoryId)) @@ -171,4 +171,62 @@ public class VoiceChannelManager { public String getTriggerChannelId() { return triggerChannelId; } public String getCategoryId() { return categoryId; } public String getTextChannelId() { return textChannelId; } + + /** + * Сохранить пресет пользователя + */ + public void saveUserPreset(String userId, VoicePreset preset) { + Map presets = getUserPresets(); + presets.put(userId, preset); + dataStore.setData("voice_user_presets", presets); + logger.info("Сохранён пресет для пользователя {}", userId); + } + + /** + * Получить пресет пользователя + */ + public VoicePreset getUserPreset(String userId) { + Map presets = getUserPresets(); + return presets.getOrDefault(userId, new VoicePreset()); + } + + /** + * Применить пресет пользователя к каналу + */ + public void applyUserPreset(VoiceChannel channel, String userId) { + VoicePreset preset = getUserPreset(userId); + + try { + // Применяем настройки + if (preset.getName() != null && !preset.getName().isEmpty()) { + channel.getManager().setName(preset.getName()).queue(); + } + + if (preset.getLimit() >= 0) { + channel.getManager().setUserLimit(preset.getLimit()).queue(); + } + + if (preset.getBitrate() > 0) { + channel.getManager().setBitrate(preset.getBitrate()).queue(); + } + + // Применяем регион + if (preset.getRegion() != null && !preset.getRegion().equals("auto")) { + // JDA 5 не поддерживает регионы напрямую, но можно сохранить в настройках + logger.info("Регион {} будет применён для канала {}", preset.getRegion(), channel.getId()); + } + + logger.info("Применён пресет пользователя {} к каналу {}", userId, channel.getId()); + + } catch (Exception e) { + logger.error("Ошибка при применении пресета", e); + } + } + + @SuppressWarnings("unchecked") + private Map getUserPresets() { + Type type = new TypeToken>(){}.getType(); + Map presets = dataStore.getData("voice_user_presets", type); + return presets != null ? presets : new ConcurrentHashMap<>(); + } } diff --git a/src/main/java/creator/loki/feature/voice/VoicePreset.java b/src/main/java/creator/loki/feature/voice/VoicePreset.java index c725985..7898305 100644 --- a/src/main/java/creator/loki/feature/voice/VoicePreset.java +++ b/src/main/java/creator/loki/feature/voice/VoicePreset.java @@ -1,26 +1,55 @@ package creator.loki.feature.voice; +import net.dv8tion.jda.api.Region; + /** - * Пресет настроек голосового канала + * Пресет настроек голосового канала с расширенными опциями */ public class VoicePreset { private String name; private int limit; private int bitrate; + private String region; // Регион канала (auto, russia, europe, us-west, etc.) + private boolean locked; // Закрыт ли канал + private String allowedRoleId; // ID роли с доступом (если канал закрыт) + private String deniedRoleId; // ID роли без доступа + + public VoicePreset() { + // Конструктор по умолчанию для Gson + this.name = "Мой канал"; + this.limit = 0; + this.bitrate = 64000; + this.region = "auto"; + this.locked = false; + this.allowedRoleId = null; + this.deniedRoleId = null; + } public VoicePreset(String name, int limit, int bitrate) { this.name = name; this.limit = limit; this.bitrate = bitrate; + this.region = "auto"; + this.locked = false; + this.allowedRoleId = null; + this.deniedRoleId = null; } // Getters public String getName() { return name; } public int getLimit() { return limit; } public int getBitrate() { return bitrate; } + public String getRegion() { return region; } + public boolean isLocked() { return locked; } + public String getAllowedRoleId() { return allowedRoleId; } + public String getDeniedRoleId() { return deniedRoleId; } // Setters public void setName(String name) { this.name = name; } public void setLimit(int limit) { this.limit = limit; } public void setBitrate(int bitrate) { this.bitrate = bitrate; } + public void setRegion(String region) { this.region = region; } + public void setLocked(boolean locked) { this.locked = locked; } + public void setAllowedRoleId(String allowedRoleId) { this.allowedRoleId = allowedRoleId; } + public void setDeniedRoleId(String deniedRoleId) { this.deniedRoleId = deniedRoleId; } }