diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..8ce7b51 --- /dev/null +++ b/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..58ebfab --- /dev/null +++ b/.project @@ -0,0 +1,34 @@ + + + discord-bot-bot + Project bot created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + + + 1783162972591 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..f7f3856 --- /dev/null +++ b/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments=--init-script /home/loki/.cache/opencode/bin/jdtls/config_linux/org.eclipse.osgi/57/0/.cp/gradle/init/init.gradle +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home=/usr/lib/jvm/java-21-openjdk +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/TODO.md b/TODO.md index b060e62..4d6bb4f 100644 --- a/TODO.md +++ b/TODO.md @@ -251,7 +251,150 @@ feature/economy/ --- -**Версия:** 2.3 | **Последнее обновление:** 2026-07-02 | **Java 21 ✅, God Constructor ✅, Critical баги ✅, Economy ✅, Level Roles ✅, Lavalink Music ✅, SpotBugs ✅, Checkstyle ✅, CI/CD ✅** +--- + +## 🔴 Результаты аудита KISS/DRY/SOLID (2026-07-04) + +### DRY — дублирование кода (критично) + +| # | Что дублируется | Где | Сколько раз | Статус | +|---|----------------|-----|-------------|--------| +| D1 | `getDeterministicValue()` — целиком | GayCommand, FatCommand, IqCommand, PpCommand, BoykisserCommand, MustacheCommand, RateCommand | 7 | ✅ RatingService | +| D2 | Cooldown check boilerplate (5 строк) | Все entertainment команды | 10+ | ✅ RatingService | +| D3 | `trunc()` / `err()` helpers | MuteCommand, WarnCommand, SwapAndCaseCommand, UnmuteCommand, PunishCommand | 5 | ✅ ModerationHelper | +| D4 | Voice connect retry loop (цикл 10×200ms) | PlayCommand, RadioManager | 2 | +| D5 | RatingConfig — 6 методов с идентичной структурой | RatingConfig.java | 6 | +| D6 | Social команды (hug/kiss/slap/poke/tickle/cuddle/feed/pat) — шаблон | 10 файлов | 10 | + +### SOLID + +| # | Нарушение | Файл | Описание | +|---|-----------|------|----------| +| S1 | SRP | BotModule.java | Биндит всё приложение | +| S2 | SRP | SwapAndCaseCommand.java | Две разные команды в одном файле | +| S3 | SRP | BotInitializer.java | Инициализирует 3 JDA, Lavalink, mutes, counters, health | +| S4 | SRP | ShutdownManager.java | 9 зависимостей в конструкторе | +| S5 | SRP | PlayCommand.java | execute() на 145 строк | +| S6 | OCP | Meter-команды (gay/fat/iq/pp) | Добавить новую = копировать 90 строк | ✅ RatingService | +| S7 | DIP | MuteCommand, WarnCommand | >5 конкретных репозиториев вместо интерфейсов | + +### Файлы >150 строк + +| # | Файл | Строк | +|---|------|-------| +| F1 | PlayCommand.java | 219 | +| F2 | QueueSlashHandler.java | 207 | +| F3 | MusicBotControlHandler.java | 207 | +| F4 | QueueCommands.java | 207 | +| F5 | TrackScheduler.java | 202 | +| F6 | MuteCommand.java | 199 | +| F7 | ModerationLogger.java | 199 | +| F8 | FeatureTables.java | 193 | +| F9 | DatabaseMigration.java | 193 | +| F10 | VoiceChannelManager.java | 187 | +| F11 | ServerStatsService.java | 187 | +| F12 | VoiceSetupPrefixCommand.java | 183 | +| F13 | RadioManager.java | 182 | +| F14 | MetricsCollector.java | 182 | +| F15 | WarnCommand.java | 184 | +| F16 | BotInitializer.java | 182 | +| F17 | SlashHandler's (Queue/Filter) | >100 | +| F18 | GenerateRatingsCommand.java | 172 | +| F19 | BlackjackService.java | 172 | +| F20 | SqlEconomyRepository.java | 169 | +| F21 | GuildMusicManager.java | 167 | +| F22 | ProfileCommand/RankCardGen | >150 | + +### >3 файла в папке + +| # | Папка | Файлов | +|---|-------|--------| +| P1 | feature/entertainment/commands/ | 15 | +| P2 | feature/social/commands/ | 10 | +| P3 | feature/economy/command/minigame/ | 7 | +| P4 | feature/level/ (все субпапки) | 21 | +| P5 | feature/moderation/ (все субпакеты) | 18 | +| P6 | feature/music/ (все субпакеты) | 22 | +| P7 | feature/voice/ (все субпакеты) | 12 | + +### Нет тестов + +| # | Фича | Файлов | +|---|------|--------| +| T1 | music | 20+ | +| T2 | voice | 12 | +| T3 | social | 10 | +| T4 | role | 9 | +| T5 | radio | 5 | +| T6 | reputation | 4 | +| T7 | counter | 7 | +| T8 | admin | 1 | +| T9 | help | 2 | +| T10 | utility | 2 | +| T11 | welcome | 2 | +| T12 | profile | 5 | +| T13 | serverstats | 1 | + +## 📋 План рефакторинга (KISS/DRY/SOLID) + +### Этап 8: Убить дублирование (DRY) — в процессе + +- [x] D1/D2: `RatingService` с `getDeterministicValue()`, cooldown, member parsing — 7 команд переписаны +- [x] D3: `ModerationHelper` с `trunc()`/`err()` — MuteCommand, UnmuteCommand, WarnCommand, SwapAndCaseCommand, PunishCommand +- [ ] D4: Вынести voice connect retry в утилиту +- [ ] D5: RatingConfig — заменить 6 методов на data-driven (Map или config) +- [ ] D6: Social команды — единый `SocialCommand` с параметром action + +### Этап 9: SOLID + +- [ ] S1: Разбить `BotModule` на per-feature модули (FeatureModule → install в BotModule) +- [ ] S2: Разделить `SwapAndCaseCommand` на два отдельных файла +- [ ] S3: Выделить `MusicBotFactory`, `RadioBotFactory` из `BotInitializer` +- [ ] S4: Уменьшить кол-во зависимостей `ShutdownManager` через события (EventBus) +- [ ] S5: Разбить `PlayCommand.execute()` на методы +- [ ] S6: Meter-команды сделать data-driven через `RatingService` +- [ ] S7: Выделить интерфейсы для репозиториев/сервисов (DIP) + +### Этап 10: Файлы ≤150 строк + +- [ ] F01: PlayCommand.java — разбить на методы/классы +- [ ] F02-04: QueueSlashHandler, MusicBotControlHandler, QueueCommands — вынести общие части +- [ ] F05: TrackScheduler.java — вывести FilterState в отд. файл или утилиту +- [ ] F06: MuteCommand.java — разбить на subcommand handler'ы +- [ ] F07: ModerationLogger.java — разбить по типам событий +- [ ] F08-09: FeatureTables, DatabaseMigration — разбить по фичам +- [ ] F10-12: Voice* — вынести логику в сервисы +- [ ] F13: RadioManager.java — вынести voice connect в утилиту +- [ ] F14: MetricsCollector.java — разбить по типам метрик +- [ ] F15: WarnCommand.java — вынести auto-mute в отдельный сервис +- [ ] F16: BotInitializer.java — вынести фабрики ботов +- [ ] Остальные: разбить по SOLID + +### Этап 11: ≤6 файлов в папке + +- [ ] P1: Entertainment команды — группировать по смыслу (coin/dice → `random/`, gay/iq/pp → `rating/`) +- [ ] P2: Social команды — объединить в `SocialCommand` с параметром +- [ ] P4: Level — разбить на commands/, service/, repository/ +- [ ] P5: Moderation — разбить по поддоменам (commands/punish, commands/utils, warn) +- [ ] P6: Music — разбить на commands/, player/, lavalink/ + +### Этап 12: Тесты + +- [ ] T1: Music — написать тесты на LavalinkManager, TrackScheduler, PlayCommand +- [ ] T2: Voice — VoiceChannelManager, VoiceButtonHandler +- [ ] T3: Social — интеграционные тесты с NekosLifeApi (мок) +- [ ] T4: Radio — RadioManager, RadioCommand +- [ ] T5: Role — CustomRoleManager, RoleCommandValidator +- [ ] T6-13: Остальные фичи — минимум smoke test + +### Техдолг (баги) + +- [ ] B1: EmbedFactoryTest — 2 падающих теста (`withTitle`, `withColor`) — IllegalStateException +- [ ] B2: SlowmodeCommandTest — починен (было 7 ошибок компиляции из-за JDA 6.4 SelfMember) + +--- + +**Версия:** 2.3 | **Последнее обновление:** 2026-07-04 | **Java 21 ✅, God Constructor ✅, Critical баги ✅, Economy ✅, Level Roles ✅, Lavalink Music ✅, SpotBugs ✅, Checkstyle ✅, CI/CD ✅** ## ✅ Что добавлено в v2.3 diff --git a/build.gradle b/build.gradle index 6b255bf..c600a02 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,8 @@ dependencies { implementation libs.guice implementation libs.sqlite.jdbc implementation libs.hikaricp + implementation libs.okhttp + implementation libs.logging.interceptor testImplementation libs.junit.jupiter testImplementation libs.mockito.core diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9d486ac..505dc61 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ caffeine = "3.2.0" guice = "7.0.0" sqlite = "3.49.1.0" hikaricp = "6.3.0" +okhttp = "5.4.0" junit = "5.12.1" mockito = "5.15.2" junitPlatformLauncher = "1.12.1" @@ -29,6 +30,8 @@ caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "c guice = { module = "com.google.inject:guice", version.ref = "guice" } sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" } hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikaricp" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } diff --git a/src/main/java/creator/loki/core/BotInitializer.java b/src/main/java/creator/loki/core/BotInitializer.java index 485c1aa..ed855c2 100644 --- a/src/main/java/creator/loki/core/BotInitializer.java +++ b/src/main/java/creator/loki/core/BotInitializer.java @@ -1,5 +1,7 @@ package creator.loki.core; +import java.util.List; + import com.google.inject.Inject; import creator.loki.config.BotConfig; import creator.loki.core.registry.PrefixCommandRegistrar; @@ -63,119 +65,92 @@ public class BotInitializer { public BotCluster start() { LoggerDecorator.startup(logger, "Инициализация main бота..."); registrar.registerAll(); - JDA mainJda = createMainJDA(); - try { - mainJda.awaitReady(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Main бот прерван во время ожидания готовности", e); - } + JDA mainJda = buildMainJDA(); mainJda.updateCommands().queue(); muteScheduler.restorePending(mainJda); - for (var guild : mainJda.getGuilds()) { - memberCounterListener.startScheduler(guild); - } + mainJda.getGuilds().forEach(g -> memberCounterListener.startScheduler(g)); healthHttpServer.start(); - JDA musicJda = null; - if (config.getMusicToken() != null && !config.getMusicToken().isBlank()) { - LoggerDecorator.startup(logger, "Инициализация music бота..."); - musicJda = createMusicJDA(); - try { - musicJda.awaitReady(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Music бот прерван во время ожидания готовности", e); - } - } - - JDA radioJda = null; - if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) { - LoggerDecorator.startup(logger, "Инициализация radio бота..."); - radioJda = createRadioJDA(); - try { - radioJda.awaitReady(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Radio бот прерван во время ожидания готовности", e); - } + JDA musicJda = createMusicJDA(); + JDA radioJda = createRadioJDA(); + if (radioJda != null) { radioManager.setRadioJda(radioJda); + radioManager.setLavalinkManager(lavalinkManager); } - BotCluster cluster = new BotCluster(mainJda, musicJda, radioJda); - registerShutdownHook(cluster); + Runtime.getRuntime() + .addShutdownHook(new Thread(() -> shutdownManager.shutdown(cluster), "ShutdownHook")); return cluster; } - private JDA createMainJDA() { + private JDA buildMainJDA() { var builder = - JDABuilder.createDefault(config.getMainToken()) - .setStatus(OnlineStatus.ONLINE) - .setActivity(Activity.playing(config.getActivity())) - .enableIntents( - GatewayIntent.GUILD_MESSAGES, - GatewayIntent.MESSAGE_CONTENT, - GatewayIntent.GUILD_MEMBERS, - GatewayIntent.GUILD_VOICE_STATES) - .enableCache(CacheFlag.VOICE_STATE) - .addEventListeners(listenerAggregator.getAll()); - + createBuilder( + config.getMainToken(), + config.getActivity(), + GatewayIntent.GUILD_MESSAGES, + GatewayIntent.MESSAGE_CONTENT, + GatewayIntent.GUILD_MEMBERS, + GatewayIntent.GUILD_VOICE_STATES); if (lavalinkManager.getClient() != null) { builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient())); } - - return builder.build(); + builder.addEventListeners(listenerAggregator.getAll()); + return awaitJda(builder.build()); } private JDA createMusicJDA() { - LavalinkManager musicLavalink = - new LavalinkManager(config, queueRepository, config.getMusicToken()); - + if (config.getMusicToken() == null || config.getMusicToken().isBlank()) return null; + LoggerDecorator.startup(logger, "Инициализация music бота..."); + LavalinkManager ml = new LavalinkManager(config, queueRepository, config.getMusicToken()); var builder = - JDABuilder.createDefault(config.getMusicToken()) - .setStatus(OnlineStatus.ONLINE) - .setActivity(Activity.playing("🎵 q!play")) - .enableIntents( - GatewayIntent.GUILD_MESSAGES, - GatewayIntent.MESSAGE_CONTENT, - GatewayIntent.GUILD_VOICE_STATES) - .enableCache(CacheFlag.VOICE_STATE); - - if (musicLavalink.getClient() != null) { - builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(musicLavalink.getClient())); + createBuilder( + config.getMusicToken(), + "🎵 q!play", + GatewayIntent.GUILD_MESSAGES, + GatewayIntent.MESSAGE_CONTENT, + GatewayIntent.GUILD_VOICE_STATES); + if (ml.getClient() != null) { + builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ml.getClient())); } - builder.addEventListeners( new MusicBotListener( - musicLavalink, - queueRepository, - new creator.loki.feature.music.core.PlayerManager(musicLavalink))); - - return builder.build(); + ml, queueRepository, new creator.loki.feature.music.core.PlayerManager(ml))); + return awaitJda(builder.build()); } private JDA createRadioJDA() { + if (config.getRadioToken() == null || config.getRadioToken().isBlank()) return null; + LoggerDecorator.startup(logger, "Инициализация radio бота..."); var builder = - JDABuilder.createDefault(config.getRadioToken()) - .setStatus(OnlineStatus.ONLINE) - .setActivity(Activity.playing("📻 r!play")) - .enableIntents( - GatewayIntent.GUILD_MESSAGES, - GatewayIntent.MESSAGE_CONTENT, - GatewayIntent.GUILD_VOICE_STATES) - .enableCache(CacheFlag.VOICE_STATE); - + createBuilder( + config.getRadioToken(), + "📻 r!play", + GatewayIntent.GUILD_MESSAGES, + GatewayIntent.MESSAGE_CONTENT, + GatewayIntent.GUILD_VOICE_STATES); if (lavalinkManager.getClient() != null) { builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient())); } - builder.addEventListeners(new RadioBotListener(radioManager)); - - return builder.build(); + return awaitJda(builder.build()); } - private void registerShutdownHook(BotCluster cluster) { - Runtime.getRuntime() - .addShutdownHook(new Thread(() -> shutdownManager.shutdown(cluster), "ShutdownHook")); + private static JDABuilder createBuilder(String token, String activity, GatewayIntent... intents) { + return JDABuilder.createDefault(token) + .setStatus(OnlineStatus.ONLINE) + .setActivity(Activity.playing(activity)) + .enableIntents(List.of(intents)) + .enableCache(CacheFlag.VOICE_STATE); + } + + private static JDA awaitJda(JDA jda) { + try { + jda.awaitReady(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("JDA прерван во время ожидания готовности", e); + } + return jda; } } diff --git a/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java b/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java index 465398c..237dbe8 100644 --- a/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java +++ b/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java @@ -9,13 +9,10 @@ import creator.loki.config.BotConfig; import creator.loki.embed.EmbedFactory; import creator.loki.feature.rating.repository.RatingsRepository; import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Команда для генерации всех rating значений для всех участников сервера Доступна только - * супер-админу - */ public class GenerateRatingsCommand implements Command { private static final Logger logger = LoggerFactory.getLogger(GenerateRatingsCommand.class); private final RatingsRepository ratingsRepository; @@ -53,120 +50,98 @@ public class GenerateRatingsCommand implements Command { context .getChannel() .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("❌ Недостаточно прав") - .withDescription("Эта команда доступна только супер-администратору бота.") - .withColor(Color.RED) - .build()) + errorEmbed( + "❌ Недостаточно прав", "Эта команда доступна только супер-администратору бота.")) .queue(); return; } - if (context.getGuild() == null) { context.getChannel().sendMessage("❌ Команда работает только на сервере.").queue(); return; } - var initial = context .getChannel() .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("⏳ Генерация рейтингов...") - .withDescription( - "Начинаю генерацию всех rating значений для участников сервера...") - .withColor(Color.YELLOW) - .build()) + warnEmbed( + "⏳ Генерация рейтингов...", + "Начинаю генерацию всех rating значений для участников сервера...")) .complete(); - try { - String guildId = context.getGuild().getId(); - int memberCount = 0; - int generatedCount = 0; - + var guildId = context.getGuild().getId(); + var memberCount = 0; + var generatedCount = 0; for (Member member : context.getGuild().getMembers()) { - if (member.getUser().isBot()) { - continue; - } - + if (member.getUser().isBot()) continue; memberCount++; - String userId = member.getUser().getId(); - - generateRatingForUser(guildId, userId, "ship"); - generateRatingForUser(guildId, userId, "gay"); - generateRatingForUser(guildId, userId, "iq"); - generateRatingForUser(guildId, userId, "pp"); - generateRatingForUser(guildId, userId, "mustache"); - generateRatingForUser(guildId, userId, "fat"); - generateRatingForUser(guildId, userId, "boykisser"); - + var userId = member.getUser().getId(); + for (var type : new String[] {"ship", "gay", "iq", "pp", "mustache", "fat", "boykisser"}) { + generateRatingForUser(guildId, userId, type); + } generatedCount++; } - - initial - .editMessageEmbeds( - EmbedFactory.create() - .withTitle("✅ Генерация завершена") - .withDescription( - "Успешно сгенерированы все rating значения!\n\n" - + "**Статистика:**\n" - + "• Обработано участников: **" - + memberCount - + "**\n" - + "• Сгенерировано значений: **" - + (generatedCount * 7) - + "**\n" - + "• Типы рейтингов: ship, gay, iq, pp, mustache, fat, boykisser") - .withColor(Color.GREEN) - .withFooter( - "Инициатор: " + context.getAuthor().getUser().getName(), - context.getAuthor().getAvatarUrl()) - .build()) - .queue(); - + initial.editMessageEmbeds(successEmbed(context, memberCount, generatedCount)).queue(); logger.info( "Супер-админ {} сгенерировал рейтинги для {} участников", context.getAuthor().getUser().getName(), memberCount); - } catch (Exception e) { logger.error("Ошибка при генерации рейтингов", e); initial .editMessageEmbeds( - EmbedFactory.create() - .withTitle("❌ Ошибка") - .withDescription("Произошла ошибка при генерации рейтингов: " + e.getMessage()) - .withColor(Color.RED) - .build()) + errorEmbed("❌ Ошибка", "Произошла ошибка при генерации рейтингов: " + e.getMessage())) .queue(); } } - /** Генерирует rating значение для пользователя */ private void generateRatingForUser(String guildId, String userId, String ratingType) { - // Проверяем существует ли уже значение - if (ratingsRepository.hasRating(guildId, userId, ratingType)) { - return; // Не перезаписываем существующие значения - } - - // Генерируем значение в зависимости от типа - int value = + if (ratingsRepository.hasRating(guildId, userId, ratingType)) return; + var value = switch (ratingType) { - case "iq" -> random.nextInt(201); // 0-200 - case "pp" -> random.nextInt(31); // 0-30 см + case "iq" -> random.nextInt(201); + case "pp" -> random.nextInt(31); case "mustache" -> { - // Редкий шанс на легендарные усы - if (random.nextInt(100) < 1) { - yield 1000 + random.nextInt(500); // 1000-1500 - } - yield random.nextInt(101); // 0-100 + if (random.nextInt(100) < 1) yield 1000 + random.nextInt(500); + yield random.nextInt(101); } - case "fat" -> random.nextInt(101); // 0-100% - case "boykisser" -> random.nextInt(101); // 0-100% - default -> random.nextInt(101); // ship, gay: 0-100% + default -> random.nextInt(101); }; - - // Сохраняем значение в БД ratingsRepository.setRating(guildId, userId, ratingType, value); } + + private MessageEmbed errorEmbed(String title, String description) { + return EmbedFactory.create() + .withTitle(title) + .withDescription(description) + .withColor(Color.RED) + .build(); + } + + private MessageEmbed warnEmbed(String title, String description) { + return EmbedFactory.create() + .withTitle(title) + .withDescription(description) + .withColor(Color.YELLOW) + .build(); + } + + private MessageEmbed successEmbed(CommandContext context, int memberCount, int generatedCount) { + return EmbedFactory.create() + .withTitle("✅ Генерация завершена") + .withDescription( + "Успешно сгенерированы все rating значения!\n\n" + + "**Статистика:**\n" + + "• Обработано участников: **" + + memberCount + + "**\n" + + "• Сгенерировано значений: **" + + (generatedCount * 7) + + "**\n" + + "• Типы рейтингов: ship, gay, iq, pp, mustache, fat, boykisser") + .withColor(Color.GREEN) + .withFooter( + "Инициатор: " + context.getAuthor().getUser().getName(), + context.getAuthor().getAvatarUrl()) + .build(); + } } diff --git a/src/main/java/creator/loki/feature/economy/repository/SqlEconomyRepository.java b/src/main/java/creator/loki/feature/economy/repository/SqlEconomyRepository.java index 51eb6da..c1de989 100644 --- a/src/main/java/creator/loki/feature/economy/repository/SqlEconomyRepository.java +++ b/src/main/java/creator/loki/feature/economy/repository/SqlEconomyRepository.java @@ -3,6 +3,7 @@ package creator.loki.feature.economy.repository; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Types; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -19,24 +20,44 @@ public class SqlEconomyRepository implements EconomyRepository { private static final int MAX_TOP = 100; private final Database database; + private static final String COLUMNS = + "user_id, cash, bank, total_earned, total_spent, last_daily, daily_streak, fishing_luck, efficiency, last_fish, job, last_work"; + public SqlEconomyRepository(Database database) { this.database = database; } + private static String selectSql() { + return "SELECT " + COLUMNS + " FROM economy WHERE user_id = ?"; + } + + private static String upsertSql() { + return "INSERT INTO economy (" + + COLUMNS + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " + + "ON CONFLICT(user_id) DO UPDATE SET " + + "cash = excluded.cash, bank = excluded.bank, total_earned = excluded.total_earned, " + + "total_spent = excluded.total_spent, last_daily = excluded.last_daily, " + + "daily_streak = excluded.daily_streak, fishing_luck = excluded.fishing_luck, " + + "efficiency = excluded.efficiency, last_fish = excluded.last_fish, " + + "job = excluded.job, last_work = excluded.last_work"; + } + + private static void setNullableString(PreparedStatement stmt, int index, String value) + throws SQLException { + if (value != null) { + stmt.setString(index, value); + } else { + stmt.setNull(index, Types.VARCHAR); + } + } + @Override public Optional findById(String userId) { - String sql = - """ - SELECT user_id, cash, bank, total_earned, total_spent, - last_daily, daily_streak, fishing_luck, efficiency, - last_fish, job, last_work - FROM economy - WHERE user_id = ? - """; - - try (PreparedStatement stmt = database.prepare(sql)) { + var sql = selectSql(); + try (var stmt = database.prepare(sql)) { stmt.setString(1, userId); - try (ResultSet rs = stmt.executeQuery()) { + try (var rs = stmt.executeQuery()) { if (rs.next()) { return Optional.of(mapResultSetToAccount(rs)); } @@ -49,56 +70,20 @@ public class SqlEconomyRepository implements EconomyRepository { @Override public void save(EconomyAccount account) { - String sql = - """ - INSERT INTO economy (user_id, cash, bank, total_earned, total_spent, - last_daily, daily_streak, fishing_luck, efficiency, - last_fish, job, last_work) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(user_id) - DO UPDATE SET - cash = excluded.cash, - bank = excluded.bank, - total_earned = excluded.total_earned, - total_spent = excluded.total_spent, - last_daily = excluded.last_daily, - daily_streak = excluded.daily_streak, - fishing_luck = excluded.fishing_luck, - efficiency = excluded.efficiency, - last_fish = excluded.last_fish, - job = excluded.job, - last_work = excluded.last_work - """; - - try (PreparedStatement stmt = database.prepare(sql)) { + var sql = upsertSql(); + try (var stmt = database.prepare(sql)) { stmt.setString(1, account.getUserId()); stmt.setLong(2, account.getCash()); stmt.setLong(3, account.getBank()); stmt.setLong(4, account.getTotalEarned()); stmt.setLong(5, account.getTotalSpent()); - if (account.getLastDaily() != null) { - stmt.setString(6, account.getLastDaily()); - } else { - stmt.setNull(6, java.sql.Types.VARCHAR); - } + setNullableString(stmt, 6, account.getLastDaily()); stmt.setInt(7, account.getDailyStreak()); stmt.setInt(8, account.getFishingLuck()); stmt.setInt(9, account.getEfficiency()); - if (account.getLastFish() != null) { - stmt.setString(10, account.getLastFish()); - } else { - stmt.setNull(10, java.sql.Types.VARCHAR); - } - if (account.getJob() != null) { - stmt.setString(11, account.getJob()); - } else { - stmt.setNull(11, java.sql.Types.VARCHAR); - } - if (account.getLastWork() != null) { - stmt.setString(12, account.getLastWork()); - } else { - stmt.setNull(12, java.sql.Types.VARCHAR); - } + setNullableString(stmt, 10, account.getLastFish()); + setNullableString(stmt, 11, account.getJob()); + setNullableString(stmt, 12, account.getLastWork()); stmt.executeUpdate(); } catch (SQLException e) { logger.error("Error saving economy account for {}", account.getUserId(), e); @@ -107,10 +92,10 @@ public class SqlEconomyRepository implements EconomyRepository { @Override public boolean exists(String userId) { - String sql = "SELECT 1 FROM economy WHERE user_id = ?"; - try (PreparedStatement stmt = database.prepare(sql)) { + var sql = "SELECT 1 FROM economy WHERE user_id = ?"; + try (var stmt = database.prepare(sql)) { stmt.setString(1, userId); - try (ResultSet rs = stmt.executeQuery()) { + try (var rs = stmt.executeQuery()) { return rs.next(); } } catch (SQLException e) { @@ -121,20 +106,15 @@ public class SqlEconomyRepository implements EconomyRepository { @Override public List findTopByBalance(int limit) { - int lim = clampLimit(limit); - String sql = - """ - SELECT user_id, cash, bank, total_earned, total_spent, - last_daily, daily_streak, fishing_luck, efficiency, - last_fish, job, last_work - FROM economy - ORDER BY (cash + bank) DESC, cash DESC, user_id ASC - LIMIT ? - """; - List list = new ArrayList<>(); - try (PreparedStatement stmt = database.prepare(sql)) { + var lim = clampLimit(limit); + var sql = + "SELECT " + + COLUMNS + + " FROM economy ORDER BY (cash + bank) DESC, cash DESC, user_id ASC LIMIT ?"; + var list = new ArrayList(); + try (var stmt = database.prepare(sql)) { stmt.setInt(1, lim); - try (ResultSet rs = stmt.executeQuery()) { + try (var rs = stmt.executeQuery()) { while (rs.next()) { list.add(mapResultSetToAccount(rs)); } diff --git a/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java b/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java index 540640c..c09e7f3 100644 --- a/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java +++ b/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java @@ -5,41 +5,41 @@ import creator.loki.config.BotConfig; import creator.loki.core.registry.CommandRegistrar; import creator.loki.core.registry.CommandRegistry; import creator.loki.core.security.CooldownManager; -import creator.loki.feature.entertainment.commands.BoykisserCommand; import creator.loki.feature.entertainment.commands.CoinCommand; import creator.loki.feature.entertainment.commands.DiceCommand; import creator.loki.feature.entertainment.commands.EightBallCommand; -import creator.loki.feature.entertainment.commands.FatCommand; -import creator.loki.feature.entertainment.commands.GayCommand; import creator.loki.feature.entertainment.commands.GlazbobaCommand; -import creator.loki.feature.entertainment.commands.IqCommand; import creator.loki.feature.entertainment.commands.MemeCommand; -import creator.loki.feature.entertainment.commands.MustacheCommand; -import creator.loki.feature.entertainment.commands.PpCommand; import creator.loki.feature.entertainment.commands.RandomChoiceCommand; -import creator.loki.feature.entertainment.commands.RateCommand; +import creator.loki.feature.entertainment.commands.rating.BoykisserCommand; +import creator.loki.feature.entertainment.commands.rating.FatCommand; +import creator.loki.feature.entertainment.commands.rating.GayCommand; +import creator.loki.feature.entertainment.commands.rating.IqCommand; +import creator.loki.feature.entertainment.commands.rating.MustacheCommand; +import creator.loki.feature.entertainment.commands.rating.PpCommand; +import creator.loki.feature.entertainment.commands.rating.RateCommand; +import creator.loki.feature.entertainment.service.RatingService; import creator.loki.feature.ship.ShipCommand; import creator.loki.feature.ship.repository.ShipPairRepository; import creator.loki.feature.social.commands.ComplimentCommand; -import creator.loki.feature.social.commands.CuddleCommand; -import creator.loki.feature.social.commands.FeedCommand; -import creator.loki.feature.social.commands.RoastCommand; -import creator.loki.feature.social.commands.HugCommand; -import creator.loki.feature.social.commands.KissCommand; import creator.loki.feature.social.commands.PatCommand; -import creator.loki.feature.social.commands.PokeCommand; -import creator.loki.feature.social.commands.SlapCommand; -import creator.loki.feature.social.commands.TickleCommand; +import creator.loki.feature.social.commands.RoastCommand; +import creator.loki.feature.social.commands.SocialCommand; public class EntertainmentCommandRegistrar implements CommandRegistrar { private final CooldownManager cooldownManager; + private final RatingService ratingService; private final ShipPairRepository shipPairRepository; private final BotConfig config; @Inject public EntertainmentCommandRegistrar( - CooldownManager cooldownManager, ShipPairRepository shipPairRepository, BotConfig config) { + CooldownManager cooldownManager, + RatingService ratingService, + ShipPairRepository shipPairRepository, + BotConfig config) { this.cooldownManager = cooldownManager; + this.ratingService = ratingService; this.shipPairRepository = shipPairRepository; this.config = config; } @@ -55,22 +55,18 @@ public class EntertainmentCommandRegistrar implements CommandRegistrar { registry.register(new ComplimentCommand()); registry.register(new GlazbobaCommand()); - registry.register(new IqCommand(cooldownManager)); - registry.register(new GayCommand(cooldownManager)); - registry.register(new PpCommand(cooldownManager)); - registry.register(new RateCommand(cooldownManager)); - registry.register(new BoykisserCommand(cooldownManager)); - registry.register(new MustacheCommand(cooldownManager)); - registry.register(new FatCommand(cooldownManager)); + registry.register(new IqCommand(ratingService)); + registry.register(new GayCommand(ratingService)); + registry.register(new PpCommand(ratingService)); + registry.register(new RateCommand(ratingService)); + registry.register(new BoykisserCommand(ratingService)); + registry.register(new MustacheCommand(ratingService)); + registry.register(new FatCommand(ratingService)); registry.register(new PatCommand(cooldownManager)); - registry.register(new HugCommand()); - registry.register(new KissCommand(cooldownManager)); - registry.register(new SlapCommand(cooldownManager)); - registry.register(new TickleCommand(cooldownManager)); - registry.register(new PokeCommand(cooldownManager)); - registry.register(new CuddleCommand(cooldownManager)); - registry.register(new FeedCommand(cooldownManager)); + for (var flavor : SocialCommand.Flavor.values()) { + registry.register(new SocialCommand(flavor, cooldownManager)); + } registry.register( new RandomChoiceCommand(config, "choose", RandomChoiceCommand.Flavor.DEFAULT)); diff --git a/src/main/java/creator/loki/feature/entertainment/commands/BoykisserCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/BoykisserCommand.java deleted file mode 100644 index 2f6a1ad..0000000 --- a/src/main/java/creator/loki/feature/entertainment/commands/BoykisserCommand.java +++ /dev/null @@ -1,104 +0,0 @@ -package creator.loki.feature.entertainment.commands; - -import java.util.List; -import java.util.Random; - -import creator.loki.command.Command; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import net.dv8tion.jda.api.entities.Member; - -/** Boykisser метр с детерминированным результатом */ -public class BoykisserCommand implements Command { - - private final CooldownManager cooldownManager; - - public BoykisserCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "boykisser"; - } - - @Override - public String getDescription() { - return "На сколько ты boykisser (результат всегда одинаковый)"; - } - - @Override - public String getUsage() { - return "boykisser [@user]"; - } - - @Override - public String getCategory() { - return "Развлечения"; - } - - @Override - public void execute(CommandContext context) { - // Проверка cooldown (5 секунд) - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "boykisser")) { - long remaining = - cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "boykisser"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); - - if (target == null) return; - - if (target.getUser().isBot()) { - context.getChannel().sendMessage("❌ Боты не целуют мальчиков!").queue(); - return; - } - - // Применяем cooldown - cooldownManager.applyCooldown(context.getAuthor().getId(), "boykisser", 5); - - // Детерминированный процент - int boykisserLevel = getDeterministicValue(target.getId(), 777, 0, 100); - - // Прогресс-бар с поцелуями - StringBuilder bar = new StringBuilder(); - int filled = boykisserLevel / 10; - for (int i = 0; i < filled; i++) { - bar.append("💋"); - } - for (int i = filled; i < 10; i++) { - bar.append("⬜"); - } - - // Определяем уровень - RatingLevel level = RatingLevel.findLevel(boykisserLevel, RatingConfig.getBoykisserLevels()); - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("💋 Boykisser Метр") - .withDescription(target.getAsMention()) - .addField( - "Уровень", - bar.toString() + "\n**" + boykisserLevel + "%** - " + level.getComment(), - false) - .withThumbnail(target.getUser().getAvatarUrl()) - .withColor(level.getColor()) - .withFooter("💡 Детерминировано вашим Discord ID") - .build()) - .queue(); - } - - private static final Random RANDOM = new Random(); - - private int getDeterministicValue(String userId, int salt, int min, int max) { - long seed = Long.parseLong(userId) + salt; - RANDOM.setSeed(seed); - return RANDOM.nextInt(max - min + 1) + min; - } -} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/FatCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/FatCommand.java deleted file mode 100644 index 62d45ec..0000000 --- a/src/main/java/creator/loki/feature/entertainment/commands/FatCommand.java +++ /dev/null @@ -1,98 +0,0 @@ -package creator.loki.feature.entertainment.commands; - -import java.util.List; -import java.util.Random; - -import creator.loki.command.Command; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import net.dv8tion.jda.api.entities.Member; - -/** Fat метр - проверка на жирность */ -public class FatCommand implements Command { - - private final CooldownManager cooldownManager; - - public FatCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "fat"; - } - - @Override - public String getDescription() { - return "Проверка на жирность (результат всегда одинаковый)"; - } - - @Override - public String getUsage() { - return "fat [@user]"; - } - - @Override - public String getCategory() { - return "Развлечения"; - } - - @Override - public void execute(CommandContext context) { - // Проверка cooldown (5 секунд) - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "fat")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "fat"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); - - if (target == null) return; - - // Применяем cooldown - cooldownManager.applyCooldown(context.getAuthor().getId(), "fat", 5); - - // Детерминированный процент от 0 до 100 - int fatLevel = getDeterministicValue(target.getId(), 999, 0, 100); - - // Прогресс-бар - StringBuilder bar = new StringBuilder(); - int filled = fatLevel / 10; - for (int i = 0; i < filled; i++) { - bar.append("🟩"); - } - for (int i = filled; i < 10; i++) { - bar.append("⬜"); - } - - // Определяем уровень - RatingLevel level = RatingLevel.findLevel(fatLevel, RatingConfig.getFatLevels()); - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("🐷 Fat Метр") - .withDescription(target.getAsMention()) - .addField( - "Жирность", - bar.toString() + "\n**" + fatLevel + "%** - " + level.getComment(), - false) - .withThumbnail(target.getUser().getAvatarUrl()) - .withColor(level.getColor()) - .withFooter("💡 Детерминировано вашим Discord ID") - .build()) - .queue(); - } - - private static final Random RANDOM = new Random(); - - private int getDeterministicValue(String userId, int salt, int min, int max) { - long seed = Long.parseLong(userId) + salt; - RANDOM.setSeed(seed); - return RANDOM.nextInt(max - min + 1) + min; - } -} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/GayCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/GayCommand.java deleted file mode 100644 index 14f253d..0000000 --- a/src/main/java/creator/loki/feature/entertainment/commands/GayCommand.java +++ /dev/null @@ -1,99 +0,0 @@ -package creator.loki.feature.entertainment.commands; - -import java.awt.Color; -import java.util.List; -import java.util.Random; - -import creator.loki.command.Command; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import net.dv8tion.jda.api.entities.Member; - -/** Gay-метр с детерминированным результатом */ -public class GayCommand implements Command { - - private final CooldownManager cooldownManager; - - public GayCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "gay"; - } - - @Override - public String getDescription() { - return "Гей-метр (результат всегда одинаковый)"; - } - - @Override - public String getUsage() { - return "gay [@user]"; - } - - @Override - public String getCategory() { - return "Развлечения"; - } - - @Override - public void execute(CommandContext context) { - // Проверка cooldown (5 секунд) - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "gay")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "gay"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); - - if (target == null) return; - - if (target.getUser().isBot()) { - context.getChannel().sendMessage("❌ Боты не имеют ориентации!").queue(); - return; - } - - // Применяем cooldown - cooldownManager.applyCooldown(context.getAuthor().getId(), "gay", 5); - - // Детерминированный процент - int gayLevel = getDeterministicValue(target.getId(), 456, 0, 100); - - // Прогресс-бар с радугой - String[] rainbow = {"🟥", "🟧", "🟨", "🟩", "🟦", "🟪"}; - StringBuilder bar = new StringBuilder(); - int filled = Math.min(gayLevel / 17 + 1, 6); - for (int i = 0; i < filled; i++) { - bar.append(rainbow[i]); - } - for (int i = filled; i < 6; i++) { - bar.append("⬜"); - } - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("🏳️‍🌈 Гей-метр") - .withDescription(target.getAsMention()) - .addField("Уровень", bar.toString() + "\n**" + gayLevel + "%**", false) - .withThumbnail(target.getUser().getAvatarUrl()) - .withColor(new Color(255, 0, 255)) - .withFooter("💡 Это просто мем, не воспринимайте серьезно") - .build()) - .queue(); - } - - private static final Random RANDOM = new Random(); - - private int getDeterministicValue(String userId, int salt, int min, int max) { - long seed = Long.parseLong(userId) + salt; - RANDOM.setSeed(seed); - return RANDOM.nextInt(max - min + 1) + min; - } -} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/IqCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/IqCommand.java deleted file mode 100644 index 5f83593..0000000 --- a/src/main/java/creator/loki/feature/entertainment/commands/IqCommand.java +++ /dev/null @@ -1,92 +0,0 @@ -package creator.loki.feature.entertainment.commands; - -import java.util.List; -import java.util.Random; - -import creator.loki.command.Command; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import net.dv8tion.jda.api.entities.Member; - -/** IQ тест с детерминированным результатом */ -public class IqCommand implements Command { - - private final CooldownManager cooldownManager; - - public IqCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "iq"; - } - - @Override - public String getDescription() { - return "Тест IQ (результат всегда одинаковый)"; - } - - @Override - public String getUsage() { - return "iq [@user]"; - } - - @Override - public String getCategory() { - return "Развлечения"; - } - - @Override - public void execute(CommandContext context) { - // Проверка cooldown (5 секунд) - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "iq")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "iq"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); - - if (target == null) return; - - // Применяем cooldown - cooldownManager.applyCooldown(context.getAuthor().getId(), "iq", 5); - - int iq; - if (target.getUser().isBot()) { - iq = 9000; // Боты умные! - } else if (target.getId().equals("1414975197269987471")) { - iq = 1000; // Специальное значение - } else { - // Детерминированный IQ от 0 до 140 - iq = getDeterministicValue(target.getId(), 123, 0, 140); - } - - // Определяем уровень - RatingLevel level = RatingLevel.findLevel(iq, RatingConfig.getIqLevels()); - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("🧠 Тест IQ") - .withDescription("Результат для " + target.getAsMention()) - .addField("IQ Уровень", "**" + iq + "** - " + level.getComment(), false) - .withThumbnail(target.getUser().getAvatarUrl()) - .withColor(level.getColor()) - .withFooter("💡 Научно доказано вашим ID") - .build()) - .queue(); - } - - private static final Random RANDOM = new Random(); - - private int getDeterministicValue(String userId, int salt, int min, int max) { - long seed = Long.parseLong(userId) + salt; - RANDOM.setSeed(seed); - return RANDOM.nextInt(max - min + 1) + min; - } -} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/MustacheCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/MustacheCommand.java deleted file mode 100644 index 6090793..0000000 --- a/src/main/java/creator/loki/feature/entertainment/commands/MustacheCommand.java +++ /dev/null @@ -1,107 +0,0 @@ -package creator.loki.feature.entertainment.commands; - -import java.util.List; -import java.util.Random; - -import creator.loki.command.Command; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import net.dv8tion.jda.api.entities.Member; - -/** Mustache метр - проверка на усатость */ -public class MustacheCommand implements Command { - - private final CooldownManager cooldownManager; - - public MustacheCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "mustache"; - } - - @Override - public String getDescription() { - return "Проверка на усатость (результат всегда одинаковый)"; - } - - @Override - public String getUsage() { - return "mustache [@user]"; - } - - @Override - public String getCategory() { - return "Развлечения"; - } - - @Override - public void execute(CommandContext context) { - // Проверка cooldown (5 секунд) - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "mustache")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "mustache"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); - - if (target == null) return; - - // Применяем cooldown - cooldownManager.applyCooldown(context.getAuthor().getId(), "mustache", 5); - - int mustacheLevel; - - // Специальные значения - if (target.getUser().isBot() || target.getId().equals("1414975197269987471")) { - mustacheLevel = 0; // Боты и специальный ID = 0% - } else if (target.getId().equals("678283217094180915")) { - mustacheLevel = 1000; // Специальный ID = 1000% - } else { - // Детерминированный процент от 0 до 100 - mustacheLevel = getDeterministicValue(target.getId(), 888, 0, 100); - } - - // Прогресс-бар с усами - StringBuilder bar = new StringBuilder(); - int filled = Math.min(mustacheLevel / 10, 10); - for (int i = 0; i < filled; i++) { - bar.append("🧔"); - } - for (int i = filled; i < 10; i++) { - bar.append("⬜"); - } - - // Определяем уровень - RatingLevel level = RatingLevel.findLevel(mustacheLevel, RatingConfig.getMustacheLevels()); - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("🧔 Mustache Метр") - .withDescription(target.getAsMention()) - .addField( - "Усатость", - bar.toString() + "\n**" + mustacheLevel + "%** - " + level.getComment(), - false) - .withThumbnail(target.getUser().getAvatarUrl()) - .withColor(level.getColor()) - .withFooter("💡 Детерминировано вашим Discord ID") - .build()) - .queue(); - } - - private static final Random RANDOM = new Random(); - - private int getDeterministicValue(String userId, int salt, int min, int max) { - long seed = Long.parseLong(userId) + salt; - RANDOM.setSeed(seed); - return RANDOM.nextInt(max - min + 1) + min; - } -} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/PpCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/PpCommand.java deleted file mode 100644 index a55accd..0000000 --- a/src/main/java/creator/loki/feature/entertainment/commands/PpCommand.java +++ /dev/null @@ -1,111 +0,0 @@ -package creator.loki.feature.entertainment.commands; - -import java.util.List; -import java.util.Random; - -import creator.loki.command.Command; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import net.dv8tion.jda.api.entities.Member; - -/** PP размер с детерминированным результатом */ -public class PpCommand implements Command { - - private final CooldownManager cooldownManager; - - public PpCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "pp"; - } - - @Override - public String getDescription() { - return "Измеритель pp (результат всегда одинаковый)"; - } - - @Override - public String getUsage() { - return "pp [@user]"; - } - - @Override - public String getCategory() { - return "Развлечения"; - } - - @Override - public void execute(CommandContext context) { - // Проверка cooldown (5 секунд) - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "pp")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "pp"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); - - if (target == null) return; - - if (target.getUser().isBot()) { - context.getChannel().sendMessage("❌ У ботов нет pp!").queue(); - return; - } - - // Применяем cooldown - cooldownManager.applyCooldown(context.getAuthor().getId(), "pp", 5); - - // Специальное значение для конкретного пользователя - int size; - if (target.getId().equals("1414975197269987471")) { - size = 100; - } else { - // Детерминированный размер от 0 до 30 см - size = getDeterministicValue(target.getId(), 6969, 0, 30); - } - - // Визуализация - StringBuilder ppVisual = new StringBuilder("8"); - for (int i = 0; i < size; i++) { - ppVisual.append("="); - } - ppVisual.append("D"); - - // Определяем уровень - RatingLevel level = RatingLevel.findLevel(size, RatingConfig.getPpLevels()); - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("🍆 PP Размер") - .withDescription(target.getAsMention()) - .addField( - "Размер", - "```" - + ppVisual.toString() - + "```\n**" - + size - + " см** - " - + level.getComment(), - false) - .withThumbnail(target.getUser().getAvatarUrl()) - .withColor(level.getColor()) - .withFooter("💡 Детерминировано вашим Discord ID") - .build()) - .queue(); - } - - private static final Random RANDOM = new Random(); - - private int getDeterministicValue(String userId, int salt, int min, int max) { - long seed = Long.parseLong(userId) + salt; - RANDOM.setSeed(seed); - return RANDOM.nextInt(max - min + 1) + min; - } -} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/RatingConfig.java b/src/main/java/creator/loki/feature/entertainment/commands/RatingConfig.java deleted file mode 100644 index 26cd9f6..0000000 --- a/src/main/java/creator/loki/feature/entertainment/commands/RatingConfig.java +++ /dev/null @@ -1,64 +0,0 @@ -package creator.loki.feature.entertainment.commands; - -import java.awt.Color; -import java.util.Arrays; -import java.util.List; - -/** Конфигурации уровней для различных rating команд */ -public class RatingConfig { - - /** Уровни для Mustache команды */ - public static List getMustacheLevels() { - return Arrays.asList( - new RatingLevel(1000, "🎩 ЛЕГЕНДАРНЫЕ УСИЩИ!", new Color(255, 215, 0)), - new RatingLevel(80, "🧔 Роскошные усы!", new Color(139, 69, 19)), - new RatingLevel(60, "👨 Хорошие усы", new Color(160, 82, 45)), - new RatingLevel(40, "😊 Усики растут", new Color(205, 133, 63)), - new RatingLevel(20, "🙂 Лёгкая щетина", new Color(210, 180, 140)), - new RatingLevel(1, "😐 Почти нет усов", new Color(128, 128, 128)), - new RatingLevel(0, "🪒 Гладко выбрит", new Color(200, 200, 200))); - } - - /** Уровни для IQ команды */ - public static List getIqLevels() { - return Arrays.asList( - new RatingLevel(140, "🧠 Гений!", new Color(255, 215, 0)), - new RatingLevel(120, "🎓 Очень умный", new Color(100, 255, 150)), - new RatingLevel(100, "📚 Средний", new Color(52, 152, 219)), - new RatingLevel(80, "📖 Ниже среднего", new Color(255, 165, 0)), - new RatingLevel(0, "🤔 Низкий", new Color(231, 76, 60))); - } - - /** Уровни для Fat команды */ - public static List getFatLevels() { - return Arrays.asList( - new RatingLevel(90, "🐷 СВИНОТА!", new Color(255, 0, 0)), - new RatingLevel(70, "🍔 Очень жирный", new Color(255, 100, 0)), - new RatingLevel(50, "🍕 Жирноват", new Color(255, 165, 0)), - new RatingLevel(30, "👍 Нормальный вес", new Color(0, 200, 0)), - new RatingLevel(10, "🥗 Худой", new Color(100, 200, 255)), - new RatingLevel(0, "💀 Очень худой", new Color(150, 150, 150))); - } - - /** Уровни для PP команды (размер в см) */ - public static List getPpLevels() { - return Arrays.asList( - new RatingLevel(25, "😳 НЕВЕРОЯТНО!", new Color(255, 215, 0)), - new RatingLevel(20, "😮 Впечатляет", new Color(100, 255, 150)), - new RatingLevel(15, "👍 Выше среднего", new Color(52, 152, 219)), - new RatingLevel(10, "🙂 Средний", new Color(128, 128, 128)), - new RatingLevel(5, "😐 Ниже среднего", new Color(255, 165, 0)), - new RatingLevel(0, "💀 Микро", new Color(231, 76, 60))); - } - - /** Уровни для Boykisser команды */ - public static List getBoykisserLevels() { - return Arrays.asList( - new RatingLevel(90, "💋 ЛЕГЕНДАРНЫЙ BOYKISSER!", new Color(255, 105, 180)), - new RatingLevel(70, "😘 Активный boykisser", new Color(255, 182, 193)), - new RatingLevel(50, "😊 Любит целоваться", new Color(255, 192, 203)), - new RatingLevel(30, "🙂 Иногда целуется", new Color(255, 218, 185)), - new RatingLevel(10, "😐 Редко целуется", new Color(176, 196, 222)), - new RatingLevel(0, "😶 Не целуется", new Color(128, 128, 128))); - } -} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/BoykisserCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/BoykisserCommand.java new file mode 100644 index 0000000..1c15407 --- /dev/null +++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/BoykisserCommand.java @@ -0,0 +1,70 @@ +package creator.loki.feature.entertainment.commands.rating; + +import com.google.inject.Inject; +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.feature.entertainment.service.RatingService; +import net.dv8tion.jda.api.entities.Member; + +public class BoykisserCommand implements Command { + + private final RatingService ratingService; + + @Inject + public BoykisserCommand(RatingService ratingService) { + this.ratingService = ratingService; + } + + @Override + public String getName() { + return "boykisser"; + } + + @Override + public String getDescription() { + return "На сколько ты boykisser (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "boykisser [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (!ratingService.checkAndApplyCooldown(context, "boykisser")) return; + Member target = ratingService.resolveTarget(context); + if (target == null) return; + if (target.getUser().isBot()) { + context.getChannel().sendMessage("❌ Боты не целуют мальчиков!").queue(); + return; + } + + int boykisserLevel = ratingService.getDeterministicValue(target.getId(), 777, 0, 100); + StringBuilder bar = new StringBuilder(); + int filled = boykisserLevel / 10; + for (int i = 0; i < filled; i++) bar.append("💋"); + for (int i = filled; i < 10; i++) bar.append("⬜"); + + RatingLevel level = RatingLevel.findLevel(boykisserLevel, RatingConfig.getBoykisserLevels()); + context + .getChannel() + .sendMessageEmbeds( + EmbedFactory.create() + .withTitle("💋 Boykisser Метр") + .withDescription(target.getAsMention()) + .addField( + "Уровень", bar + "\n**" + boykisserLevel + "%** - " + level.getComment(), false) + .withThumbnail(target.getUser().getAvatarUrl()) + .withColor(level.getColor()) + .withFooter("💡 Детерминировано вашим Discord ID") + .build()) + .queue(); + } +} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/FatCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/FatCommand.java new file mode 100644 index 0000000..3efd589 --- /dev/null +++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/FatCommand.java @@ -0,0 +1,66 @@ +package creator.loki.feature.entertainment.commands.rating; + +import com.google.inject.Inject; +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.feature.entertainment.service.RatingService; +import net.dv8tion.jda.api.entities.Member; + +public class FatCommand implements Command { + + private final RatingService ratingService; + + @Inject + public FatCommand(RatingService ratingService) { + this.ratingService = ratingService; + } + + @Override + public String getName() { + return "fat"; + } + + @Override + public String getDescription() { + return "Проверка на жирность (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "fat [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (!ratingService.checkAndApplyCooldown(context, "fat")) return; + Member target = ratingService.resolveTarget(context); + if (target == null) return; + + int fatLevel = ratingService.getDeterministicValue(target.getId(), 999, 0, 100); + StringBuilder bar = new StringBuilder(); + int filled = fatLevel / 10; + for (int i = 0; i < filled; i++) bar.append("🟩"); + for (int i = filled; i < 10; i++) bar.append("⬜"); + + RatingLevel level = RatingLevel.findLevel(fatLevel, RatingConfig.getFatLevels()); + context + .getChannel() + .sendMessageEmbeds( + EmbedFactory.create() + .withTitle("🐷 Fat Метр") + .withDescription(target.getAsMention()) + .addField( + "Жирность", bar + "\n**" + fatLevel + "%** - " + level.getComment(), false) + .withThumbnail(target.getUser().getAvatarUrl()) + .withColor(level.getColor()) + .withFooter("💡 Детерминировано вашим Discord ID") + .build()) + .queue(); + } +} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/GayCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/GayCommand.java new file mode 100644 index 0000000..5b8a3f4 --- /dev/null +++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/GayCommand.java @@ -0,0 +1,71 @@ +package creator.loki.feature.entertainment.commands.rating; + +import java.awt.Color; + +import com.google.inject.Inject; +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.feature.entertainment.service.RatingService; +import net.dv8tion.jda.api.entities.Member; + +public class GayCommand implements Command { + + private final RatingService ratingService; + + @Inject + public GayCommand(RatingService ratingService) { + this.ratingService = ratingService; + } + + @Override + public String getName() { + return "gay"; + } + + @Override + public String getDescription() { + return "Гей-метр (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "gay [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (!ratingService.checkAndApplyCooldown(context, "gay")) return; + Member target = ratingService.resolveTarget(context); + if (target == null) return; + if (target.getUser().isBot()) { + context.getChannel().sendMessage("❌ Боты не имеют ориентации!").queue(); + return; + } + + int gayLevel = ratingService.getDeterministicValue(target.getId(), 456, 0, 100); + String[] rainbow = {"🟥", "🟧", "🟨", "🟩", "🟦", "🟪"}; + StringBuilder bar = new StringBuilder(); + int filled = Math.min(gayLevel / 17 + 1, 6); + for (int i = 0; i < filled; i++) bar.append(rainbow[i]); + for (int i = filled; i < 6; i++) bar.append("⬜"); + + context + .getChannel() + .sendMessageEmbeds( + EmbedFactory.create() + .withTitle("🏳️‍🌈 Гей-метр") + .withDescription(target.getAsMention()) + .addField("Уровень", bar + "\n**" + gayLevel + "%**", false) + .withThumbnail(target.getUser().getAvatarUrl()) + .withColor(new Color(255, 0, 255)) + .withFooter("💡 Это просто мем, не воспринимайте серьезно") + .build()) + .queue(); + } +} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/IqCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/IqCommand.java new file mode 100644 index 0000000..5f0723d --- /dev/null +++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/IqCommand.java @@ -0,0 +1,68 @@ +package creator.loki.feature.entertainment.commands.rating; + +import com.google.inject.Inject; +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.feature.entertainment.service.RatingService; +import net.dv8tion.jda.api.entities.Member; + +public class IqCommand implements Command { + + private final RatingService ratingService; + + @Inject + public IqCommand(RatingService ratingService) { + this.ratingService = ratingService; + } + + @Override + public String getName() { + return "iq"; + } + + @Override + public String getDescription() { + return "Тест IQ (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "iq [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (!ratingService.checkAndApplyCooldown(context, "iq")) return; + Member target = ratingService.resolveTarget(context); + if (target == null) return; + + int iq; + if (target.getUser().isBot()) { + iq = 9000; + } else if (target.getId().equals("1414975197269987471")) { + iq = 1000; + } else { + iq = ratingService.getDeterministicValue(target.getId(), 123, 0, 140); + } + + RatingLevel level = RatingLevel.findLevel(iq, RatingConfig.getIqLevels()); + context + .getChannel() + .sendMessageEmbeds( + EmbedFactory.create() + .withTitle("🧠 Тест IQ") + .withDescription("Результат для " + target.getAsMention()) + .addField("IQ Уровень", "**" + iq + "** - " + level.getComment(), false) + .withThumbnail(target.getUser().getAvatarUrl()) + .withColor(level.getColor()) + .withFooter("💡 Научно доказано вашим ID") + .build()) + .queue(); + } +} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/MustacheCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/MustacheCommand.java new file mode 100644 index 0000000..c924d99 --- /dev/null +++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/MustacheCommand.java @@ -0,0 +1,74 @@ +package creator.loki.feature.entertainment.commands.rating; + +import com.google.inject.Inject; +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.feature.entertainment.service.RatingService; +import net.dv8tion.jda.api.entities.Member; + +public class MustacheCommand implements Command { + + private final RatingService ratingService; + + @Inject + public MustacheCommand(RatingService ratingService) { + this.ratingService = ratingService; + } + + @Override + public String getName() { + return "mustache"; + } + + @Override + public String getDescription() { + return "Проверка на усатость (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "mustache [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (!ratingService.checkAndApplyCooldown(context, "mustache")) return; + Member target = ratingService.resolveTarget(context); + if (target == null) return; + + int mustacheLevel; + if (target.getUser().isBot() || target.getId().equals("1414975197269987471")) { + mustacheLevel = 0; + } else if (target.getId().equals("678283217094180915")) { + mustacheLevel = 1000; + } else { + mustacheLevel = ratingService.getDeterministicValue(target.getId(), 888, 0, 100); + } + + StringBuilder bar = new StringBuilder(); + int filled = Math.min(mustacheLevel / 10, 10); + for (int i = 0; i < filled; i++) bar.append("🧔"); + for (int i = filled; i < 10; i++) bar.append("⬜"); + + RatingLevel level = RatingLevel.findLevel(mustacheLevel, RatingConfig.getMustacheLevels()); + context + .getChannel() + .sendMessageEmbeds( + EmbedFactory.create() + .withTitle("🧔 Mustache Метр") + .withDescription(target.getAsMention()) + .addField( + "Усатость", bar + "\n**" + mustacheLevel + "%** - " + level.getComment(), false) + .withThumbnail(target.getUser().getAvatarUrl()) + .withColor(level.getColor()) + .withFooter("💡 Детерминировано вашим Discord ID") + .build()) + .queue(); + } +} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/PpCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/PpCommand.java new file mode 100644 index 0000000..35f0905 --- /dev/null +++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/PpCommand.java @@ -0,0 +1,77 @@ +package creator.loki.feature.entertainment.commands.rating; + +import com.google.inject.Inject; +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.feature.entertainment.service.RatingService; +import net.dv8tion.jda.api.entities.Member; + +public class PpCommand implements Command { + + private final RatingService ratingService; + + @Inject + public PpCommand(RatingService ratingService) { + this.ratingService = ratingService; + } + + @Override + public String getName() { + return "pp"; + } + + @Override + public String getDescription() { + return "Измеритель pp (результат всегда одинаковый)"; + } + + @Override + public String getUsage() { + return "pp [@user]"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (!ratingService.checkAndApplyCooldown(context, "pp")) return; + Member target = ratingService.resolveTarget(context); + if (target == null) return; + if (target.getUser().isBot()) { + context.getChannel().sendMessage("❌ У ботов нет pp!").queue(); + return; + } + + int size; + if (target.getId().equals("1414975197269987471")) { + size = 100; + } else { + size = ratingService.getDeterministicValue(target.getId(), 6969, 0, 30); + } + + StringBuilder ppVisual = new StringBuilder("8"); + for (int i = 0; i < size; i++) ppVisual.append("="); + ppVisual.append("D"); + + RatingLevel level = RatingLevel.findLevel(size, RatingConfig.getPpLevels()); + context + .getChannel() + .sendMessageEmbeds( + EmbedFactory.create() + .withTitle("🍆 PP Размер") + .withDescription(target.getAsMention()) + .addField( + "Размер", + "```" + ppVisual + "```\n**" + size + " см** - " + level.getComment(), + false) + .withThumbnail(target.getUser().getAvatarUrl()) + .withColor(level.getColor()) + .withFooter("💡 Детерминировано вашим Discord ID") + .build()) + .queue(); + } +} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/RateCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/RateCommand.java similarity index 56% rename from src/main/java/creator/loki/feature/entertainment/commands/RateCommand.java rename to src/main/java/creator/loki/feature/entertainment/commands/rating/RateCommand.java index c48397c..1b9ee50 100644 --- a/src/main/java/creator/loki/feature/entertainment/commands/RateCommand.java +++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/RateCommand.java @@ -1,24 +1,23 @@ -package creator.loki.feature.entertainment.commands; +package creator.loki.feature.entertainment.commands.rating; import java.awt.Color; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Random; +import com.google.inject.Inject; import creator.loki.command.Command; import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; import creator.loki.embed.EmbedFactory; +import creator.loki.feature.entertainment.service.RatingService; import net.dv8tion.jda.api.entities.Member; -/** Оценка пользователя из 10 с детерминированным результатом */ public class RateCommand implements Command { - private final CooldownManager cooldownManager; + private final RatingService ratingService; - public RateCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; + @Inject + public RateCommand(RatingService ratingService) { + this.ratingService = ratingService; } @Override @@ -43,28 +42,13 @@ public class RateCommand implements Command { @Override public void execute(CommandContext context) { - // Проверка cooldown (5 секунд) - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "rate")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "rate"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); - + if (!ratingService.checkAndApplyCooldown(context, "rate")) return; + Member target = ratingService.resolveTarget(context); if (target == null) return; - // Применяем cooldown - cooldownManager.applyCooldown(context.getAuthor().getId(), "rate", 5); - - // Детерминированная оценка от 1 до 10 - int rating = getDeterministicValue(target.getId(), 420, 1, 10); - - // Визуализация звездами + int rating = ratingService.getDeterministicValue(target.getId(), 420, 1, 10); String stars = "⭐".repeat(rating) + "☆".repeat(10 - rating); - // Комментарий Map comments = new HashMap<>(); comments.put(10, "💯 ИДЕАЛЬНО!"); comments.put(9, "🔥 Почти идеально!"); @@ -92,12 +76,4 @@ public class RateCommand implements Command { .build()) .queue(); } - - private static final Random RANDOM = new Random(); - - private int getDeterministicValue(String userId, int salt, int min, int max) { - long seed = Long.parseLong(userId) + salt; - RANDOM.setSeed(seed); - return RANDOM.nextInt(max - min + 1) + min; - } } diff --git a/src/main/java/creator/loki/feature/entertainment/commands/rating/RatingConfig.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/RatingConfig.java new file mode 100644 index 0000000..fed7f33 --- /dev/null +++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/RatingConfig.java @@ -0,0 +1,81 @@ +package creator.loki.feature.entertainment.commands.rating; + +import java.awt.Color; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class RatingConfig { + private static final Map> LEVELS; + + static { + var m = new LinkedHashMap>(); + m.put( + "mustache", + List.of( + new RatingLevel(1000, "🎩 ЛЕГЕНДАРНЫЕ УСИЩИ!", new Color(255, 215, 0)), + new RatingLevel(80, "🧔 Роскошные усы!", new Color(139, 69, 19)), + new RatingLevel(60, "👨 Хорошие усы", new Color(160, 82, 45)), + new RatingLevel(40, "😊 Усики растут", new Color(205, 133, 63)), + new RatingLevel(20, "🙂 Лёгкая щетина", new Color(210, 180, 140)), + new RatingLevel(1, "😐 Почти нет усов", new Color(128, 128, 128)), + new RatingLevel(0, "🪒 Гладко выбрит", new Color(200, 200, 200)))); + m.put( + "iq", + List.of( + new RatingLevel(140, "🧠 Гений!", new Color(255, 215, 0)), + new RatingLevel(120, "🎓 Очень умный", new Color(100, 255, 150)), + new RatingLevel(100, "📚 Средний", new Color(52, 152, 219)), + new RatingLevel(80, "📖 Ниже среднего", new Color(255, 165, 0)), + new RatingLevel(0, "🤔 Низкий", new Color(231, 76, 60)))); + m.put( + "fat", + List.of( + new RatingLevel(90, "🐷 СВИНОТА!", new Color(255, 0, 0)), + new RatingLevel(70, "🍔 Очень жирный", new Color(255, 100, 0)), + new RatingLevel(50, "🍕 Жирноват", new Color(255, 165, 0)), + new RatingLevel(30, "👍 Нормальный вес", new Color(0, 200, 0)), + new RatingLevel(10, "🥗 Худой", new Color(100, 200, 255)), + new RatingLevel(0, "💀 Очень худой", new Color(150, 150, 150)))); + m.put( + "pp", + List.of( + new RatingLevel(25, "😳 НЕВЕРОЯТНО!", new Color(255, 215, 0)), + new RatingLevel(20, "😮 Впечатляет", new Color(100, 255, 150)), + new RatingLevel(15, "👍 Выше среднего", new Color(52, 152, 219)), + new RatingLevel(10, "🙂 Средний", new Color(128, 128, 128)), + new RatingLevel(5, "😐 Ниже среднего", new Color(255, 165, 0)), + new RatingLevel(0, "💀 Микро", new Color(231, 76, 60)))); + m.put( + "boykisser", + List.of( + new RatingLevel(90, "💋 ЛЕГЕНДАРНЫЙ BOYKISSER!", new Color(255, 105, 180)), + new RatingLevel(70, "😘 Активный boykisser", new Color(255, 182, 193)), + new RatingLevel(50, "😊 Любит целоваться", new Color(255, 192, 203)), + new RatingLevel(30, "🙂 Иногда целуется", new Color(255, 218, 185)), + new RatingLevel(10, "😐 Редко целуется", new Color(176, 196, 222)), + new RatingLevel(0, "😶 Не целуется", new Color(128, 128, 128)))); + LEVELS = Collections.unmodifiableMap(m); + } + + public static List getMustacheLevels() { + return LEVELS.get("mustache"); + } + + public static List getIqLevels() { + return LEVELS.get("iq"); + } + + public static List getFatLevels() { + return LEVELS.get("fat"); + } + + public static List getPpLevels() { + return LEVELS.get("pp"); + } + + public static List getBoykisserLevels() { + return LEVELS.get("boykisser"); + } +} diff --git a/src/main/java/creator/loki/feature/entertainment/commands/RatingLevel.java b/src/main/java/creator/loki/feature/entertainment/commands/rating/RatingLevel.java similarity index 95% rename from src/main/java/creator/loki/feature/entertainment/commands/RatingLevel.java rename to src/main/java/creator/loki/feature/entertainment/commands/rating/RatingLevel.java index d507486..54821df 100644 --- a/src/main/java/creator/loki/feature/entertainment/commands/RatingLevel.java +++ b/src/main/java/creator/loki/feature/entertainment/commands/rating/RatingLevel.java @@ -1,4 +1,4 @@ -package creator.loki.feature.entertainment.commands; +package creator.loki.feature.entertainment.commands.rating; import java.awt.Color; import java.util.List; diff --git a/src/main/java/creator/loki/feature/entertainment/service/RatingService.java b/src/main/java/creator/loki/feature/entertainment/service/RatingService.java new file mode 100644 index 0000000..e4f2e82 --- /dev/null +++ b/src/main/java/creator/loki/feature/entertainment/service/RatingService.java @@ -0,0 +1,47 @@ +package creator.loki.feature.entertainment.service; + +import java.time.Duration; +import java.util.List; +import java.util.Random; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import creator.loki.command.CommandContext; +import creator.loki.core.security.CooldownManager; +import net.dv8tion.jda.api.entities.Member; + +@Singleton +public class RatingService { + private static final Random RANDOM = new Random(); + private static final Duration COOLDOWN = Duration.ofSeconds(5); + + private final CooldownManager cooldownManager; + + @Inject + public RatingService(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + public int getDeterministicValue(String userId, int salt, int min, int max) { + long seed = Long.parseLong(userId) + salt; + RANDOM.setSeed(seed); + return RANDOM.nextInt(max - min + 1) + min; + } + + public Member resolveTarget(CommandContext context) { + List mentioned = context.getMessage().getMentions().getMembers(); + return mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0); + } + + public boolean checkAndApplyCooldown(CommandContext context, String commandName) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), commandName)) { + long remaining = + cooldownManager.getRemainingSeconds(context.getAuthor().getId(), commandName); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return false; + } + cooldownManager.applyCooldown( + context.getAuthor().getId(), commandName, (int) COOLDOWN.toSeconds()); + return true; + } +} diff --git a/src/main/java/creator/loki/feature/help/HelpMenuHandler.java b/src/main/java/creator/loki/feature/help/HelpMenuHandler.java index e8aef4c..fff3579 100644 --- a/src/main/java/creator/loki/feature/help/HelpMenuHandler.java +++ b/src/main/java/creator/loki/feature/help/HelpMenuHandler.java @@ -13,10 +13,10 @@ import creator.loki.core.registry.CommandRegistry; import creator.loki.embed.EmbedFactory; import creator.loki.embed.HelpCategories; import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.components.actionrow.ActionRow; import net.dv8tion.jda.api.components.selections.StringSelectMenu; +import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; @Singleton public class HelpMenuHandler extends ListenerAdapter { diff --git a/src/main/java/creator/loki/feature/help/commands/HelpCommand.java b/src/main/java/creator/loki/feature/help/commands/HelpCommand.java index d4c7949..eee9237 100644 --- a/src/main/java/creator/loki/feature/help/commands/HelpCommand.java +++ b/src/main/java/creator/loki/feature/help/commands/HelpCommand.java @@ -12,9 +12,9 @@ import creator.loki.core.registry.CommandRegistry; import creator.loki.embed.EmbedFactory; import creator.loki.embed.HelpCategories; import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.components.actionrow.ActionRow; import net.dv8tion.jda.api.components.selections.StringSelectMenu; +import net.dv8tion.jda.api.entities.User; public class HelpCommand implements Command { private final CommandRegistry registry; @@ -75,7 +75,11 @@ public class HelpCommand implements Command { net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji)); }); - context.getChannel().sendMessageEmbeds(embed.build()).addComponents(ActionRow.of(menuBuilder.build())).queue(); + context + .getChannel() + .sendMessageEmbeds(embed.build()) + .addComponents(ActionRow.of(menuBuilder.build())) + .queue(); } private EmbedFactory createMainPage( diff --git a/src/main/java/creator/loki/feature/level/MessageListener.java b/src/main/java/creator/loki/feature/level/MessageListener.java index 8cdebf8..23c07c4 100644 --- a/src/main/java/creator/loki/feature/level/MessageListener.java +++ b/src/main/java/creator/loki/feature/level/MessageListener.java @@ -5,7 +5,6 @@ import java.util.concurrent.ThreadLocalRandom; import com.google.inject.Inject; import com.google.inject.Singleton; import creator.loki.core.EventDispatcher; -import creator.loki.embed.EmbedTemplates; import creator.loki.feature.economy.service.EconomyService; import creator.loki.feature.level.domain.LevelUpResult; import creator.loki.feature.level.service.LevelService; @@ -20,13 +19,16 @@ public class MessageListener extends ListenerAdapter { private final EventDispatcher dispatcher; private final LevelService levelService; private final EconomyService economyService; + private final LevelUpNotifier levelUpNotifier; @Inject public MessageListener( - EventDispatcher dispatcher, LevelService levelService, EconomyService economyService) { + EventDispatcher dispatcher, LevelService levelService, + EconomyService economyService, LevelUpNotifier levelUpNotifier) { this.dispatcher = dispatcher; this.levelService = levelService; this.economyService = economyService; + this.levelUpNotifier = levelUpNotifier; } @Override @@ -35,19 +37,10 @@ public class MessageListener extends ListenerAdapter { if (!event.getAuthor().isBot()) { String userId = event.getAuthor().getId(); LevelUpResult levelUp = levelService.addMessageXP(userId); - if (levelUp != null) { - event - .getChannel() - .sendMessageEmbeds( - EmbedTemplates.success( - "🎉 Повышение уровня!", - event.getAuthor().getAsMention() - + " достиг **" - + levelUp.getNewLevel() - + " уровня**! Всего XP: **" - + levelUp.getTotalXP() - + "**")) - .queue(); + if (levelUp != null && event.getMember() != null) { + levelUpNotifier.sendLevelUpNotification( + event.getMember(), levelUp.getOldLevel(), levelUp.getNewLevel(), + event.getChannel()); } int lcAmount = ThreadLocalRandom.current().nextInt(1, 4); economyService.earnCash(userId, lcAmount); diff --git a/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java b/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java index e4c4bc6..c48a71f 100644 --- a/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java +++ b/src/main/java/creator/loki/feature/level/ResetLevelsButtonHandler.java @@ -11,10 +11,12 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** Обработчик кнопок подтверждения сброса уровней */ @Singleton public class ResetLevelsButtonHandler extends ListenerAdapter { private static final Logger logger = LoggerFactory.getLogger(ResetLevelsButtonHandler.class); + private static final String PREFIX_CONFIRM = "reset_levels_confirm:"; + private static final String PREFIX_CANCEL = "reset_levels_cancel:"; + private final ResetLevelsCommand resetCommand; @Inject @@ -26,87 +28,47 @@ public class ResetLevelsButtonHandler extends ListenerAdapter { 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:")) { - // Подтверждение + if (buttonId.startsWith(PREFIX_CONFIRM)) { + String authorId = buttonId.substring(PREFIX_CONFIRM.length()); + if (authorId.isEmpty()) return; + if (!event.getUser().getId().equals(authorId)) { + event.reply("❌ Только инициатор команды может подтвердить действие!").setEphemeral(true).queue(); + return; + } event.deferReply().queue(); - try { - String guildId = event.getGuild().getId(); - String userId = event.getUser().getId(); - - // Выполняем сброс - resetCommand.executeReset(guildId, userId); - - // Удаляем исходное сообщение с кнопками + resetCommand.executeReset(event.getGuild().getId(), event.getUser().getId()); event.getMessage().delete().queue(); - - // Отправляем подтверждение - event - .getHook() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("✅ Сброс уровней выполнен") - .withDescription( - "Уровни и XP всех пользователей успешно сброшены!\n\n" - + "**Сохранено:**\n" - + "• Время в голосовых каналах\n" - + "• Количество сообщений\n" - + "• Репутация\n\n" - + "**Сброшено:**\n" - + "• Уровень → 0\n" - + "• XP → 0\n\n" - + "Теперь используется новая система уровней!") - .withColor(Color.GREEN) - .withFooter( - "Инициатор: " + event.getUser().getName(), event.getUser().getAvatarUrl()) - .build()) - .queue(); - - LoggerDecorator.bot( - logger, "Сброс уровней подтверждён пользователем: " + event.getUser().getName()); - + event.getHook().sendMessageEmbeds( + EmbedFactory.create() + .withTitle("✅ Сброс уровней выполнен") + .withDescription("Уровни и XP всех пользователей успешно сброшены!\n\n" + + "**Сохранено:**\n• Время в голосовых каналах\n" + + "• Количество сообщений\n• Репутация\n\n" + + "**Сброшено:**\n• Уровень → 0\n• XP → 0\n\n" + + "Теперь используется новая система уровней!") + .withColor(Color.GREEN) + .withFooter("Инициатор: " + event.getUser().getName(), event.getUser().getAvatarUrl()) + .build()).queue(); + LoggerDecorator.bot(logger, "Сброс уровней подтверждён: " + event.getUser().getName()); } catch (Exception e) { LoggerDecorator.error(logger, "Ошибка при выполнении сброса", e); - event - .getHook() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("❌ Ошибка") - .withDescription("Произошла ошибка при сбросе уровней: " + e.getMessage()) - .withColor(Color.RED) - .build()) - .queue(); + event.getHook().sendMessageEmbeds( + EmbedFactory.create() + .withTitle("❌ Ошибка") + .withDescription("Произошла ошибка при сбросе уровней: " + e.getMessage()) + .withColor(Color.RED).build()).queue(); + } + } else if (buttonId.startsWith(PREFIX_CANCEL)) { + String authorId = buttonId.substring(PREFIX_CANCEL.length()); + if (authorId.isEmpty()) return; + if (!event.getUser().getId().equals(authorId)) { + event.reply("❌ Только инициатор команды может отменить действие!").setEphemeral(true).queue(); + return; } - - } else if (buttonId.startsWith("reset_levels_cancel:")) { - // Отмена event.getMessage().delete().queue(); event.reply("❌ Сброс уровней отменён.").setEphemeral(true).queue(); - - LoggerDecorator.bot( - logger, "Сброс уровней отменён пользователем: " + event.getUser().getName()); + LoggerDecorator.bot(logger, "Сброс уровней отменён: " + event.getUser().getName()); } } } diff --git a/src/main/java/creator/loki/feature/level/VoiceTracker.java b/src/main/java/creator/loki/feature/level/VoiceTracker.java index f3a11c9..f8be26a 100644 --- a/src/main/java/creator/loki/feature/level/VoiceTracker.java +++ b/src/main/java/creator/loki/feature/level/VoiceTracker.java @@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit; import com.google.inject.Inject; import com.google.inject.Singleton; import creator.loki.feature.economy.service.EconomyService; +import creator.loki.feature.level.domain.LevelUpResult; import creator.loki.feature.level.service.LevelService; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent; @@ -16,21 +17,22 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** Отслеживание времени в голосовых каналах */ @Singleton public class VoiceTracker extends ListenerAdapter { private static final Logger logger = LoggerFactory.getLogger(VoiceTracker.class); private final LevelService levelService; private final EconomyService economyService; + private final LevelUpNotifier levelUpNotifier; private final Map voiceJoinTimes = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private final Object lock = new Object(); @Inject - public VoiceTracker(LevelService levelService, EconomyService economyService) { + public VoiceTracker(LevelService levelService, EconomyService economyService, + LevelUpNotifier levelUpNotifier) { this.levelService = levelService; this.economyService = economyService; - - // Каждую минуту начисляем XP тем, кто в войсе + this.levelUpNotifier = levelUpNotifier; scheduler.scheduleAtFixedRate( () -> Thread.startVirtualThread(this::updateVoiceTime), 1, 1, TimeUnit.MINUTES); } @@ -39,66 +41,48 @@ public class VoiceTracker extends ListenerAdapter { public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) { Member member = event.getMember(); String userId = member.getId(); + if (member.getUser().isBot()) return; - // Игнорируем ботов - if (member.getUser().isBot()) { - return; - } - - // Пользователь зашёл в войс if (event.getChannelJoined() != null && event.getChannelLeft() == null) { voiceJoinTimes.put(userId, System.currentTimeMillis()); - logger.debug("Пользователь {} зашёл в войс", member.getEffectiveName()); } else if (event.getChannelLeft() != null && event.getChannelJoined() == null) { - // Пользователь вышел из войса - Long joinTime = voiceJoinTimes.remove(userId); + Long joinTime; + synchronized (lock) { joinTime = voiceJoinTimes.remove(userId); } if (joinTime != null) { - long timeSpent = System.currentTimeMillis() - joinTime; - long minutes = TimeUnit.MILLISECONDS.toMinutes(timeSpent); - + long minutes = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - joinTime); if (minutes > 0) { - levelService.addVoiceXP(userId, minutes); + LevelUpResult lr = levelService.addVoiceXP(userId, minutes); economyService.earnCash(userId, minutes / 2); - logger.debug( - "Пользователь {} провёл в войсе {} минут", member.getEffectiveName(), minutes); + if (lr != null && event.getMember() != null) { + levelUpNotifier.sendLevelUpNotification( + event.getMember(), lr.getOldLevel(), lr.getNewLevel(), + event.getGuild().getSystemChannel()); + } } } } else if (event.getChannelJoined() != null && event.getChannelLeft() != null) { - // Пользователь переключился между каналами - // Обновляем время входа - voiceJoinTimes.put(userId, System.currentTimeMillis()); + synchronized (lock) { voiceJoinTimes.put(userId, System.currentTimeMillis()); } } } - /** Периодическое обновление времени для тех, кто долго в войсе */ private void updateVoiceTime() { long now = System.currentTimeMillis(); - - for (Map.Entry entry : voiceJoinTimes.entrySet()) { - String userId = entry.getKey(); - long joinTime = entry.getValue(); - long timeSpent = now - joinTime; - long minutes = TimeUnit.MILLISECONDS.toMinutes(timeSpent); - - if (minutes >= 1) { - // Начисляем XP за каждую минуту - levelService.addVoiceXP(userId, 1); - economyService.earnCash(userId, 1); - // Обновляем время входа - voiceJoinTimes.put(userId, now); + synchronized (lock) { + for (Map.Entry entry : voiceJoinTimes.entrySet()) { + String userId = entry.getKey(); + long minutes = TimeUnit.MILLISECONDS.toMinutes(now - entry.getValue()); + if (minutes >= 1) { + levelService.addVoiceXP(userId, 1); + economyService.earnCash(userId, 1); + entry.setValue(now); + } } } } public void shutdown() { scheduler.shutdown(); - try { - if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { - scheduler.shutdownNow(); - } - } catch (InterruptedException e) { - scheduler.shutdownNow(); - Thread.currentThread().interrupt(); - } + try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) scheduler.shutdownNow(); } + catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); } } } diff --git a/src/main/java/creator/loki/feature/level/service/LevelService.java b/src/main/java/creator/loki/feature/level/service/LevelService.java index c237170..d527d28 100644 --- a/src/main/java/creator/loki/feature/level/service/LevelService.java +++ b/src/main/java/creator/loki/feature/level/service/LevelService.java @@ -36,6 +36,7 @@ public class LevelService { return null; } + cooldownService.markMessageSent(userId); UserStats stats = repository.getUserStats(userId); long now = System.currentTimeMillis(); int oldLevel = stats.getLevel(); diff --git a/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java b/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java index 7db7a9c..3c27581 100644 --- a/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java +++ b/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java @@ -1,55 +1,45 @@ package creator.loki.feature.level.service; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import creator.loki.feature.level.domain.UserStats; import creator.loki.feature.level.repository.UserStatsRepository; -/** - * Сервис для управления кулдауном сообщений Использует персистентное хранение через - * UserStats.lastMessageTime - */ public class MessageCooldownService { - private static final long MESSAGE_COOLDOWN_MS = 60000; // 1 минута + private static final long MESSAGE_COOLDOWN_MS = 60000; private final UserStatsRepository repository; + private final Map lastMessageTimes = new ConcurrentHashMap<>(); public MessageCooldownService(UserStatsRepository repository) { this.repository = repository; } - /** - * Проверить, может ли пользователь получить XP за сообщение - * - * @param userId ID пользователя - * @return true если кулдаун прошел - */ public boolean canReceiveXP(String userId) { - UserStats stats = repository.getUserStats(userId); long now = System.currentTimeMillis(); - long lastMessage = stats.getLastMessageTime(); - - return lastMessage == 0 || (now - lastMessage) >= MESSAGE_COOLDOWN_MS; + Long lastTime = lastMessageTimes.get(userId); + if (lastTime != null && (now - lastTime) < MESSAGE_COOLDOWN_MS) return false; + if (lastTime != null) return true; + long dbTime = repository.getUserStats(userId).getLastMessageTime(); + if (dbTime != 0) lastMessageTimes.put(userId, dbTime); + return dbTime == 0 || (now - dbTime) >= MESSAGE_COOLDOWN_MS; + } + + public void markMessageSent(String userId) { + lastMessageTimes.put(userId, System.currentTimeMillis()); } - /** - * Получить оставшееся время кулдауна в секундах - * - * @param userId ID пользователя - * @return секунды до окончания кулдауна, 0 если кулдаун прошел - */ public long getRemainingCooldown(String userId) { - UserStats stats = repository.getUserStats(userId); long now = System.currentTimeMillis(); - long lastMessage = stats.getLastMessageTime(); - - if (lastMessage == 0) { - return 0; + Long lastTime = lastMessageTimes.get(userId); + if (lastTime == null) { + long dbTime = repository.getUserStats(userId).getLastMessageTime(); + if (dbTime == 0) return 0; + lastMessageTimes.put(userId, dbTime); + lastTime = dbTime; } - - long elapsed = now - lastMessage; - if (elapsed >= MESSAGE_COOLDOWN_MS) { - return 0; - } - - return (MESSAGE_COOLDOWN_MS - elapsed) / 1000; + long elapsed = now - lastTime; + return elapsed >= MESSAGE_COOLDOWN_MS ? 0 : (MESSAGE_COOLDOWN_MS - elapsed) / 1000; } } diff --git a/src/main/java/creator/loki/feature/metrics/MetricsCollector.java b/src/main/java/creator/loki/feature/metrics/MetricsCollector.java index fef7e72..6c2f14e 100644 --- a/src/main/java/creator/loki/feature/metrics/MetricsCollector.java +++ b/src/main/java/creator/loki/feature/metrics/MetricsCollector.java @@ -10,16 +10,10 @@ import creator.loki.util.LoggerDecorator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** Сборщик метрик для мониторинга бота */ public class MetricsCollector { private static final Logger logger = LoggerFactory.getLogger(MetricsCollector.class); - // Счётчики команд - private final ConcurrentHashMap commandCounts = new ConcurrentHashMap<>(); - private final ConcurrentHashMap commandErrors = new ConcurrentHashMap<>(); - private final ConcurrentHashMap commandTotalTime = new ConcurrentHashMap<>(); - - // Общие метрики + private final ConcurrentHashMap commands = new ConcurrentHashMap<>(); private final LongAdder totalCommands = new LongAdder(); private final LongAdder totalErrors = new LongAdder(); private final AtomicLong startTime = new AtomicLong(System.currentTimeMillis()); @@ -28,79 +22,60 @@ public class MetricsCollector { LoggerDecorator.bot(logger, "MetricsCollector инициализирован"); } - /** Записывает выполнение команды */ public void recordCommand(String commandName, long executionTimeMs) { - commandCounts.computeIfAbsent(commandName, k -> new LongAdder()).increment(); - commandTotalTime.computeIfAbsent(commandName, k -> new AtomicLong()).addAndGet(executionTimeMs); + var entry = commands.computeIfAbsent(commandName, k -> new CommandEntry()); + entry.count.increment(); + entry.totalTime.addAndGet(executionTimeMs); totalCommands.increment(); } - /** Записывает ошибку команды */ public void recordError(String commandName) { - commandErrors.computeIfAbsent(commandName, k -> new LongAdder()).increment(); + var entry = commands.computeIfAbsent(commandName, k -> new CommandEntry()); + entry.errors.increment(); totalErrors.increment(); } - /** Возвращает количество выполнений команды */ public long getCommandCount(String commandName) { - LongAdder counter = commandCounts.get(commandName); - return counter != null ? counter.sum() : 0; + var entry = commands.get(commandName); + return entry != null ? entry.count.sum() : 0; } - /** Возвращает количество ошибок команды */ public long getCommandErrors(String commandName) { - LongAdder counter = commandErrors.get(commandName); - return counter != null ? counter.sum() : 0; + var entry = commands.get(commandName); + return entry != null ? entry.errors.sum() : 0; } - /** Возвращает среднее время выполнения команды */ public double getAverageExecutionTime(String commandName) { - long count = getCommandCount(commandName); - if (count == 0) { - return 0.0; - } - AtomicLong totalTime = commandTotalTime.get(commandName); - if (totalTime == null) { - return 0.0; - } - return (double) totalTime.get() / count; + var entry = commands.get(commandName); + var count = entry != null ? entry.count.sum() : 0; + return count == 0 ? 0.0 : (double) entry.totalTime.get() / count; } - /** Возвращает общее количество выполненных команд */ public long getTotalCommands() { return totalCommands.sum(); } - /** Возвращает общее количество ошибок */ public long getTotalErrors() { return totalErrors.sum(); } - /** Возвращает процент ошибок */ public double getErrorRate() { - long total = getTotalCommands(); - if (total == 0) { - return 0.0; - } - return (double) getTotalErrors() / total * 100; + return getTotalCommands() == 0 ? 0.0 : (double) getTotalErrors() / getTotalCommands() * 100; } - /** Возвращает время работы бота в секундах */ public long getUptimeSeconds() { return (System.currentTimeMillis() - startTime.get()) / 1000; } - /** JSON для {@code GET /health} (пробы оркестраторов). */ public String buildHealthJson() { - String version = - Optional.ofNullable(MetricsCollector.class.getPackage().getImplementationVersion()) - .filter(v -> !v.isBlank()) - .orElse("dev"); return String.format( Locale.ROOT, "{\"status\":\"ok\",\"version\":%s,\"uptimeSeconds\":%d,\"commandsRun\":%d," + "\"errors\":%d,\"errorRatePercent\":%.4f}", - jsonQuoted(version), + jsonQuoted( + Optional.ofNullable(MetricsCollector.class.getPackage().getImplementationVersion()) + .filter(v -> !v.isBlank()) + .orElse("dev")), getUptimeSeconds(), getTotalCommands(), getTotalErrors(), @@ -108,75 +83,61 @@ public class MetricsCollector { } private static String jsonQuoted(String s) { - StringBuilder sb = new StringBuilder(s.length() + 2); - sb.append('"'); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - switch (c) { - case '\\' -> sb.append("\\\\"); - case '"' -> sb.append("\\\""); - case '\n' -> sb.append("\\n"); - case '\r' -> sb.append("\\r"); - case '\t' -> sb.append("\\t"); - default -> sb.append(c < ' ' ? ' ' : c); - } - } - sb.append('"'); - return sb.toString(); + return '"' + + s.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + + '"'; } - /** Возвращает топ команд по использованию */ public String getTopCommands(int limit) { - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); sb.append("📊 Топ команд:\n"); - - commandCounts.entrySet().stream() - .sorted((e1, e2) -> Long.compare(e2.getValue().sum(), e1.getValue().sum())) + commands.entrySet().stream() + .sorted((e1, e2) -> Long.compare(e2.getValue().count.sum(), e1.getValue().count.sum())) .limit(limit) .forEach( entry -> { - String cmd = entry.getKey(); - long count = entry.getValue().sum(); - long errors = getCommandErrors(cmd); - double avgTime = getAverageExecutionTime(cmd); - + var ce = entry.getValue(); + var count = ce.count.sum(); + var avgTime = count == 0 ? 0.0 : (double) ce.totalTime.get() / count; sb.append( String.format( " %s: %d выполнений, %.1f мс среднее, %d ошибок%n", - cmd, count, avgTime, errors)); + entry.getKey(), count, avgTime, ce.errors.sum())); }); - return sb.toString(); } - /** Возвращает полную статистику */ public String getFullStats() { - long uptime = getUptimeSeconds(); - long hours = uptime / 3600; - long minutes = (uptime % 3600) / 60; - long seconds = uptime % 60; - - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); sb.append("📈 Статистика бота\n"); sb.append("━━━━━━━━━━━━━━━━━━━━\n"); - sb.append(String.format("⏱️ Uptime: %dч %dм %dс%n", hours, minutes, seconds)); + sb.append( + String.format( + "⏱️ Uptime: %dч %dм %dс%n", + getUptimeSeconds() / 3600, (getUptimeSeconds() % 3600) / 60, getUptimeSeconds() % 60)); sb.append(String.format("📝 Всего команд: %d%n", getTotalCommands())); sb.append(String.format("❌ Всего ошибок: %d (%.2f%%)%n", getTotalErrors(), getErrorRate())); - sb.append(String.format("📊 Уникальных команд: %d%n", commandCounts.size())); + sb.append(String.format("📊 Уникальных команд: %d%n", commands.size())); sb.append("\n"); sb.append(getTopCommands(10)); - return sb.toString(); } - /** Сбрасывает все метрики */ public void reset() { - commandCounts.clear(); - commandErrors.clear(); - commandTotalTime.clear(); + commands.clear(); totalCommands.reset(); totalErrors.reset(); startTime.set(System.currentTimeMillis()); LoggerDecorator.bot(logger, "Метрики сброшены"); } + + private static class CommandEntry { + final LongAdder count = new LongAdder(); + final LongAdder errors = new LongAdder(); + final AtomicLong totalTime = new AtomicLong(); + } } diff --git a/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java b/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java index 4306de7..fc74270 100644 --- a/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java +++ b/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java @@ -6,12 +6,13 @@ import creator.loki.core.registry.CommandRegistry; import creator.loki.feature.config.repository.ConfigRepository; import creator.loki.feature.config.repository.GuildConfigRepository; import creator.loki.feature.logging.LogSetupPrefixCommand; +import creator.loki.feature.moderation.commands.caselookup.CaseLookupCommand; import creator.loki.feature.moderation.commands.punish.MuteCommand; import creator.loki.feature.moderation.commands.punish.PunishCommand; import creator.loki.feature.moderation.commands.punish.UnmuteCommand; +import creator.loki.feature.moderation.commands.swap.SwapCommand; import creator.loki.feature.moderation.commands.utils.ClearCommand; import creator.loki.feature.moderation.commands.utils.SlowmodeCommand; -import creator.loki.feature.moderation.commands.utils.SwapAndCaseCommand; import creator.loki.feature.moderation.commands.warn.UnwarnCommand; import creator.loki.feature.moderation.commands.warn.WarnCommand; import creator.loki.feature.moderation.commands.warn.WarningsHistoryCommand; @@ -84,8 +85,8 @@ public class ModerationCommandRegistrar implements CommandRegistrar { registry.register(new WarningsHistoryCommand(warningRepository)); registry.register(new ClearCommand()); registry.register(new SlowmodeCommand()); - registry.register(SwapAndCaseCommand.createSwap()); - registry.register(SwapAndCaseCommand.createCaseLookup(moderationCaseRepository)); + registry.register(new SwapCommand()); + registry.register(new CaseLookupCommand(moderationCaseRepository)); registry.register(new CreateRoleCommand(roleManager)); registry.register(new DeleteRoleCommand(roleManager)); diff --git a/src/main/java/creator/loki/feature/moderation/commands/caselookup/CaseLookupCommand.java b/src/main/java/creator/loki/feature/moderation/commands/caselookup/CaseLookupCommand.java new file mode 100644 index 0000000..97e8be3 --- /dev/null +++ b/src/main/java/creator/loki/feature/moderation/commands/caselookup/CaseLookupCommand.java @@ -0,0 +1,80 @@ +package creator.loki.feature.moderation.commands.caselookup; + +import java.awt.Color; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.feature.moderation.repository.ModerationCaseRepository; +import creator.loki.feature.moderation.util.ModerationHelper; +import net.dv8tion.jda.api.Permission; + +public class CaseLookupCommand implements Command { + private final ModerationCaseRepository cases; + + public CaseLookupCommand(ModerationCaseRepository cr) { + cases = cr; + } + + @Override + public String getName() { + return "case"; + } + + @Override + public String getDescription() { + return "Показать кейс модерации по ID"; + } + + @Override + public String getUsage() { + return "case "; + } + + @Override + public String getCategory() { + return "Модерация"; + } + + @Override + public void execute(CommandContext ctx) { + var g = ctx.getGuild(); + if (g == null) { + ModerationHelper.error(ctx, "Команда только на сервере"); + return; + } + var mod = ctx.getAuthor(); + if (mod == null || !mod.hasPermission(Permission.KICK_MEMBERS)) { + ModerationHelper.error(ctx, "Нужно **Kick Members** или выше"); + return; + } + if (ctx.getArgs().isEmpty()) { + ModerationHelper.error(ctx, "Использование: `" + getUsage() + "`"); + return; + } + long caseId; + try { + caseId = Long.parseLong(ctx.getArgs().get(0).trim()); + } catch (NumberFormatException e) { + ModerationHelper.error(ctx, "ID кейса должен быть числом"); + return; + } + cases + .findById(g.getId(), caseId) + .ifPresentOrElse( + c -> + ctx.getChannel() + .sendMessageEmbeds( + EmbedFactory.create() + .withTitle("Кейс #" + c.id()) + .withColor(Color.BLUE) + .addField("Действие", "`" + c.action() + "`", true) + .addField("Цель", "<@" + c.targetId() + ">", true) + .addField("Модератор", "<@" + c.moderatorId() + ">", true) + .addField("Причина", ModerationHelper.trunc(c.reason(), 1000), false) + .addField("Время", "", false) + .build()) + .queue(), + () -> ModerationHelper.error(ctx, "Кейса `" + caseId + "` на этом сервере нет")); + } +} diff --git a/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java b/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java index 392f4b0..b77e48e 100644 --- a/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java +++ b/src/main/java/creator/loki/feature/moderation/commands/punish/MuteCommand.java @@ -6,8 +6,8 @@ import java.time.OffsetDateTime; import creator.loki.command.Command; import creator.loki.command.CommandContext; import creator.loki.embed.EmbedFactory; -import creator.loki.embed.EmbedTemplates; import creator.loki.feature.config.repository.ConfigRepository; +import creator.loki.feature.moderation.util.ModerationHelper; import creator.loki.feature.moderation.MuteRoleService; import creator.loki.feature.moderation.MuteScheduler; import creator.loki.feature.moderation.repository.ActiveMuteRepository; @@ -190,10 +190,10 @@ public class MuteCommand implements Command { } private static String sub(String s, int max) { - return s.length() <= max ? s : s.substring(0, max - 1) + "…"; + return ModerationHelper.trunc(s, max); } private static void err(CommandContext ctx, String msg) { - ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue(); + ModerationHelper.error(ctx, msg); } } diff --git a/src/main/java/creator/loki/feature/moderation/commands/punish/PunishCommand.java b/src/main/java/creator/loki/feature/moderation/commands/punish/PunishCommand.java index 25483f4..f0eaed7 100644 --- a/src/main/java/creator/loki/feature/moderation/commands/punish/PunishCommand.java +++ b/src/main/java/creator/loki/feature/moderation/commands/punish/PunishCommand.java @@ -5,6 +5,7 @@ import java.util.concurrent.TimeUnit; import creator.loki.command.Command; import creator.loki.command.CommandContext; import creator.loki.embed.EmbedTemplates; +import creator.loki.feature.moderation.util.ModerationHelper; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; @@ -123,6 +124,6 @@ public final class PunishCommand { } private static void err(CommandContext ctx, String msg) { - ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue(); + ModerationHelper.error(ctx, msg); } } diff --git a/src/main/java/creator/loki/feature/moderation/commands/punish/UnmuteCommand.java b/src/main/java/creator/loki/feature/moderation/commands/punish/UnmuteCommand.java index aafcc93..9bec7af 100644 --- a/src/main/java/creator/loki/feature/moderation/commands/punish/UnmuteCommand.java +++ b/src/main/java/creator/loki/feature/moderation/commands/punish/UnmuteCommand.java @@ -13,6 +13,7 @@ import creator.loki.feature.moderation.MuteRoleService; import creator.loki.feature.moderation.MuteScheduler; import creator.loki.feature.moderation.repository.ActiveMuteRepository; import creator.loki.feature.moderation.repository.ModerationCaseRepository; +import creator.loki.feature.moderation.util.ModerationHelper; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.Permission; @@ -137,10 +138,10 @@ public class UnmuteCommand implements Command { } private static String trunc(String s, int max) { - return s.length() <= max ? s : s.substring(0, max - 1) + "…"; + return ModerationHelper.trunc(s, max); } private static void err(CommandContext ctx, String msg) { - ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue(); + ModerationHelper.error(ctx, msg); } } diff --git a/src/main/java/creator/loki/feature/moderation/commands/swap/SwapCommand.java b/src/main/java/creator/loki/feature/moderation/commands/swap/SwapCommand.java new file mode 100644 index 0000000..b6a8ddf --- /dev/null +++ b/src/main/java/creator/loki/feature/moderation/commands/swap/SwapCommand.java @@ -0,0 +1,97 @@ +package creator.loki.feature.moderation.commands.swap; + +import java.util.Random; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; +import creator.loki.feature.moderation.util.ModerationHelper; +import net.dv8tion.jda.api.Permission; + +public class SwapCommand implements Command { + private final Random random = new Random(); + + @Override + public String getName() { + return "swap"; + } + + @Override + public String getDescription() { + return "Перекидывает пользователя между случайными голосовыми каналами"; + } + + @Override + public String getUsage() { + return "swap <@user> <количество 1-5>"; + } + + @Override + public String getCategory() { + return "Модерация"; + } + + @Override + public void execute(CommandContext ctx) { + var g = ctx.getGuild(); + if (g == null) { + ModerationHelper.error(ctx, "Команда доступна только на сервере"); + return; + } + var mod = ctx.getAuthor(); + if (mod == null || !mod.hasPermission(Permission.VOICE_MOVE_OTHERS)) { + ModerationHelper.error(ctx, "Нет прав на перемещение"); + return; + } + if (!g.getMember(g.getJDA().getSelfUser()).hasPermission(Permission.VOICE_MOVE_OTHERS)) { + ModerationHelper.error(ctx, "У бота нет прав на перемещение"); + return; + } + var mentioned = ctx.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty() || ctx.getArgs().size() < 2) { + ModerationHelper.error(ctx, "Используй: `!swap <@user> <количество 1-5>`"); + return; + } + var t = mentioned.get(0); + int swapCount; + try { + swapCount = Integer.parseInt(ctx.getArgs().get(1)); + if (swapCount < 1 || swapCount > 5) throw new NumberFormatException(); + } catch (NumberFormatException e) { + ModerationHelper.error(ctx, "Количество должно быть от 1 до 5"); + return; + } + var voiceState = t.getVoiceState(); + if (voiceState == null || !voiceState.inAudioChannel()) { + ModerationHelper.error(ctx, t.getUser().getName() + " не в голосовом канале"); + return; + } + var original = voiceState.getChannel().asVoiceChannel(); + var channels = + g.getVoiceChannels().stream().filter(vc -> !vc.getId().equals(original.getId())).toList(); + if (channels.isEmpty()) { + ModerationHelper.error(ctx, "Недостаточно голосовых каналов для swap"); + return; + } + ctx.getChannel() + .sendMessageEmbeds( + EmbedTemplates.success( + "🔄 Swap запущен!", + "Перекидываю " + t.getAsMention() + " между каналами " + swapCount + " раз(а)")) + .queue(); + Thread.startVirtualThread( + () -> { + try { + for (int i = 0; i < swapCount; i++) { + var rnd = channels.get(random.nextInt(channels.size())); + g.moveVoiceMember(t, rnd).queue(); + Thread.sleep(800); + g.moveVoiceMember(t, original).queue(); + Thread.sleep(800); + } + } catch (Exception e) { + ModerationHelper.error(ctx, "Не удалось выполнить swap: " + e.getMessage()); + } + }); + } +} diff --git a/src/main/java/creator/loki/feature/moderation/commands/utils/SwapAndCaseCommand.java b/src/main/java/creator/loki/feature/moderation/commands/utils/SwapAndCaseCommand.java deleted file mode 100644 index 649cdcd..0000000 --- a/src/main/java/creator/loki/feature/moderation/commands/utils/SwapAndCaseCommand.java +++ /dev/null @@ -1,189 +0,0 @@ -package creator.loki.feature.moderation.commands.utils; - -import java.awt.Color; -import java.util.Random; - -import creator.loki.command.Command; -import creator.loki.command.CommandContext; -import creator.loki.embed.EmbedFactory; -import creator.loki.embed.EmbedTemplates; -import creator.loki.feature.moderation.repository.ModerationCaseRepository; -import net.dv8tion.jda.api.Permission; - -public final class SwapAndCaseCommand { - private SwapAndCaseCommand() {} - - public static Command createSwap() { - return new Swap(); - } - - public static Command createCaseLookup(ModerationCaseRepository cr) { - return new Case(cr); - } - - private static class Swap implements Command { - private final Random random = new Random(); - - @Override - public String getName() { - return "swap"; - } - - @Override - public String getDescription() { - return "Перекидывает пользователя между случайными голосовыми каналами"; - } - - @Override - public String getUsage() { - return "swap <@user> <количество 1-5>"; - } - - @Override - public String getCategory() { - return "Модерация"; - } - - @Override - public void execute(CommandContext ctx) { - var g = ctx.getGuild(); - if (g == null) { - err(ctx, "Команда доступна только на сервере"); - return; - } - var mod = ctx.getAuthor(); - if (mod == null || !mod.hasPermission(Permission.VOICE_MOVE_OTHERS)) { - err(ctx, "Нет прав на перемещение"); - return; - } - if (!g.getMember(g.getJDA().getSelfUser()).hasPermission(Permission.VOICE_MOVE_OTHERS)) { - err(ctx, "У бота нет прав на перемещение"); - return; - } - var mentioned = ctx.getMessage().getMentions().getMembers(); - if (mentioned.isEmpty() || ctx.getArgs().size() < 2) { - err(ctx, "Используй: `!swap <@user> <количество 1-5>`"); - return; - } - var t = mentioned.get(0); - int swapCount; - try { - swapCount = Integer.parseInt(ctx.getArgs().get(1)); - if (swapCount < 1 || swapCount > 5) throw new NumberFormatException(); - } catch (NumberFormatException e) { - err(ctx, "Количество должно быть от 1 до 5"); - return; - } - var voiceState = t.getVoiceState(); - if (voiceState == null || !voiceState.inAudioChannel()) { - err(ctx, t.getUser().getName() + " не в голосовом канале"); - return; - } - var original = voiceState.getChannel().asVoiceChannel(); - var channels = - g.getVoiceChannels().stream().filter(vc -> !vc.getId().equals(original.getId())).toList(); - if (channels.isEmpty()) { - err(ctx, "Недостаточно голосовых каналов для swap"); - return; - } - ctx.getChannel() - .sendMessageEmbeds( - EmbedTemplates.success( - "🔄 Swap запущен!", - "Перекидываю " + t.getAsMention() + " между каналами " + swapCount + " раз(а)")) - .queue(); - Thread.startVirtualThread( - () -> { - try { - for (int i = 0; i < swapCount; i++) { - var rnd = channels.get(random.nextInt(channels.size())); - g.moveVoiceMember(t, rnd).queue(); - Thread.sleep(800); - g.moveVoiceMember(t, original).queue(); - Thread.sleep(800); - } - } catch (Exception e) { - err(ctx, "Не удалось выполнить swap: " + e.getMessage()); - } - }); - } - } - - private static class Case implements Command { - private final ModerationCaseRepository cases; - - Case(ModerationCaseRepository cr) { - cases = cr; - } - - @Override - public String getName() { - return "case"; - } - - @Override - public String getDescription() { - return "Показать кейс модерации по ID"; - } - - @Override - public String getUsage() { - return "case "; - } - - @Override - public String getCategory() { - return "Модерация"; - } - - @Override - public void execute(CommandContext ctx) { - var g = ctx.getGuild(); - if (g == null) { - err(ctx, "Команда только на сервере"); - return; - } - var mod = ctx.getAuthor(); - if (mod == null || !mod.hasPermission(Permission.KICK_MEMBERS)) { - err(ctx, "Нужно **Kick Members** или выше"); - return; - } - if (ctx.getArgs().isEmpty()) { - err(ctx, "Использование: `" + getUsage() + "`"); - return; - } - long caseId; - try { - caseId = Long.parseLong(ctx.getArgs().get(0).trim()); - } catch (NumberFormatException e) { - err(ctx, "ID кейса должен быть числом"); - return; - } - cases - .findById(g.getId(), caseId) - .ifPresentOrElse( - c -> - ctx.getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("Кейс #" + c.id()) - .withColor(Color.BLUE) - .addField("Действие", "`" + c.action() + "`", true) - .addField("Цель", "<@" + c.targetId() + ">", true) - .addField("Модератор", "<@" + c.moderatorId() + ">", true) - .addField("Причина", trunc(c.reason(), 1000), false) - .addField("Время", "", false) - .build()) - .queue(), - () -> err(ctx, "Кейса `" + caseId + "` на этом сервере нет")); - } - } - - private static String trunc(String s, int max) { - return s.length() <= max ? s : s.substring(0, max - 1) + "…"; - } - - private static void err(CommandContext ctx, String msg) { - ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue(); - } -} diff --git a/src/main/java/creator/loki/feature/moderation/commands/warn/WarnCommand.java b/src/main/java/creator/loki/feature/moderation/commands/warn/WarnCommand.java index 436b076..56f6820 100644 --- a/src/main/java/creator/loki/feature/moderation/commands/warn/WarnCommand.java +++ b/src/main/java/creator/loki/feature/moderation/commands/warn/WarnCommand.java @@ -13,6 +13,7 @@ import creator.loki.feature.moderation.MuteScheduler; import creator.loki.feature.moderation.repository.ActiveMuteRepository; import creator.loki.feature.moderation.repository.ModerationCaseRepository; import creator.loki.feature.moderation.repository.WarningRepository; +import creator.loki.feature.moderation.util.ModerationHelper; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Guild; @@ -66,38 +67,46 @@ public class WarnCommand implements Command { @Override public void execute(CommandContext ctx) { + var t = validateInput(ctx); + if (t != null) saveWarning(ctx, t); + } + + private Member validateInput(CommandContext ctx) { var g = ctx.getGuild(); if (g == null) { - err(ctx, "Только на сервере"); - return; + ModerationHelper.error(ctx, "Только на сервере"); + return null; } var mod = ctx.getAuthor(); if (mod == null || !mod.hasPermission(Permission.KICK_MEMBERS)) { - err(ctx, "Нужно **Kick Members**"); - return; + ModerationHelper.error(ctx, "Нужно **Kick Members**"); + return null; } var mentioned = ctx.getMessage().getMentions().getMembers(); if (mentioned.isEmpty()) { - err(ctx, "Укажи пользователя: `" + getUsage() + "`"); - return; + ModerationHelper.error(ctx, "Укажи пользователя: `" + getUsage() + "`"); + return null; } var t = mentioned.get(0); if (t.getId().equals(mod.getId())) { - err(ctx, "Нельзя выдать варн самому себе"); - return; + ModerationHelper.error(ctx, "Нельзя выдать варн самому себе"); + return null; } if (!mod.canInteract(t)) { - err(ctx, "Участник выше ваших ролей"); - return; + ModerationHelper.error(ctx, "Участник выше ваших ролей"); + return null; } - var reason = - ctx.getArgs().size() > 1 - ? ctx.getArgs().stream().skip(1).collect(Collectors.joining(" ")).trim() - : "Без причины"; + return t; + } + + private void saveWarning(CommandContext ctx, Member t) { + var g = ctx.getGuild(); + var mod = ctx.getAuthor(); + var reason = ctx.getArgs().stream().skip(1).collect(Collectors.joining(" ")).trim(); if (reason.isEmpty()) reason = "Без причины"; long id = warnings.insert(g.getId(), t.getId(), mod.getId(), reason); if (id < 0) { - err(ctx, "Не удалось сохранить предупреждение"); + ModerationHelper.error(ctx, "Не удалось сохранить предупреждение"); return; } int total = warnings.countByUser(g.getId(), t.getId()); @@ -110,7 +119,7 @@ public class WarnCommand implements Command { + "\n**ID записи:** `" + id + "`\n**Причина:** " - + trunc(reason, 500) + + ModerationHelper.trunc(reason, 500) + "\n**Всего варнов:** " + total)) .queue(); @@ -133,7 +142,7 @@ public class WarnCommand implements Command { .addField("Модератор", mod.getAsMention(), false) .addField("ID записи", "`" + warnId + "`", true) .addField("Всего варнов", String.valueOf(total), true) - .addField("Причина", trunc(reason, 1000), false) + .addField("Причина", ModerationHelper.trunc(reason, 1000), false) .setTimestamp(OffsetDateTime.now()) .build()) .queue(s -> {}, e -> {}); @@ -148,7 +157,7 @@ public class WarnCommand implements Command { } var self = g.getMember(g.getJDA().getSelfUser()); if (!self.canInteract(t) || !self.canInteract(muteRole)) return; - var reason = "Авто-мут: 3+ варна"; + String reason = "Авто-мут: 3+ варна"; long untilMs = System.currentTimeMillis() + AUTO_MUTE_HOURS * 3600 * 1000; long durSec = AUTO_MUTE_HOURS * 3600; long caseId = cases.insert(g.getId(), "MUTE", t.getId(), mod.getId(), reason, durSec, untilMs); @@ -173,12 +182,4 @@ public class WarnCommand implements Command { }, err -> activeMutes.delete(g.getId(), t.getId())); } - - private static String trunc(String s, int max) { - return s.length() <= max ? s : s.substring(0, max - 1) + "…"; - } - - private static void err(CommandContext ctx, String msg) { - ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue(); - } } diff --git a/src/main/java/creator/loki/feature/moderation/log/ModerationLogger.java b/src/main/java/creator/loki/feature/moderation/log/ModerationLogger.java index 3f17bee..14b4d6d 100644 --- a/src/main/java/creator/loki/feature/moderation/log/ModerationLogger.java +++ b/src/main/java/creator/loki/feature/moderation/log/ModerationLogger.java @@ -12,7 +12,6 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.audit.ActionType; import net.dv8tion.jda.api.audit.AuditLogEntry; import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.events.guild.GuildBanEvent; import net.dv8tion.jda.api.events.guild.GuildUnbanEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; @@ -35,165 +34,118 @@ public class ModerationLogger extends ListenerAdapter { } private void handle( - Guild guild, ActionType type, int limit, Function, EmbedBuilder> fn) { - var log = getLogChannel(guild); + Guild g, ActionType t, int l, Function, EmbedBuilder> fn) { + var log = g.getTextChannelById(configRepo.get("moderation.log_channel_id")); if (log == null) return; - guild - .retrieveAuditLogs() - .type(type) - .limit(limit) - .queue( - logs -> { - var embed = fn.apply(logs); - if (embed != null) { - log.sendMessageEmbeds(embed.setTimestamp(OffsetDateTime.now()).build()).queue(); - } - }); - } - - public void onMessageDelete(MessageDeleteEvent event) { - var content = messageCache.getContent(event.getMessageId()); - handle( - event.getGuild(), - ActionType.MESSAGE_DELETE, - 1, + var al = g.retrieveAuditLogs().type(t).limit(l); + al.queue( logs -> { - var entry = logs.isEmpty() ? null : logs.get(0); - return LogEmbeds.messageDelete(entry, event.getChannel().getAsMention(), content); + var embed = fn.apply(logs); + if (embed == null) return; + log.sendMessageEmbeds(embed.setTimestamp(OffsetDateTime.now()).build()).queue(); }); } - public void onMessageBulkDelete(MessageBulkDeleteEvent event) { - handle( - event.getGuild(), - ActionType.MESSAGE_BULK_DELETE, - 1, - logs -> { - var entry = logs.isEmpty() ? null : logs.get(0); - return LogEmbeds.messageBulkDelete( - entry, event.getChannel().getAsMention(), event.getMessageIds().size()); - }); + public void onMessageDelete(MessageDeleteEvent e) { + var content = messageCache.getContent(e.getMessageId()); + handle(e.getGuild(), ActionType.MESSAGE_DELETE, 1, l -> md(e, content, l)); } - @Override - public void onGuildBan(GuildBanEvent event) { - handle( - event.getGuild(), - ActionType.BAN, - 1, - logs -> { - if (logs.isEmpty()) return null; - var entry = logs.get(0); - var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно"; - var reason = entry.getReason() != null ? entry.getReason() : "Не указана"; - return LogEmbeds.ban( - event.getUser().getAsMention(), mod, reason, event.getUser().getAvatarUrl()); - }); + private EmbedBuilder md(MessageDeleteEvent e, String c, List l) { + var entry = l.isEmpty() ? null : l.get(0); + return LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), c); } - @Override - public void onGuildUnban(GuildUnbanEvent event) { - handle( - event.getGuild(), - ActionType.UNBAN, - 1, - logs -> { - if (logs.isEmpty()) return null; - var entry = logs.get(0); - var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно"; - return LogEmbeds.unban( - event.getUser().getAsMention(), mod, event.getUser().getAvatarUrl()); - }); + public void onMessageBulkDelete(MessageBulkDeleteEvent e) { + handle(e.getGuild(), ActionType.MESSAGE_BULK_DELETE, 1, l -> mbd(e, l)); } - @Override - public void onGuildMemberRemove(GuildMemberRemoveEvent event) { - handle( - event.getGuild(), - ActionType.KICK, - 1, - logs -> { - if (logs.isEmpty()) return null; - var entry = logs.get(0); - if (!entry.getTargetId().equals(event.getUser().getId())) return null; - var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно"; - var reason = entry.getReason() != null ? entry.getReason() : "Не указана"; - return LogEmbeds.kick( - event.getUser().getAsMention(), mod, reason, event.getUser().getAvatarUrl()); - }); + private EmbedBuilder mbd(MessageBulkDeleteEvent e, List l) { + var entry = l.isEmpty() ? null : l.get(0); + var size = e.getMessageIds().size(); + return LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size); } - @Override - public void onGuildMemberUpdateTimeOut(GuildMemberUpdateTimeOutEvent event) { - var muted = event.getNewTimeOutEnd() != null; - handle( - event.getGuild(), - ActionType.MEMBER_UPDATE, - 5, - logs -> { - var entry = - logs.stream() - .filter(e -> event.getUser().getId().equals(e.getTargetId())) - .findFirst() - .orElse(null); - if (entry == null) return null; - var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно"; - var end = muted ? event.getNewTimeOutEnd().toEpochSecond() : null; - return LogEmbeds.timeout( - event.getUser().getAsMention(), mod, muted, end, entry.getReason()); - }); + public void onGuildBan(GuildBanEvent ev) { + handle(ev.getGuild(), ActionType.BAN, 1, l -> b(ev, l)); } - @Override - public void onGuildVoiceGuildMute(GuildVoiceGuildMuteEvent event) { - var muted = event.isGuildMuted(); - handle( - event.getGuild(), - ActionType.MEMBER_UPDATE, - 5, - logs -> { - var entry = - logs.stream() - .filter(e -> event.getMember().getId().equals(e.getTargetId())) - .filter(e -> e.getChangeByKey("mute") != null) - .findFirst() - .orElse(null); - var mod = - entry != null && entry.getUser() != null ? entry.getUser().getAsMention() : null; - return LogEmbeds.voiceMute( - event.getMember().getAsMention(), - event.getVoiceState().getChannel().getAsMention(), - muted, - mod); - }); + private EmbedBuilder b(GuildBanEvent ev, List l) { + if (l.isEmpty()) return null; + var user = ev.getUser(); + var e = l.get(0); + var mod = e.getUser() != null ? e.getUser().getAsMention() : "Неизвестно"; + var reason = e.getReason() != null ? e.getReason() : "Не указана"; + return LogEmbeds.ban(user.getAsMention(), mod, reason, user.getAvatarUrl()); } - @Override - public void onGuildVoiceGuildDeafen(GuildVoiceGuildDeafenEvent event) { - var deaf = event.isGuildDeafened(); - handle( - event.getGuild(), - ActionType.MEMBER_UPDATE, - 5, - logs -> { - var entry = - logs.stream() - .filter(e -> event.getMember().getId().equals(e.getTargetId())) - .filter(e -> e.getChangeByKey("deaf") != null) - .findFirst() - .orElse(null); - var mod = - entry != null && entry.getUser() != null ? entry.getUser().getAsMention() : null; - return LogEmbeds.voiceDeafen( - event.getMember().getAsMention(), - event.getVoiceState().getChannel().getAsMention(), - deaf, - mod); - }); + public void onGuildUnban(GuildUnbanEvent ev) { + handle(ev.getGuild(), ActionType.UNBAN, 1, l -> ub(ev, l)); } - private TextChannel getLogChannel(Guild guild) { - var logChannelId = configRepo.get("moderation.log_channel_id"); - return logChannelId != null ? guild.getTextChannelById(logChannelId) : null; + private EmbedBuilder ub(GuildUnbanEvent ev, List l) { + if (l.isEmpty()) return null; + var user = ev.getUser(); + var e = l.get(0); + var mod = e.getUser() != null ? e.getUser().getAsMention() : "Неизвестно"; + return LogEmbeds.unban(user.getAsMention(), mod, user.getAvatarUrl()); + } + + public void onGuildMemberRemove(GuildMemberRemoveEvent ev) { + handle(ev.getGuild(), ActionType.KICK, 1, l -> k(ev, l)); + } + + private EmbedBuilder k(GuildMemberRemoveEvent ev, List l) { + if (l.isEmpty()) return null; + var e = l.get(0); + if (!e.getTargetId().equals(ev.getUser().getId())) return null; + var mod = e.getUser() != null ? e.getUser().getAsMention() : "Неизвестно"; + var reason = e.getReason() != null ? e.getReason() : "Не указана"; + return LogEmbeds.kick(ev.getUser().getAsMention(), mod, reason, ev.getUser().getAvatarUrl()); + } + + public void onGuildMemberUpdateTimeOut(GuildMemberUpdateTimeOutEvent e) { + var muted = e.getNewTimeOutEnd() != null; + handle(e.getGuild(), ActionType.MEMBER_UPDATE, 5, l -> t(e, muted, l)); + } + + private EmbedBuilder t(GuildMemberUpdateTimeOutEvent e, boolean muted, List l) { + var entry = findEntry(l, e.getUser().getId()); + if (entry == null) return null; + var mod = entry.getUser() != null ? entry.getUser().getAsMention() : "Неизвестно"; + var end = muted ? e.getNewTimeOutEnd().toEpochSecond() : null; + return LogEmbeds.timeout(e.getUser().getAsMention(), mod, muted, end, entry.getReason()); + } + + public void onGuildVoiceGuildMute(GuildVoiceGuildMuteEvent e) { + var muted = e.isGuildMuted(); + handle(e.getGuild(), ActionType.MEMBER_UPDATE, 5, l -> vm(e, muted, l)); + } + + private EmbedBuilder vm(GuildVoiceGuildMuteEvent e, boolean muted, List l) { + var member = e.getMember(); + var ch = e.getVoiceState().getChannel(); + var entry = findEntry(l, member.getId(), "mute"); + var mod = entry != null && entry.getUser() != null ? entry.getUser().getAsMention() : null; + return LogEmbeds.voiceMute(member.getAsMention(), ch.getAsMention(), muted, mod); + } + + public void onGuildVoiceGuildDeafen(GuildVoiceGuildDeafenEvent e) { + var deaf = e.isGuildDeafened(); + handle(e.getGuild(), ActionType.MEMBER_UPDATE, 5, l -> vd(e, deaf, l)); + } + + private EmbedBuilder vd(GuildVoiceGuildDeafenEvent e, boolean deaf, List l) { + var member = e.getMember(); + var ch = e.getVoiceState().getChannel(); + var entry = findEntry(l, member.getId(), "deaf"); + var mod = entry != null && entry.getUser() != null ? entry.getUser().getAsMention() : null; + return LogEmbeds.voiceDeafen(member.getAsMention(), ch.getAsMention(), deaf, mod); + } + + private AuditLogEntry findEntry(List l, String targetId, String... key) { + var s = l.stream().filter(e -> targetId.equals(e.getTargetId())); + if (key.length > 0) s = s.filter(e -> e.getChangeByKey(key[0]) != null); + return s.findFirst().orElse(null); } } diff --git a/src/main/java/creator/loki/feature/moderation/util/ModerationHelper.java b/src/main/java/creator/loki/feature/moderation/util/ModerationHelper.java new file mode 100644 index 0000000..5cc17a3 --- /dev/null +++ b/src/main/java/creator/loki/feature/moderation/util/ModerationHelper.java @@ -0,0 +1,16 @@ +package creator.loki.feature.moderation.util; + +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; + +public final class ModerationHelper { + private ModerationHelper() {} + + public static String trunc(String s, int max) { + return s.length() <= max ? s : s.substring(0, max - 1) + "…"; + } + + public static void error(CommandContext ctx, String msg) { + ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue(); + } +} diff --git a/src/main/java/creator/loki/feature/music/commands/FilterCommands.java b/src/main/java/creator/loki/feature/music/commands/FilterCommands.java index 88d1ee0..4d035a0 100644 --- a/src/main/java/creator/loki/feature/music/commands/FilterCommands.java +++ b/src/main/java/creator/loki/feature/music/commands/FilterCommands.java @@ -161,7 +161,10 @@ public class FilterCommands { public void execute(CommandContext ctx) { var m = mgr(pm, ctx); if (!checkChannel(m, ctx)) return; - boolean enabled = isNight ? !m.getScheduler().isNightcore() : !m.getScheduler().isVaporwave(); + boolean enabled = + isNight + ? !m.getScheduler().filterState.isNightcore() + : !m.getScheduler().filterState.isVaporwave(); if (isNight) m.getScheduler().setNightcore(enabled); else m.getScheduler().setVaporwave(enabled); ctx.getChannel() diff --git a/src/main/java/creator/loki/feature/music/commands/QueueCommands.java b/src/main/java/creator/loki/feature/music/commands/QueueCommands.java index a8b4d7f..0e08eb6 100644 --- a/src/main/java/creator/loki/feature/music/commands/QueueCommands.java +++ b/src/main/java/creator/loki/feature/music/commands/QueueCommands.java @@ -1,207 +1,149 @@ package creator.loki.feature.music.commands; +import static creator.loki.embed.EmbedTemplates.error; +import static creator.loki.embed.EmbedTemplates.info; + import java.awt.Color; -import java.util.List; +import java.util.function.BiConsumer; import creator.loki.command.Command; import creator.loki.command.CommandContext; import creator.loki.embed.EmbedFactory; -import creator.loki.embed.EmbedTemplates; import creator.loki.feature.music.core.PlayerManager; -import dev.arbjerg.lavalink.client.player.Track; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class QueueCommands { + private static final Color C = new Color(88, 101, 242); + private static final Logger LOG = LoggerFactory.getLogger(QueueCommands.class); + private QueueCommands() {} public static Command queue(PlayerManager pm) { - return new Queue(pm); + return new Mc(pm, "queue", "Показать очередь треков", null, null, QueueCommands::executeQueue); } public static Command nowplaying(PlayerManager pm) { - return new NowPlaying(pm); + BiConsumer exec = QueueCommands::executeNowPlaying; + return new Mc(pm, "nowplaying", "Показать текущий трек", "nowplaying", "Музыка", exec); } public static Command volume(PlayerManager pm) { - return new Volume(pm); + BiConsumer exec = QueueCommands::executeVolume; + return new Mc(pm, "volume", "Регулировка громкости (0-100)", "volume [0-100]", "Музыка", exec); } - private static final class Queue implements Command { - private final PlayerManager pm; + private static final class Mc implements Command { + private final PlayerManager p; + private final String n, d, u, c; + private final BiConsumer e; - Queue(PlayerManager pm) { - this.pm = pm; + Mc( + PlayerManager p, + String n, + String d, + String u, + String c, + BiConsumer e) { + this.p = p; + this.n = n; + this.d = d; + this.u = u; + this.c = c; + this.e = e; } - @Override public String getName() { - return "queue"; + return n; } - @Override public String getDescription() { - return "Показать очередь треков"; + return d; } - @Override - public void execute(CommandContext ctx) { - var m = pm.getMusicManager(ctx.getGuild().getIdLong()); - Track cur = m.getScheduler().getCurrentTrack(); - List q = m.getScheduler().getQueue(); - if (cur == null && q.isEmpty()) { - ctx.getChannel().sendMessage("Очередь пуста.").queue(); - return; - } - var embed = - EmbedFactory.create().withTitle("Очередь треков").withColor(new Color(88, 101, 242)); - if (cur != null) { - embed.addField( - "Сейчас играет", - "**" + cur.getInfo().getTitle() + "**\n" + cur.getInfo().getAuthor(), - false); - } - if (!q.isEmpty()) { - var sb = new StringBuilder(); - int show = Math.min(q.size(), 10); - for (int i = 0; i < show; i++) { - sb.append("`") - .append(i + 1) - .append(".` **") - .append(q.get(i).getInfo().getTitle()) - .append("**\n"); - } - if (q.size() > show) { - sb.append("... и ещё **").append(q.size() - show).append("** треков"); - } - embed.addField("В очереди (" + q.size() + ")", sb.toString(), false); - } - ctx.getChannel().sendMessageEmbeds(embed.build()).queue(); - } - } - - private static final class NowPlaying implements Command { - private final PlayerManager pm; - - NowPlaying(PlayerManager pm) { - this.pm = pm; - } - - @Override - public String getName() { - return "nowplaying"; - } - - @Override - public String getDescription() { - return "Показать текущий трек"; - } - - @Override public String getUsage() { - return "nowplaying"; + return u != null ? u : n; } - @Override public String getCategory() { - return "Музыка"; + return c != null ? c : "Общие"; } - @Override public void execute(CommandContext ctx) { - var lm = pm.getLavalinkManager(); - var track = lm.getMusicManager(ctx.getGuild().getIdLong()).getScheduler().getCurrentTrack(); - if (track == null) { - ctx.getChannel().sendMessage("Сейчас ничего не играет.").queue(); - return; - } - var link = lm.getClient().getLinkIfCached(ctx.getGuild().getIdLong()); - long pos = - link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getPosition() : 0; - long dur = track.getInfo().getLength(); - long ps = pos / 1000; - long ds = dur / 1000; - if (ds <= 0) ds = 1; - int filled = (int) ((double) pos / dur * 20); - StringBuilder bar = new StringBuilder("\u2503"); - for (int i = 0; i < 20; i++) bar.append(i < filled ? "\u2588" : "\u2591"); - String pbar = - bar - + "\u2503 " - + String.format("%d:%02d", ps / 60, ps % 60) - + " / " - + String.format("%d:%02d", ds / 60, ds % 60); + e.accept(p, ctx); + } + } + + private static void executeQueue(PlayerManager pm, CommandContext ctx) { + var m = pm.getMusicManager(ctx.getGuild().getIdLong()); + var cur = m.getScheduler().getCurrentTrack(); + var q = m.getScheduler().getQueue(); + if (cur == null && q.isEmpty()) { + ctx.getChannel().sendMessage("Очередь пуста.").queue(); + return; + } + var embed = EmbedFactory.create().withTitle("Очередь треков").withColor(C); + if (cur != null) { + var i = cur.getInfo(); + embed.addField("Сейчас играет", "**" + i.getTitle() + "**\n" + i.getAuthor(), false); + } + if (!q.isEmpty()) { + var sb = new StringBuilder(); + var show = Math.min(q.size(), 10); + for (var i = 0; i < show; i++) + sb.append(String.format("`%d.` **%s**\n", i + 1, q.get(i).getInfo().getTitle())); + if (q.size() > show) sb.append("... и ещё **").append(q.size() - show).append("** треков"); + embed.addField("В очереди (" + q.size() + ")", sb.toString(), false); + } + ctx.getChannel().sendMessageEmbeds(embed.build()).queue(); + } + + private static void executeNowPlaying(PlayerManager pm, CommandContext ctx) { + var lm = pm.getLavalinkManager(); + var track = lm.getMusicManager(ctx.getGuild().getIdLong()).getScheduler().getCurrentTrack(); + if (track == null) { + ctx.getChannel().sendMessage("Сейчас ничего не играет.").queue(); + return; + } + var l = lm.getClient().getLinkIfCached(ctx.getGuild().getIdLong()); + var pos = l != null && l.getCachedPlayer() != null ? l.getCachedPlayer().getPosition() : 0L; + var dur = track.getInfo().getLength(); + var ds = dur / 1000 <= 0 ? 1 : dur / 1000; + var filled = (int) ((double) pos / dur * 20); + var bar = new StringBuilder("\u2503"); + for (var i = 0; i < 20; i++) bar.append(i < filled ? "\u2588" : "\u2591"); + var t0 = String.format("%d:%02d", pos / 1000 / 60, pos / 1000 % 60); + var t1 = String.format("%d:%02d", ds / 60, ds % 60); + var pbar = bar + "\u2503 " + t0 + " / " + t1; + var msg = + EmbedFactory.create() + .withTitle("Сейчас играет") + .withColor(C) + .addField("Название", "**" + track.getInfo().getTitle() + "**", false) + .addField("Автор", track.getInfo().getAuthor(), true) + .addField("Длительность", pbar, true) + .build(); + ctx.getChannel().sendMessageEmbeds(msg).queue(); + } + + private static void executeVolume(PlayerManager pm, CommandContext ctx) { + var l = pm.getLavalinkManager().getClient().getLinkIfCached(ctx.getGuild().getIdLong()); + if (ctx.getArgs().isEmpty()) { + var v = l != null && l.getCachedPlayer() != null ? l.getCachedPlayer().getVolume() : 100; + ctx.getChannel().sendMessageEmbeds(info("Громкость", "Текущая: **" + v + "%**")).queue(); + return; + } + try { + var vol = Math.max(0, Math.min(100, Integer.parseInt(ctx.getArgs().get(0)))); + if (l != null && l.getCachedPlayer() != null) + l.createOrUpdatePlayer() + .setVolume(vol) + .subscribe(null, err -> LOG.warn("Lavalink: {}", err.getMessage())); ctx.getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("Сейчас играет") - .withColor(new Color(88, 101, 242)) - .addField("Название", "**" + track.getInfo().getTitle() + "**", false) - .addField("Автор", track.getInfo().getAuthor(), true) - .addField("Длительность", pbar, true) - .build()) + .sendMessageEmbeds(info("Громкость", "Установлена на **" + vol + "%**")) .queue(); - } - } - - private static final class Volume implements Command { - private final PlayerManager pm; - - Volume(PlayerManager pm) { - this.pm = pm; - } - - @Override - public String getName() { - return "volume"; - } - - @Override - public String getDescription() { - return "Регулировка громкости (0-100)"; - } - - @Override - public String getUsage() { - return "volume [0-100]"; - } - - @Override - public String getCategory() { - return "Музыка"; - } - - @Override - public void execute(CommandContext ctx) { - var lm = pm.getLavalinkManager(); - var link = lm.getClient().getLinkIfCached(ctx.getGuild().getIdLong()); - if (ctx.getArgs().isEmpty()) { - int v = - link != null && link.getCachedPlayer() != null - ? link.getCachedPlayer().getVolume() - : 100; - ctx.getChannel() - .sendMessageEmbeds(EmbedTemplates.info("Громкость", "Текущая: **" + v + "%**")) - .queue(); - return; - } - try { - var vol = Math.max(0, Math.min(100, Integer.parseInt(ctx.getArgs().get(0)))); - if (link != null && link.getCachedPlayer() != null) { - link.createOrUpdatePlayer() - .setVolume(vol) - .subscribe( - null, - err -> - LoggerFactory.getLogger(Volume.class).warn("Lavalink: {}", err.getMessage())); - } - ctx.getChannel() - .sendMessageEmbeds(EmbedTemplates.info("Громкость", "Установлена на **" + vol + "%**")) - .queue(); - } catch (NumberFormatException e) { - ctx.getChannel() - .sendMessageEmbeds(EmbedTemplates.error("Аргументы", "Нужно число от 0 до 100.")) - .queue(); - } + } catch (NumberFormatException e) { + ctx.getChannel().sendMessageEmbeds(error("Аргументы", "Нужно число от 0 до 100.")).queue(); } } } diff --git a/src/main/java/creator/loki/feature/music/core/LavalinkManager.java b/src/main/java/creator/loki/feature/music/core/LavalinkManager.java index 0f07013..85dfa7c 100644 --- a/src/main/java/creator/loki/feature/music/core/LavalinkManager.java +++ b/src/main/java/creator/loki/feature/music/core/LavalinkManager.java @@ -92,7 +92,7 @@ public class LavalinkManager { event -> { GuildMusicManager manager = musicManagers.get(event.getGuildId()); if (manager != null) { - manager.getScheduler().onTrackStart(event.getTrack()); + manager.cancelAutoLeaveTimer(); } }); diff --git a/src/main/java/creator/loki/feature/music/listener/MusicBotControlHandler.java b/src/main/java/creator/loki/feature/music/listener/MusicBotControlHandler.java index d7ae493..aa037f7 100644 --- a/src/main/java/creator/loki/feature/music/listener/MusicBotControlHandler.java +++ b/src/main/java/creator/loki/feature/music/listener/MusicBotControlHandler.java @@ -3,20 +3,21 @@ package creator.loki.feature.music.listener; import java.util.List; import creator.loki.feature.music.core.LavalinkManager; -import creator.loki.feature.music.core.PlayerManager; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MusicBotControlHandler { - private final LavalinkManager lavalinkManager; - private final PlayerManager playerManager; + private static final Logger log = LoggerFactory.getLogger(MusicBotControlHandler.class); + private static final int ERR = 0xFF4444; - public MusicBotControlHandler(LavalinkManager lavalinkManager, PlayerManager playerManager) { + private final LavalinkManager lavalinkManager; + + public MusicBotControlHandler(LavalinkManager lavalinkManager) { this.lavalinkManager = lavalinkManager; - this.playerManager = playerManager; } public void handleSkip(MessageChannel ch, Guild guild, Member member) { @@ -26,7 +27,7 @@ public class MusicBotControlHandler { send(ch, err("Нечего пропускать.")); return; } - String t = s.getCurrentTrack().getInfo().getTitle(); + var t = s.getCurrentTrack().getInfo().getTitle(); s.skip(); send(ch, ok("⏭ Пропущено", "**" + t + "**")); } @@ -35,7 +36,7 @@ public class MusicBotControlHandler { if (reqVoice(ch, member) == null) return; lavalinkManager.getMusicManager(guild.getIdLong()).stop(); guild.getJDA().getDirectAudioController().disconnect(guild); - send(ch, ok(0xFF4444, "⏹ Остановлено", "Музыка остановлена, очередь очищена.")); + send(ch, ok(ERR, "⏹ Остановлено", "Музыка остановлена, очередь очищена.")); } public void handleQueue(MessageChannel ch, Guild guild) { @@ -47,18 +48,15 @@ public class MusicBotControlHandler { return; } var sb = new StringBuilder(); - if (cur != null) { - sb.append("**Сейчас:** ").append(cur.getInfo().getTitle()).append("\n"); - } + if (cur != null) sb.append("**Сейчас:** ").append(cur.getInfo().getTitle()).append("\n"); if (!q.isEmpty()) { - int show = Math.min(q.size(), 10); - for (int i = 0; i < show; i++) { + var show = Math.min(q.size(), 10); + for (var i = 0; i < show; i++) sb.append("`") .append(i + 1) .append(".` ") .append(q.get(i).getInfo().getTitle()) .append("\n"); - } if (q.size() > show) sb.append("... и ещё **").append(q.size() - show).append("** треков"); } send(ch, ok("📜 Очередь", sb.toString())); @@ -79,22 +77,23 @@ public class MusicBotControlHandler { if (reqVoice(ch, member) == null) return; var link = lavalinkManager.getClient().getLinkIfCached(guild.getIdLong()); if (args.isEmpty()) { - int v = - link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getVolume() : 100; - send(ch, ok("🔊 Громкость", "Текущая: **" + v + "%**")); + send( + ch, + ok( + "🔊 Громкость", + "Текущая: **" + + (link != null && link.getCachedPlayer() != null + ? link.getCachedPlayer().getVolume() + : 100) + + "%**")); return; } try { - int vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0)))); - if (link != null && link.getCachedPlayer() != null) { + var vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0)))); + if (link != null && link.getCachedPlayer() != null) link.createOrUpdatePlayer() .setVolume(vol) - .subscribe( - null, - err -> - LoggerFactory.getLogger(MusicBotControlHandler.class) - .warn("Lavalink: {}", err.getMessage())); - } + .subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage())); send(ch, ok("🔊 Громкость", "Установлена **" + vol + "%**")); } catch (NumberFormatException e) { send(ch, err("Укажи число от 0 до 100.")); @@ -105,17 +104,19 @@ public class MusicBotControlHandler { if (reqVoice(ch, member) == null) return; lavalinkManager.getMusicManager(guild.getIdLong()).stop(); guild.getJDA().getDirectAudioController().disconnect(guild); - send(ch, ok(0xFF4444, "👋 Отключился", "Вышел из голосового канала.")); + send(ch, ok(ERR, "👋 Отключился", "Вышел из голосового канала.")); } public void handleRepeat(MessageChannel ch, Guild guild, Member member) { if (reqVoice(ch, member) == null) return; var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler(); - boolean now = !s.isRepeating(); + var now = !s.isRepeating(); s.setRepeating(now); - String t = now ? "🔁 Повтор включён" : "➡ Повтор выключен"; - String d = now ? "Очередь будет повторяться." : "Повтор отключён."; - send(ch, ok(t, d)); + send( + ch, + ok( + now ? "🔁 Повтор включён" : "➡ Повтор выключен", + now ? "Очередь будет повторяться." : "Повтор отключён.")); } public void handleShuffle(MessageChannel ch, Guild guild, Member member) { @@ -131,13 +132,12 @@ public class MusicBotControlHandler { public void handleBassboost(MessageChannel ch, Guild guild, Member member, List args) { if (reqVoice(ch, member) == null) return; - int level = 10; - if (!args.isEmpty()) { + var level = 10; + if (!args.isEmpty()) try { level = Math.max(-25, Math.min(25, Integer.parseInt(args.get(0)))); } catch (NumberFormatException ignored) { } - } lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler().setBassboost(level); send(ch, ok("🔊 Bassboost", "**" + level + "**")); } @@ -145,7 +145,7 @@ public class MusicBotControlHandler { public void handleNightcore(MessageChannel ch, Guild guild, Member member) { if (reqVoice(ch, member) == null) return; var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler(); - boolean now = !s.isNightcore(); + var now = !s.filterState.isNightcore(); s.setNightcore(now); send(ch, ok("🎵 Nightcore", now ? "Включён ⚡" : "Выключен")); } @@ -153,7 +153,7 @@ public class MusicBotControlHandler { public void handleVaporwave(MessageChannel ch, Guild guild, Member member) { if (reqVoice(ch, member) == null) return; var s = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler(); - boolean now = !s.isVaporwave(); + var now = !s.filterState.isVaporwave(); s.setVaporwave(now); send(ch, ok("🎵 Vaporwave", now ? "Включён 🌊" : "Выключен")); } @@ -171,10 +171,12 @@ public class MusicBotControlHandler { return; } try { - int band = Math.max(0, Math.min(14, Integer.parseInt(args.get(0)))); - float gain = (float) Math.max(-0.25, Math.min(1.0, Double.parseDouble(args.get(1)))); - var sc = lavalinkManager.getMusicManager(guild.getIdLong()).getScheduler(); - sc.setEqualizerBand(band, gain); + var band = Math.max(0, Math.min(14, Integer.parseInt(args.get(0)))); + var gain = (float) Math.max(-0.25, Math.min(1.0, Double.parseDouble(args.get(1)))); + lavalinkManager + .getMusicManager(guild.getIdLong()) + .getScheduler() + .setEqualizerBand(band, gain); send( ch, ok("🎛 Эквалайзер", "Band **" + band + "** → **" + String.format("%.2f", gain) + "**")); @@ -202,6 +204,6 @@ public class MusicBotControlHandler { } private static EmbedBuilder err(String msg) { - return new EmbedBuilder().setColor(0xFF4444).setDescription("❌ " + msg); + return new EmbedBuilder().setColor(ERR).setDescription("❌ " + msg); } } diff --git a/src/main/java/creator/loki/feature/music/listener/MusicBotListener.java b/src/main/java/creator/loki/feature/music/listener/MusicBotListener.java index 143a483..417bf3e 100644 --- a/src/main/java/creator/loki/feature/music/listener/MusicBotListener.java +++ b/src/main/java/creator/loki/feature/music/listener/MusicBotListener.java @@ -23,7 +23,7 @@ public class MusicBotListener extends ListenerAdapter { QueueRepository queueRepository, creator.loki.feature.music.core.PlayerManager playerManager) { this.playHandler = new MusicBotPlayHandler(lavalinkManager); - this.controlHandler = new MusicBotControlHandler(lavalinkManager, playerManager); + this.controlHandler = new MusicBotControlHandler(lavalinkManager); } @Override diff --git a/src/main/java/creator/loki/feature/music/manage/FilterState.java b/src/main/java/creator/loki/feature/music/manage/FilterState.java index 0a86eec..66d9476 100644 --- a/src/main/java/creator/loki/feature/music/manage/FilterState.java +++ b/src/main/java/creator/loki/feature/music/manage/FilterState.java @@ -34,34 +34,39 @@ public class FilterState { return vaporwave; } - public void setBassboost(int level) { + public FilterState setBassboost(int level) { this.bassboost = Math.max(0f, Math.min(1.0f, level / 100.0f)); + return this; } - public void setNightcore(boolean enabled) { + public FilterState setNightcore(boolean enabled) { this.nightcore = enabled; if (enabled) this.vaporwave = false; + return this; } - public void setVaporwave(boolean enabled) { + public FilterState setVaporwave(boolean enabled) { this.vaporwave = enabled; if (enabled) this.nightcore = false; + return this; } - public void setEqualizerBand(int band, float gain) { - if (band < 0 || band > 14) return; + public FilterState setEqualizerBand(int band, float gain) { + if (band < 0 || band > 14) return this; customBands.removeIf(b -> b.getBand() == band); customBands.add(new Band(band, Math.max(-0.25f, Math.min(10.0f, gain)))); + return this; } - public void reset() { + public FilterState reset() { this.bassboost = 0f; this.nightcore = false; this.vaporwave = false; this.customBands.clear(); + return this; } - public void apply(Optional link) { + public FilterState apply(Optional link) { link.ifPresent( l -> { FilterBuilder fb = new FilterBuilder(); @@ -86,5 +91,6 @@ public class FilterState { .setFilters(fb.build()) .subscribe(null, err -> System.err.println("Filter error: " + err.getMessage())); }); + return this; } } diff --git a/src/main/java/creator/loki/feature/music/manage/GuildMusicManager.java b/src/main/java/creator/loki/feature/music/manage/GuildMusicManager.java index 5876ff0..c043f38 100644 --- a/src/main/java/creator/loki/feature/music/manage/GuildMusicManager.java +++ b/src/main/java/creator/loki/feature/music/manage/GuildMusicManager.java @@ -12,6 +12,7 @@ import creator.loki.feature.music.core.LavalinkManager; import creator.loki.feature.music.repository.QueueRepository; import creator.loki.feature.music.repository.QueueRepository.QueueTrack; import creator.loki.util.LoggerDecorator; +import dev.arbjerg.lavalink.client.LavalinkNode; import dev.arbjerg.lavalink.client.Link; import dev.arbjerg.lavalink.client.player.LavalinkPlayer; import dev.arbjerg.lavalink.client.player.Track; @@ -27,7 +28,6 @@ public class GuildMusicManager { private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private ScheduledFuture autoLeaveFuture; - @SuppressWarnings("this-escape") public GuildMusicManager( long guildId, LavalinkManager lavalinkManager, QueueRepository queueRepository) { this.guildId = guildId; @@ -62,20 +62,17 @@ public class GuildMusicManager { public void startAutoLeaveTimer() { cancelAutoLeaveTimer(); - autoLeaveFuture = - executor.schedule( - () -> { - stop(); - getLink() - .ifPresent( - link -> - link.destroy() - .subscribe( - null, err -> logger.warn("Lavalink: {}", err.getMessage()))); - lavalinkManager.removeMusicManager(guildId); - }, - 2, - TimeUnit.MINUTES); + autoLeaveFuture = executor.schedule(this::autoLeave, 2, TimeUnit.MINUTES); + } + + private void autoLeave() { + stop(); + getLink() + .ifPresent( + link -> + link.destroy() + .subscribe(null, err -> logger.warn("Lavalink: {}", err.getMessage()))); + lavalinkManager.removeMusicManager(guildId); } public void cancelAutoLeaveTimer() { @@ -101,43 +98,33 @@ public class GuildMusicManager { return guildId; } - QueueRepository getQueueRepo() { - return queueRepository; - } - private void loadQueueAsync() { - List saved = queueRepository.loadQueue(String.valueOf(guildId)); + var saved = queueRepository.loadQueue(String.valueOf(guildId)); if (saved.isEmpty()) return; - var client = lavalinkManager.getClient(); if (client == null || client.getNodes().isEmpty()) return; - var node = client.getNodes().get(0); + executor.execute(() -> restoreTracks(saved, client.getNodes().get(0))); + } - executor.execute( - () -> { - List decoded = new ArrayList<>(); - for (QueueTrack qt : saved) { - try { - Track track = node.decodeTrack(qt.encoded()).block(); - if (track != null) decoded.add(track); - } catch (Exception e) { - LoggerDecorator.warn(logger, "Не удалось декодировать: " + qt.title()); - } - } - if (decoded.isEmpty()) return; - var q = scheduler.getRawQueue(); - q.addAll(decoded.subList(1, decoded.size())); - Track first = decoded.get(0); - getPlayer() - .ifPresentOrElse( - player -> { - if (player.getTrack() == null) scheduler.startTrack(first); - else q.offer(first); - }, - () -> q.offer(first)); - LoggerDecorator.music(logger, "Восстановлено " + decoded.size() + " треков"); - queueRepository.deleteQueue(String.valueOf(guildId)); - }); + private void restoreTracks(List saved, LavalinkNode node) { + var decoded = new ArrayList(); + for (var qt : saved) { + try { + var track = node.decodeTrack(qt.encoded()).block(); + if (track != null) decoded.add(track); + } catch (Exception e) { + LoggerDecorator.warn(logger, "Не удалось декодировать: " + qt.title()); + } + } + if (decoded.isEmpty()) return; + var q = scheduler.queue; + q.addAll(decoded.subList(1, decoded.size())); + var first = decoded.get(0); + var player = getPlayer().orElse(null); + if (player != null && player.getTrack() == null) scheduler.startTrack(first); + else q.offer(first); + LoggerDecorator.music(logger, "Восстановлено " + decoded.size() + " треков"); + queueRepository.deleteQueue(String.valueOf(guildId)); } private ScheduledFuture saveDebounced; @@ -148,20 +135,16 @@ public class GuildMusicManager { } void saveQueue() { - Track current = scheduler.getCurrentTrack(); - List tracks = new ArrayList<>(scheduler.getQueue()); - List queueTracks = new ArrayList<>(); + var current = scheduler.getCurrentTrack(); + var tracks = new ArrayList<>(scheduler.getQueue()); + var queueTracks = new ArrayList(); if (current != null) queueTracks.add(toQueueTrack(current)); - for (Track t : tracks) queueTracks.add(toQueueTrack(t)); + for (var t : tracks) queueTracks.add(toQueueTrack(t)); queueRepository.saveQueue(String.valueOf(guildId), queueTracks); } private static QueueTrack toQueueTrack(Track t) { - return new QueueTrack( - t.getEncoded(), - t.getInfo().getTitle(), - t.getInfo().getAuthor(), - t.getInfo().getUri(), - t.getInfo().getLength()); + var i = t.getInfo(); + return new QueueTrack(t.getEncoded(), i.getTitle(), i.getAuthor(), i.getUri(), i.getLength()); } } diff --git a/src/main/java/creator/loki/feature/music/manage/TrackScheduler.java b/src/main/java/creator/loki/feature/music/manage/TrackScheduler.java index 42c9e69..87cfe2e 100644 --- a/src/main/java/creator/loki/feature/music/manage/TrackScheduler.java +++ b/src/main/java/creator/loki/feature/music/manage/TrackScheduler.java @@ -6,6 +6,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import creator.loki.util.LoggerDecorator; import dev.arbjerg.lavalink.client.Link; @@ -17,45 +18,32 @@ import org.slf4j.LoggerFactory; public class TrackScheduler { private static final Logger log = LoggerFactory.getLogger(TrackScheduler.class); + private static final Consumer LOG_LAV = t -> log.warn("Lav: {}", t.getMessage()); private final GuildMusicManager manager; - private final Queue queue; + final Queue queue = new LinkedList<>(); private boolean repeating; - private final FilterState filterState; + public final FilterState filterState = new FilterState(); public TrackScheduler(GuildMusicManager manager) { this.manager = manager; - this.queue = new LinkedList<>(); - this.repeating = false; - this.filterState = new FilterState(); } public void enqueue(Track track) { manager.cancelAutoLeaveTimer(); - manager - .getPlayer() - .ifPresentOrElse( - player -> { - if (player.getTrack() == null) startTrack(track); - else { - queue.offer(track); - manager.saveQueueDebounced(); - } - }, - () -> startTrack(track)); + if (manager.getPlayer().filter(p -> p.getTrack() != null).isPresent()) { + queue.offer(track); + manager.saveQueueDebounced(); + } else { + startTrack(track); + } } public void skip() { - manager - .getPlayer() - .ifPresent( - player -> - player - .setTrack(null) - .subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage()))); + manager.getPlayer().ifPresent(p -> p.setTrack(null).subscribe(null, LOG_LAV)); } public boolean nextTrack() { - Track next = queue.poll(); + var next = queue.poll(); if (next == null) return false; startTrack(next); manager.saveQueueDebounced(); @@ -65,19 +53,12 @@ public class TrackScheduler { public void stop() { manager.cancelAutoLeaveTimer(); queue.clear(); - manager - .getPlayer() - .ifPresent( - player -> - player - .setPaused(false) - .setTrack(null) - .subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage()))); + manager.getPlayer().ifPresent(p -> p.setPaused(false).setTrack(null).subscribe(null, LOG_LAV)); manager.saveQueue(); } public void shuffle() { - List list = new ArrayList<>(queue); + var list = new ArrayList<>(queue); Collections.shuffle(list); queue.clear(); queue.addAll(list); @@ -100,103 +81,66 @@ public class TrackScheduler { this.repeating = r; } - public boolean isNightcore() { - return filterState.isNightcore(); - } - - public boolean isVaporwave() { - return filterState.isVaporwave(); - } - public void setBassboost(int l) { - filterState.setBassboost(l); - filterState.apply(manager.getLink()); + filterState.setBassboost(l).apply(manager.getLink()); } public void setNightcore(boolean e) { - filterState.setNightcore(e); - filterState.apply(manager.getLink()); + filterState.setNightcore(e).apply(manager.getLink()); } public void setVaporwave(boolean e) { - filterState.setVaporwave(e); - filterState.apply(manager.getLink()); + filterState.setVaporwave(e).apply(manager.getLink()); } public void setEqualizerBand(int b, float g) { - filterState.setEqualizerBand(b, g); - filterState.apply(manager.getLink()); + filterState.setEqualizerBand(b, g).apply(manager.getLink()); } public void resetFilters() { - filterState.reset(); - filterState.apply(manager.getLink()); - } - - public void onTrackStart(Track track) { - manager.cancelAutoLeaveTimer(); + filterState.reset().apply(manager.getLink()); } public void onTrackEnd(Track lastTrack, AudioTrackEndReason endReason) { if (endReason == AudioTrackEndReason.LOAD_FAILED) { LoggerDecorator.warn(log, "Load failed, skipping to next"); - Track next = queue.poll(); - if (next != null) { - startTrack(next); - } else { - manager.startAutoLeaveTimer(); - } - return; - } - if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) { + nextOrAutoLeave(); + } else if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) { LoggerDecorator.warn(log, "Track ended unexpectedly: " + endReason); manager.startAutoLeaveTimer(); - return; - } - if (repeating && endReason == AudioTrackEndReason.FINISHED) { + } else if (repeating && endReason == AudioTrackEndReason.FINISHED) { startTrack(lastTrack.makeClone()); } else { - Track next = queue.poll(); - if (next != null) { - startTrack(next); - } else { - manager.startAutoLeaveTimer(); - } + nextOrAutoLeave(); } } - Queue getRawQueue() { - return queue; - } - void startTrack(Track track) { manager.cancelAutoLeaveTimer(); - Link link = manager.getLavalinkManager().getOrCreateLink(manager.getGuildId()); + var link = manager.getLavalinkManager().getOrCreateLink(manager.getGuildId()); sendTrackDelayed(link, track, 0); } + private void nextOrAutoLeave() { + var next = queue.poll(); + if (next != null) startTrack(next); + else manager.startAutoLeaveTimer(); + } + + private void scheduleRetry(Runnable r, long delayMs) { + manager.getExecutor().schedule(r, delayMs, TimeUnit.MILLISECONDS); + } + private void sendTrackDelayed(Link link, Track track, int attempt) { if (attempt > 5) return; if (link.getCachedPlayer() == null) { - manager - .getExecutor() - .schedule(() -> sendTrackDelayed(link, track, attempt + 1), 300, TimeUnit.MILLISECONDS); + scheduleRetry(() -> sendTrackDelayed(link, track, attempt + 1), 300); return; } link.createOrUpdatePlayer() .setTrack(track) .setVolume(100) .subscribe( - null, - err -> { - if (attempt < 5) { - manager - .getExecutor() - .schedule( - () -> sendTrackDelayed(link, track, attempt + 1), - 500, - TimeUnit.MILLISECONDS); - } - }); + null, err -> scheduleRetry(() -> sendTrackDelayed(link, track, attempt + 1), 500)); } } diff --git a/src/main/java/creator/loki/feature/music/play/PlayCommand.java b/src/main/java/creator/loki/feature/music/play/PlayCommand.java index d72c801..7c7f184 100644 --- a/src/main/java/creator/loki/feature/music/play/PlayCommand.java +++ b/src/main/java/creator/loki/feature/music/play/PlayCommand.java @@ -6,15 +6,19 @@ import java.util.Set; import creator.loki.command.Command; import creator.loki.command.CommandContext; import creator.loki.embed.EmbedTemplates; +import creator.loki.feature.music.core.LavalinkManager; import creator.loki.feature.music.core.PlayerManager; import creator.loki.feature.music.core.SearchManager; import creator.loki.feature.music.manage.GuildMusicManager; +import creator.loki.util.VoiceHelper; import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler; import dev.arbjerg.lavalink.client.player.LoadFailed; import dev.arbjerg.lavalink.client.player.PlaylistLoaded; import dev.arbjerg.lavalink.client.player.SearchResult; import dev.arbjerg.lavalink.client.player.Track; import dev.arbjerg.lavalink.client.player.TrackLoaded; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; public class PlayCommand implements Command { private static final Set SOURCES = Set.of("yt", "sc", "list"); @@ -48,9 +52,9 @@ public class PlayCommand implements Command { @Override public void execute(CommandContext ctx) { - var guild = ctx.getGuild(); + Guild guild = ctx.getGuild(); if (guild == null) return; - var member = ctx.getAuthor(); + Member member = ctx.getAuthor(); if (member == null || !member.getVoiceState().inAudioChannel()) { ctx.getChannel() .sendMessageEmbeds( @@ -60,11 +64,7 @@ public class PlayCommand implements Command { } var args = ctx.getArgs(); if (args.isEmpty()) { - ctx.getChannel() - .sendMessageEmbeds( - EmbedTemplates.error( - "Аргументы", "Укажи URL, запрос или номер: `" + getUsage() + "`")) - .queue(); + sendErr(ctx, "Укажи URL, запрос или номер: `" + getUsage() + "`"); return; } String first = args.get(0); @@ -78,29 +78,16 @@ public class PlayCommand implements Command { return; } var lm = playerManager.getLavalinkManager(); - if (!guild.getMember(guild.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) { + if (!guild.getSelfMember().getVoiceState().inAudioChannel()) { var vc = member.getVoiceState().getChannel().asVoiceChannel(); guild.getJDA().getDirectAudioController().connect(vc); - if (!guild.getAudioManager().isConnected()) { - guild.getAudioManager().openAudioConnection(vc); - } - for (int i = 0; i < 10; i++) { - if (guild.getMember(guild.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) break; - try { - Thread.sleep(200); - } catch (InterruptedException e) { - return; - } - } + if (!guild.getAudioManager().isConnected()) guild.getAudioManager().openAudioConnection(vc); + var self = guild.getSelfMember(); + VoiceHelper.waitForCondition(() -> self.getVoiceState().inAudioChannel()); + if (!self.getVoiceState().inAudioChannel()) return; var link = lm.getOrCreateLink(guild.getIdLong()); - for (int i = 0; i < 10; i++) { - if (link.getCachedPlayer() != null) break; - try { - Thread.sleep(200); - } catch (InterruptedException e) { - return; - } - } + VoiceHelper.waitForCondition(() -> link.getCachedPlayer() != null); + if (link.getCachedPlayer() == null) return; } var mm = lm.getMusicManager(guild.getIdLong()); boolean isSource = SOURCES.contains(first); @@ -109,40 +96,31 @@ public class PlayCommand implements Command { sendErr(ctx, "Укажи запрос после источника."); return; } - boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://"); - boolean isList = !isUrl && first.equals("list"); - - String id; - if (isUrl) { - id = raw; - } else if (isList) { - id = "ytsearch:" + raw; - } else if (raw.startsWith("ytmsearch:") - || raw.startsWith("ytsearch:") - || raw.startsWith("scsearch:")) { - id = raw; - } else { - id = "ytsearch:" + raw; - } - - boolean autoPlay = isUrl; + String query = isUrl ? null : (first.equals("list") ? raw : raw); + String id = buildQuery(first, raw); + loadAndHandle(ctx, lm, guild.getIdLong(), mm, id, isUrl, query, raw); + } + private void loadAndHandle( + CommandContext ctx, + LavalinkManager lm, + long guildId, + GuildMusicManager mm, + String id, + boolean isUrl, + String query, + String raw) { String noMatch = isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw; - String query = isList ? raw : (isUrl ? null : raw); - - lm.getOrCreateLink(guild.getIdLong()) + lm.getOrCreateLink(guildId) .loadItem(id) .subscribe( new AbstractAudioLoadResultHandler() { @Override public void ontrackLoaded(TrackLoaded r) { - if (autoPlay) { - playTrack(ctx, r.getTrack()); - } else { - showResults(ctx, query, mm, List.of(r.getTrack())); - } + if (isUrl) playTrack(ctx, r.getTrack()); + else showResults(ctx, query, mm, List.of(r.getTrack())); } @Override @@ -159,7 +137,7 @@ public class PlayCommand implements Command { "Плейлист добавлен", "**" + r.getInfo().getName() + "** — " + c + " треков")) .queue(); - } else if (autoPlay && r.getTracks().size() <= 1 && !r.getTracks().isEmpty()) { + } else if (r.getTracks().size() <= 1 && !r.getTracks().isEmpty()) { playTrack(ctx, r.getTracks().get(0)); } else { showResults(ctx, query, mm, r.getTracks()); @@ -170,7 +148,7 @@ public class PlayCommand implements Command { public void onSearchResultLoaded(SearchResult r) { if (r.getTracks().isEmpty()) { sendErr(ctx, noMatch); - } else if (autoPlay) { + } else if (isUrl) { playTrack(ctx, r.getTracks().get(0)); } else { showResults(ctx, query, mm, r.getTracks()); @@ -193,6 +171,16 @@ public class PlayCommand implements Command { }); } + private static String buildQuery(String first, String raw) { + if (raw.startsWith("http://") || raw.startsWith("https://")) return raw; + if (!first.equals("list") + && !raw.startsWith("ytsearch:") + && !raw.startsWith("ytmsearch:") + && !raw.startsWith("scsearch:")) return "ytsearch:" + raw; + if (first.equals("list")) return "ytsearch:" + raw; + return raw; + } + private void showResults(CommandContext ctx, String q, GuildMusicManager m, List list) { if (q == null) return; searchHandler.storeResults(ctx.getGuild().getIdLong(), list); diff --git a/src/main/java/creator/loki/feature/music/slash/FilterSlashHandler.java b/src/main/java/creator/loki/feature/music/slash/FilterSlashHandler.java index 023852f..a7fe36c 100644 --- a/src/main/java/creator/loki/feature/music/slash/FilterSlashHandler.java +++ b/src/main/java/creator/loki/feature/music/slash/FilterSlashHandler.java @@ -28,7 +28,7 @@ public class FilterSlashHandler { public void handleNightcore(SlashCommandInteractionEvent event) { if (reqVoice(event) == null) return; var manager = getManager(event); - boolean now = !manager.getScheduler().isNightcore(); + boolean now = !manager.getScheduler().filterState.isNightcore(); manager.getScheduler().setNightcore(now); event .replyEmbeds( @@ -39,7 +39,7 @@ public class FilterSlashHandler { public void handleVaporwave(SlashCommandInteractionEvent event) { if (reqVoice(event) == null) return; var manager = getManager(event); - boolean now = !manager.getScheduler().isVaporwave(); + boolean now = !manager.getScheduler().filterState.isVaporwave(); manager.getScheduler().setVaporwave(now); event .replyEmbeds( diff --git a/src/main/java/creator/loki/feature/music/slash/QueueSlashHandler.java b/src/main/java/creator/loki/feature/music/slash/QueueSlashHandler.java index fdd16b0..4f1dae7 100644 --- a/src/main/java/creator/loki/feature/music/slash/QueueSlashHandler.java +++ b/src/main/java/creator/loki/feature/music/slash/QueueSlashHandler.java @@ -2,193 +2,144 @@ package creator.loki.feature.music.slash; import java.awt.Color; import java.util.List; +import java.util.function.Consumer; import creator.loki.embed.EmbedFactory; import creator.loki.embed.EmbedTemplates; import creator.loki.feature.music.core.PlayerManager; -import creator.loki.feature.music.manage.GuildMusicManager; import dev.arbjerg.lavalink.client.player.Track; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import org.slf4j.LoggerFactory; public class QueueSlashHandler { - private final PlayerManager playerManager; + private final PlayerManager pm; - public QueueSlashHandler(PlayerManager playerManager) { - this.playerManager = playerManager; + public QueueSlashHandler(PlayerManager pm) { + this.pm = pm; } - public void handleStop(SlashCommandInteractionEvent event) { - if (reqVoice(event) == null) return; - var manager = getManager(event); - manager.stop(); - event.getGuild().getJDA().getDirectAudioController().disconnect(event.getGuild()); - event - .replyEmbeds(EmbedTemplates.info("Остановлено", "Музыка остановлена, очередь очищена.")) + public void handleStop(SlashCommandInteractionEvent ev) { + if (reqVoice(ev) == null) return; + pm.getMusicManager(ev.getGuild().getIdLong()).stop(); + ev.getGuild().getJDA().getDirectAudioController().disconnect(ev.getGuild()); + ev.replyEmbeds(EmbedTemplates.info("Остановлено", "Музыка остановлена, очередь очищена.")) .queue(); } - public void handleSkip(SlashCommandInteractionEvent event) { - if (reqVoice(event) == null) return; - var manager = getManager(event); - if (manager.getScheduler().getCurrentTrack() == null) { - event.replyEmbeds(EmbedTemplates.error("Очередь пуста", "Сейчас ничего не играет.")).queue(); + public void handleSkip(SlashCommandInteractionEvent ev) { + if (reqVoice(ev) == null) return; + var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler(); + if (s.getCurrentTrack() == null) { + ev.replyEmbeds(EmbedTemplates.error("Очередь пуста", "Сейчас ничего не играет.")).queue(); return; } - String skipped = manager.getScheduler().getCurrentTrack().getInfo().getTitle(); - manager.getScheduler().skip(); - event.replyEmbeds(EmbedTemplates.info("Пропущено", "**" + skipped + "**")).queue(); + var t = s.getCurrentTrack().getInfo().getTitle(); + s.skip(); + ev.replyEmbeds(EmbedTemplates.info("Пропущено", "**" + t + "**")).queue(); } - public void handleQueue(SlashCommandInteractionEvent event) { - var manager = getManager(event); - Track current = manager.getScheduler().getCurrentTrack(); - List queue = manager.getScheduler().getQueue(); - if (current == null && queue.isEmpty()) { - event.reply("Очередь пуста.").queue(); + public void handleQueue(SlashCommandInteractionEvent ev) { + var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler(); + var cur = s.getCurrentTrack(); + var q = s.getQueue(); + if (cur == null && q.isEmpty()) { + ev.reply("Очередь пуста.").queue(); return; } - var embed = - EmbedFactory.create().withTitle("Очередь треков").withColor(new Color(88, 101, 242)); - if (current != null) { - embed.addField( - "Сейчас играет", - "**" + current.getInfo().getTitle() + "**\n" + current.getInfo().getAuthor(), - false); + var eb = EmbedFactory.create().withTitle("Очередь треков").withColor(new Color(88, 101, 242)); + if (cur != null) { + var ci = cur.getInfo(); + eb.addField("Сейчас играет", "**" + ci.getTitle() + "**\n" + ci.getAuthor(), false); } - if (!queue.isEmpty()) { - StringBuilder sb = new StringBuilder(); - int show = Math.min(queue.size(), 10); - for (int i = 0; i < show; i++) { - Track t = queue.get(i); - sb.append("`").append(i + 1).append(".` **").append(t.getInfo().getTitle()).append("**\n"); - } - if (queue.size() > show) { - sb.append("... и ещё **").append(queue.size() - show).append("** треков"); - } - embed.addField("В очереди (" + queue.size() + ")", sb.toString(), false); - } - event.replyEmbeds(embed.build()).queue(); + if (!q.isEmpty()) eb.addField("В очереди (" + q.size() + ")", buildQueueList(q), false); + ev.replyEmbeds(eb.build()).queue(); } - public void handleNowPlaying(SlashCommandInteractionEvent event) { - var manager = getManager(event); - var track = manager.getScheduler().getCurrentTrack(); + private String buildQueueList(List q) { + var sb = new StringBuilder(); + var show = Math.min(q.size(), 10); + for (int i = 0; i < show; i++) { + var t = q.get(i); + sb.append("`").append(i + 1).append(".` **").append(t.getInfo().getTitle()).append("**\n"); + } + if (q.size() > show) sb.append("... и ещё **").append(q.size() - show).append("** треков"); + return sb.toString(); + } + + public void handleNowPlaying(SlashCommandInteractionEvent ev) { + var track = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler().getCurrentTrack(); if (track == null) { - event.reply("Сейчас ничего не играет.").queue(); + ev.reply("Сейчас ничего не играет.").queue(); return; } - var link = - playerManager - .getLavalinkManager() - .getClient() - .getLinkIfCached(event.getGuild().getIdLong()); - long pos = - link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getPosition() : 0; - long dur = track.getInfo().getLength(); - event - .replyEmbeds( - EmbedFactory.create() - .withTitle("Сейчас играет") - .withColor(new Color(88, 101, 242)) - .addField("Название", "**" + track.getInfo().getTitle() + "**", false) - .addField("Автор", track.getInfo().getAuthor(), true) - .addField("Длительность", progressBar(pos, dur), true) - .build()) - .queue(); + var link = pm.getLavalinkManager().getClient().getLinkIfCached(ev.getGuild().getIdLong()); + var pos = + link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getPosition() : 0L; + var eb = + EmbedFactory.create() + .withTitle("Сейчас играет") + .withColor(new Color(88, 101, 242)) + .addField("Название", "**" + track.getInfo().getTitle() + "**", false) + .addField("Автор", track.getInfo().getAuthor(), true) + .addField("Длительность", progressBar(pos, track.getInfo().getLength()), true) + .build(); + ev.replyEmbeds(eb).queue(); } - public void handleVolume(SlashCommandInteractionEvent event) { - if (reqVoice(event) == null) return; - var link = - playerManager - .getLavalinkManager() - .getClient() - .getLinkIfCached(event.getGuild().getIdLong()); - Integer percent = - event.getOption("percent") != null ? event.getOption("percent").getAsInt() : null; - if (percent == null) { - int vol = + public void handleVolume(SlashCommandInteractionEvent ev) { + if (reqVoice(ev) == null) return; + var link = pm.getLavalinkManager().getClient().getLinkIfCached(ev.getGuild().getIdLong()); + var opt = ev.getOption("percent"); + if (opt == null) { + var cur = link != null && link.getCachedPlayer() != null ? link.getCachedPlayer().getVolume() : 100; - event.replyEmbeds(EmbedTemplates.info("Громкость", "Текущая: **" + vol + "%**")).queue(); + ev.replyEmbeds(EmbedTemplates.info("Громкость", "Текущая: **" + cur + "%**")).queue(); return; } - int vol = Math.max(0, Math.min(100, percent)); - if (link != null && link.getCachedPlayer() != null) { - link.createOrUpdatePlayer() - .setVolume(vol) - .subscribe( - null, - err -> - LoggerFactory.getLogger(QueueSlashHandler.class) - .warn("Lavalink: {}", err.getMessage())); - } - event.replyEmbeds(EmbedTemplates.info("Громкость", "Установлена на **" + vol + "%**")).queue(); + var vol = Math.max(0, Math.min(100, opt.getAsInt())); + if (link != null && link.getCachedPlayer() != null) + link.createOrUpdatePlayer().setVolume(vol).subscribe(null, LOG); + ev.replyEmbeds(EmbedTemplates.info("Громкость", "Установлена на **" + vol + "%**")).queue(); } - public void handlePause(SlashCommandInteractionEvent event) { - if (reqVoice(event) == null) return; - getManager(event) + public void handlePause(SlashCommandInteractionEvent ev) { + if (reqVoice(ev) == null) return; + pm.getMusicManager(ev.getGuild().getIdLong()) .getPlayer() - .ifPresent( - p -> - p.setPaused(true) - .subscribe( - null, - err -> - LoggerFactory.getLogger(QueueSlashHandler.class) - .warn("Lavalink: {}", err.getMessage()))); - event.replyEmbeds(EmbedTemplates.info("Пауза", "Музыка поставлена на паузу.")).queue(); + .ifPresent(pl -> pl.setPaused(true).subscribe(null, LOG)); + ev.replyEmbeds(EmbedTemplates.info("Пауза", "Музыка поставлена на паузу.")).queue(); } - public void handleResume(SlashCommandInteractionEvent event) { - if (reqVoice(event) == null) return; - getManager(event) + public void handleResume(SlashCommandInteractionEvent ev) { + if (reqVoice(ev) == null) return; + pm.getMusicManager(ev.getGuild().getIdLong()) .getPlayer() - .ifPresent( - p -> - p.setPaused(false) - .subscribe( - null, - err -> - LoggerFactory.getLogger(QueueSlashHandler.class) - .warn("Lavalink: {}", err.getMessage()))); - event.replyEmbeds(EmbedTemplates.info("Продолжаем", "Воспроизведение продолжено.")).queue(); + .ifPresent(pl -> pl.setPaused(false).subscribe(null, LOG)); + ev.replyEmbeds(EmbedTemplates.info("Продолжаем", "Воспроизведение продолжено.")).queue(); } - public void handleShuffle(SlashCommandInteractionEvent event) { - if (reqVoice(event) == null) return; - getManager(event).getScheduler().shuffle(); - event.replyEmbeds(EmbedTemplates.info("Перемешано", "Очередь перемешана.")).queue(); + public void handleShuffle(SlashCommandInteractionEvent ev) { + if (reqVoice(ev) == null) return; + pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler().shuffle(); + ev.replyEmbeds(EmbedTemplates.info("Перемешано", "Очередь перемешана.")).queue(); } - public void handleRepeat(SlashCommandInteractionEvent event) { - if (reqVoice(event) == null) return; - var s = getManager(event).getScheduler(); - boolean now = !s.isRepeating(); + public void handleRepeat(SlashCommandInteractionEvent ev) { + if (reqVoice(ev) == null) return; + var s = pm.getMusicManager(ev.getGuild().getIdLong()).getScheduler(); + var now = !s.isRepeating(); s.setRepeating(now); - event - .replyEmbeds( - EmbedTemplates.info( - "Повтор", now ? "Повтор очереди включён." : "Повтор очереди выключен.")) - .queue(); + var d = now ? "Повтор очереди включён." : "Повтор очереди выключен."; + ev.replyEmbeds(EmbedTemplates.info("Повтор", d)).queue(); } - private Member reqVoice(SlashCommandInteractionEvent event) { - Member member = event.getMember(); - if (member == null || !member.getVoiceState().inAudioChannel()) { - event - .replyEmbeds(EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала.")) - .setEphemeral(true) - .queue(); - return null; - } - return member; - } - - private GuildMusicManager getManager(SlashCommandInteractionEvent event) { - return playerManager.getMusicManager(event.getGuild().getIdLong()); + private Member reqVoice(SlashCommandInteractionEvent ev) { + var m = ev.getMember(); + if (m != null && m.getVoiceState().inAudioChannel()) return m; + var em = EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала."); + ev.replyEmbeds(em).setEphemeral(true).queue(); + return null; } static String progressBar(long pos, long dur) { @@ -197,11 +148,12 @@ public class QueueSlashHandler { int filled = (int) ((double) pos / dur * total); var bar = new StringBuilder("\u2503"); for (int i = 0; i < total; i++) bar.append(i < filled ? "\u2588" : "\u2591"); - return bar + "\u2503 " + formatTime(pos) + " / " + formatTime(dur); + var p = String.format("%d:%02d", pos / 60000, (pos / 1000) % 60); + var d = String.format("%d:%02d", dur / 60000, (dur / 1000) % 60); + return bar + "\u2503 " + p + " / " + d; } - private static String formatTime(long millis) { - long s = millis / 1000; - return String.format("%d:%02d", s / 60, s % 60); - } + private static final Consumer LOG = + err -> + LoggerFactory.getLogger(QueueSlashHandler.class).warn("Lavalink: {}", err.getMessage()); } diff --git a/src/main/java/creator/loki/feature/profile/RankCardConstants.java b/src/main/java/creator/loki/feature/profile/RankCardConstants.java new file mode 100644 index 0000000..e0f7e5c --- /dev/null +++ b/src/main/java/creator/loki/feature/profile/RankCardConstants.java @@ -0,0 +1,27 @@ +package creator.loki.feature.profile; + +import java.awt.Color; + +public final class RankCardConstants { + public static final int W = 1400; + public static final int H = 500; + public static final int PAD = 28; + public static final int AVATAR_SIZE = 190; + public static final int AVATAR_X = PAD + 18; + public static final int AVATAR_Y = PAD + 30; + public static final int CONTENT_X = AVATAR_X + AVATAR_SIZE + 40; + public static final int CONTENT_W = W - PAD - 20 - CONTENT_X; + public static final int STATS_Y = 360; + public static final int STAT_H = 100; + public static final int STAT_GAP = 18; + + public static final Color ACCENT = new Color(255, 145, 0); + public static final Color ACCENT_DIM = new Color(255, 145, 0, 60); + public static final Color WHITE = Color.WHITE; + public static final Color GREY = new Color(190, 190, 200); + public static final Color DARK_CARD = new Color(12, 12, 18, 200); + public static final Color OVERLAY = new Color(8, 8, 16, 180); + public static final Color BAR_BG = new Color(255, 255, 255, 30); + + private RankCardConstants() {} +} diff --git a/src/main/java/creator/loki/feature/profile/RankCardGenerator.java b/src/main/java/creator/loki/feature/profile/RankCardGenerator.java index e50fa42..01ba47f 100644 --- a/src/main/java/creator/loki/feature/profile/RankCardGenerator.java +++ b/src/main/java/creator/loki/feature/profile/RankCardGenerator.java @@ -1,158 +1,140 @@ package creator.loki.feature.profile; +import static creator.loki.feature.profile.RankCardConstants.*; + import java.awt.BasicStroke; import java.awt.Color; -import java.awt.Font; -import java.awt.FontFormatException; +import java.awt.FontMetrics; +import java.awt.GradientPaint; import java.awt.Graphics2D; -import java.awt.RenderingHints; import java.awt.geom.Ellipse2D; +import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; -import java.io.IOException; -import java.io.InputStream; import java.net.URI; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import javax.imageio.ImageIO; public class RankCardGenerator { - private static final int WIDTH = 1400; - private static final int HEIGHT = 500; - private static final Map FONT_CACHE = new ConcurrentHashMap<>(); + private final RankCardRenderer renderer = new RankCardRenderer(); - @SuppressWarnings("deprecation") public BufferedImage generate(ProfileData data) throws Exception { - BufferedImage canvas = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); + BufferedImage canvas = new BufferedImage(W, H, BufferedImage.TYPE_INT_ARGB); Graphics2D g = canvas.createGraphics(); - - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g.setRenderingHint( - RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - - BufferedImage bg = loadBackground("backgrounds/" + data.getBackground()); - g.drawImage(bg, 0, 0, WIDTH, HEIGHT, null); - - g.setColor(new Color(0, 0, 0, 170)); - g.fillRoundRect(20, 20, WIDTH - 40, HEIGHT - 40, 40, 40); - - g.setStroke(new BasicStroke(2f)); - g.setColor(new Color(255, 140, 0, 100)); - g.drawRoundRect(20, 20, WIDTH - 40, HEIGHT - 40, 40, 40); + RankCardRenderer.applyHints(g); + g.drawImage(renderer.loadBackground("backgrounds/" + data.getBackground()), 0, 0, W, H, null); + g.setColor(OVERLAY); + g.fillRect(0, 0, W, H); + drawCard(g, PAD, PAD, W - PAD * 2, H - PAD * 2); + drawRankBadge(g, data.getRank()); BufferedImage avatar = ImageIO.read(new URI(data.getAvatarUrl()).toURL()); - drawCircleImage(g, avatar, 70, 90, 180); + drawAvatar(g, avatar, AVATAR_X, AVATAR_Y, AVATAR_SIZE); - g.setStroke(new BasicStroke(5f)); - g.setColor(new Color(255, 140, 0)); - g.drawOval(70, 90, 180, 180); + int nameY = AVATAR_Y + 58; + g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 50f)); + g.setColor(WHITE); + g.drawString(data.getUsername(), CONTENT_X, nameY); + int nameW = g.getFontMetrics().stringWidth(data.getUsername()); + drawLevelTag(g, data.getLevel(), CONTENT_X + nameW + 20, nameY - 40); - g.setFont(loadFont("fonts/Inter-Bold.ttf", 52f)); - g.setColor(Color.WHITE); - g.drawString(data.getUsername(), 320, 150); + g.setFont(renderer.loadFont("fonts/Inter-Regular.ttf", 26f)); + g.setColor(GREY); + g.drawString(data.getSubtitle(), CONTENT_X, nameY + 36); - g.setFont(loadFont("fonts/Inter-Bold.ttf", 24f)); - g.setColor(new Color(255, 140, 0)); - g.fillRoundRect(670, 110, 90, 42, 15, 15); - g.setColor(Color.BLACK); - g.drawString(data.getTag(), 690, 138); + int barLabelY = nameY + 100; + g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 22f)); + g.setColor(GREY); + g.drawString("ОПЫТ", CONTENT_X, barLabelY); + String xpStr = data.getXp() + " / " + data.getMaxXp() + " XP"; + g.setColor(ACCENT); + FontMetrics fm = g.getFontMetrics(); + g.drawString(xpStr, CONTENT_X + CONTENT_W - fm.stringWidth(xpStr), barLabelY); + renderer.drawXpBar(g, CONTENT_X, barLabelY + 12, CONTENT_W, 22, data.getXp(), data.getMaxXp()); - g.setFont(loadFont("fonts/Inter-Regular.ttf", 28f)); - g.setColor(new Color(220, 220, 220)); - g.drawString(data.getSubtitle(), 320, 210); - - g.setFont(loadFont("fonts/Inter-Bold.ttf", 48f)); - g.setColor(new Color(255, 140, 0)); - g.drawString(String.valueOf(data.getLevel()), 320, 300); - g.setFont(loadFont("fonts/Inter-Bold.ttf", 36f)); - g.drawString("УРОВЕНЬ", 380, 300); - - g.setFont(loadFont("fonts/Inter-Regular.ttf", 34f)); - g.setColor(Color.WHITE); - g.drawString(data.getXp() + " / " + data.getMaxXp() + " XP", 920, 300); - - int barX = 320; - int barY = 340; - int barWidth = 620; - int barHeight = 28; - g.setColor(new Color(255, 255, 255, 40)); - g.fillRoundRect(barX, barY, barWidth, barHeight, 20, 20); - - float progress = (float) data.getXp() / data.getMaxXp(); - int fill = (int) (barWidth * progress); - g.setColor(new Color(255, 140, 0)); - g.fillRoundRect(barX, barY, fill, barHeight, 20, 20); - - int dotX = barX + fill - 10; - g.setColor(Color.WHITE); - g.fillOval(dotX, barY - 6, 40, 40); - - drawIconStat(g, "АКТИВНОСТЬ", data.getActivity(), IconType.CLOCK, 80, 400); - drawIconStat(g, "СООБЩЕНИЙ", String.valueOf(data.getMessages()), IconType.MESSAGE, 500, 400); - drawIconStat(g, "РЕПУТАЦИЯ", String.valueOf(data.getReputation()), IconType.STAR, 920, 400); + int statW = (CONTENT_W - STAT_GAP * 2) / 3; + renderer.drawStatCard( + g, + "АКТИВНОСТЬ", + data.getActivity(), + RankCardIcons.StatIcon.CLOCK, + CONTENT_X, + STATS_Y, + statW, + STAT_H); + renderer.drawStatCard( + g, + "СООБЩЕНИЙ", + String.valueOf(data.getMessages()), + RankCardIcons.StatIcon.MESSAGE, + CONTENT_X + statW + STAT_GAP, + STATS_Y, + statW, + STAT_H); + renderer.drawStatCard( + g, + "РЕПУТАЦИЯ", + String.valueOf(data.getReputation()), + RankCardIcons.StatIcon.STAR, + CONTENT_X + (statW + STAT_GAP) * 2, + STATS_Y, + statW, + STAT_H); g.dispose(); return canvas; } - private enum IconType { - CLOCK, - MESSAGE, - STAR + private void drawCard(Graphics2D g, int x, int y, int w, int h) { + RoundRectangle2D rect = new RoundRectangle2D.Float(x, y, w, h, 36, 36); + g.setColor(new Color(10, 10, 18, 210)); + g.fill(rect); + GradientPaint gp = new GradientPaint(x, y, ACCENT_DIM, x + 6, y, new Color(0, 0, 0, 0)); + g.setPaint(gp); + g.setStroke(new BasicStroke(3f)); + g.draw(rect); + g.setPaint(new Color(255, 145, 0, 80)); + g.setStroke(new BasicStroke(1.5f)); + g.draw(rect); } - private void drawIconStat(Graphics2D g, String title, String value, IconType icon, int x, int y) { - g.setColor(new Color(15, 15, 15, 180)); - g.fillRoundRect(x, y, 340, 110, 30, 30); - - g.setColor(new Color(255, 140, 0)); - g.fillOval(x + 25, y + 25, 48, 48); - g.setColor(Color.WHITE); - - g.setFont(loadFont("fonts/Inter-Bold.ttf", 36f)); - g.setColor(Color.WHITE); - g.drawString(value, x + 100, y + 50); - - g.setFont(loadFont("fonts/Inter-Regular.ttf", 24f)); - g.setColor(new Color(180, 180, 180)); - g.drawString(title, x + 100, y + 90); - } - - private void drawCircleImage(Graphics2D g, BufferedImage image, int x, int y, int size) { - Ellipse2D.Double circle = new Ellipse2D.Double(x, y, size, size); - g.setClip(circle); - g.drawImage(image, x, y, size, size, null); + private void drawAvatar(Graphics2D g, BufferedImage src, int x, int y, int size) { + g.setColor(new Color(255, 145, 0, 45)); + g.fillOval(x - 10, y - 10, size + 20, size + 20); + Ellipse2D clip = new Ellipse2D.Double(x, y, size, size); + g.setClip(clip); + g.drawImage(src, x, y, size, size, null); g.setClip(null); + g.setStroke(new BasicStroke(4f)); + g.setColor(ACCENT); + g.drawOval(x, y, size, size); } - private Font loadFont(String path, float size) { - String key = path + "@" + size; - Font cached = FONT_CACHE.get(key); - if (cached != null) { - return cached; - } - try (InputStream is = getClass().getClassLoader().getResourceAsStream("assets/" + path)) { - if (is == null) { - Font fallback = new Font("Arial", Font.PLAIN, (int) size); - FONT_CACHE.put(key, fallback); - return fallback; - } - Font font = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(size); - FONT_CACHE.put(key, font); - return font; - } catch (FontFormatException | IOException e) { - Font fallback = new Font("Arial", Font.PLAIN, (int) size); - FONT_CACHE.put(key, fallback); - return fallback; - } + private void drawLevelTag(Graphics2D g, int level, int x, int y) { + String text = "LVL " + level; + g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 20f)); + FontMetrics fm = g.getFontMetrics(); + int tw = fm.stringWidth(text); + int th = fm.getAscent(); + int px = 14, py = 8; + g.setColor(ACCENT); + g.fillRoundRect(x, y, tw + px * 2, th + py * 2, 12, 12); + g.setColor(new Color(20, 10, 0)); + g.drawString(text, x + px, y + py + th - 2); } - private BufferedImage loadBackground(String path) throws Exception { - try (InputStream is = getClass().getClassLoader().getResourceAsStream("assets/" + path)) { - if (is == null) { - throw new IllegalArgumentException("Resource not found: assets/" + path); - } - return ImageIO.read(is); - } + private void drawRankBadge(Graphics2D g, int rank) { + int bx = W - PAD - 20 - 110; + int by = PAD + 22; + g.setColor(new Color(20, 20, 30, 200)); + g.fillRoundRect(bx, by, 110, 44, 14, 14); + g.setStroke(new BasicStroke(1.2f)); + g.setColor(ACCENT_DIM); + g.drawRoundRect(bx, by, 110, 44, 14, 14); + g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 14f)); + g.setColor(GREY); + g.drawString("РАНГ", bx + 14, by + 17); + g.setFont(renderer.loadFont("fonts/Inter-Bold.ttf", 26f)); + g.setColor(ACCENT); + g.drawString("#" + rank, bx + 14, by + 37); } } diff --git a/src/main/java/creator/loki/feature/profile/RankCardIcons.java b/src/main/java/creator/loki/feature/profile/RankCardIcons.java new file mode 100644 index 0000000..34f97ae --- /dev/null +++ b/src/main/java/creator/loki/feature/profile/RankCardIcons.java @@ -0,0 +1,58 @@ +package creator.loki.feature.profile; + +import java.awt.BasicStroke; +import java.awt.Graphics2D; +import java.awt.geom.GeneralPath; + +public final class RankCardIcons { + public enum StatIcon { + CLOCK, + MESSAGE, + STAR + } + + public static void draw(Graphics2D g, StatIcon icon, int cx, int cy, int r) { + g.setStroke(new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + switch (icon) { + case CLOCK -> drawClock(g, cx, cy, r); + case MESSAGE -> drawMessage(g, cx, cy, r); + case STAR -> drawStar(g, cx, cy, r); + } + } + + private static void drawClock(Graphics2D g, int cx, int cy, int r) { + g.drawOval(cx - r, cy - r, r * 2, r * 2); + g.drawLine(cx, cy, cx - r / 2, cy - r / 2); + g.drawLine(cx, cy, cx, cy - (int) (r * 0.8)); + g.fillOval(cx - 2, cy - 2, 4, 4); + } + + private static void drawMessage(Graphics2D g, int cx, int cy, int r) { + int bx = cx - r, by = cy - (int) (r * 0.85); + int bw = r * 2, bh = (int) (r * 1.5); + g.drawRoundRect(bx, by, bw, bh, 6, 6); + GeneralPath tail = new GeneralPath(); + tail.moveTo(cx - r / 2, by + bh); + tail.lineTo(cx - r / 2 - 4, by + bh + 7); + tail.lineTo(cx - r / 2 + 8, by + bh); + tail.closePath(); + g.fill(tail); + } + + private static void drawStar(Graphics2D g, int cx, int cy, int r) { + double inner = r * 0.42; + GeneralPath star = new GeneralPath(); + for (int i = 0; i < 10; i++) { + double angle = Math.PI / 5 * i - Math.PI / 2; + double rad = (i % 2 == 0) ? r : inner; + double px = cx + Math.cos(angle) * rad; + double py = cy + Math.sin(angle) * rad; + if (i == 0) star.moveTo(px, py); + else star.lineTo(px, py); + } + star.closePath(); + g.fill(star); + } + + private RankCardIcons() {} +} diff --git a/src/main/java/creator/loki/feature/profile/RankCardRenderer.java b/src/main/java/creator/loki/feature/profile/RankCardRenderer.java new file mode 100644 index 0000000..44e9c52 --- /dev/null +++ b/src/main/java/creator/loki/feature/profile/RankCardRenderer.java @@ -0,0 +1,118 @@ +package creator.loki.feature.profile; + +import static creator.loki.feature.profile.RankCardConstants.*; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.imageio.ImageIO; + +public class RankCardRenderer { + private static final Map FONT_CACHE = new ConcurrentHashMap<>(); + + public void drawXpBar(Graphics2D g, int x, int y, int w, int h, int xp, int maxXp) { + g.setColor(BAR_BG); + g.fillRoundRect(x, y, w, h, h, h); + float pct = maxXp > 0 ? Math.min(1f, (float) xp / maxXp) : 0f; + int fill = Math.max(h, (int) (w * pct)); + GradientPaint gp = new GradientPaint(x, y, new Color(255, 100, 0), x + fill, y, ACCENT); + g.setPaint(gp); + g.fillRoundRect(x, y, fill, h, h, h); + int dotD = h + 14; + int dotX = x + fill - dotD / 2; + int dotY = y - 7; + g.setColor(new Color(30, 20, 0)); + g.fillOval(dotX, dotY, dotD, dotD); + g.setColor(ACCENT); + g.setStroke(new BasicStroke(2.5f)); + g.drawOval(dotX, dotY, dotD, dotD); + g.setColor(WHITE); + g.fillOval(dotX + 5, dotY + 5, dotD - 10, dotD - 10); + } + + public void drawStatCard( + Graphics2D g, + String title, + String value, + RankCardIcons.StatIcon icon, + int x, + int y, + int w, + int h) { + g.setColor(DARK_CARD); + g.fillRoundRect(x, y, w, h, 20, 20); + g.setStroke(new BasicStroke(1f)); + g.setColor(new Color(255, 145, 0, 40)); + g.drawRoundRect(x, y, w, h, 20, 20); + int iconD = 48; + int iconX = x + 18; + int iconY = y + (h - iconD) / 2; + g.setColor(new Color(255, 145, 0, 30)); + g.fillOval(iconX, iconY, iconD, iconD); + g.setColor(ACCENT); + g.setStroke(new BasicStroke(1.5f)); + RankCardIcons.draw(g, icon, iconX + iconD / 2, iconY + iconD / 2, 13); + int textX = iconX + iconD + 16; + g.setFont(loadFont("fonts/Inter-Bold.ttf", 30f)); + g.setColor(WHITE); + g.drawString(value, textX, y + 46); + g.setFont(loadFont("fonts/Inter-Regular.ttf", 18f)); + g.setColor(GREY); + g.drawString(title, textX, y + 72); + } + + public Font loadFont(String path, float size) { + String key = path + "@" + size; + Font cached = FONT_CACHE.get(key); + if (cached != null) return cached; + try (InputStream is = getClass().getClassLoader().getResourceAsStream("assets/" + path)) { + if (is == null) { + Font fallback = new Font("SansSerif", Font.BOLD, (int) size); + FONT_CACHE.put(key, fallback); + return fallback; + } + Font font = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(size); + FONT_CACHE.put(key, font); + return font; + } catch (FontFormatException | IOException e) { + Font fallback = new Font("SansSerif", Font.BOLD, (int) size); + FONT_CACHE.put(key, fallback); + return fallback; + } + } + + public BufferedImage loadBackground(String path) throws Exception { + try (InputStream is = getClass().getClassLoader().getResourceAsStream("assets/" + path)) { + if (is == null) { + BufferedImage fallback = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB); + Graphics2D fg = fallback.createGraphics(); + fg.setColor(new Color(15, 15, 25)); + fg.fillRect(0, 0, W, H); + fg.dispose(); + return fallback; + } + return ImageIO.read(is); + } + } + + public static void applyHints(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint( + RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint( + RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint( + RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + } +} diff --git a/src/main/java/creator/loki/feature/radio/RadioManager.java b/src/main/java/creator/loki/feature/radio/RadioManager.java index 39616cb..aafbda0 100644 --- a/src/main/java/creator/loki/feature/radio/RadioManager.java +++ b/src/main/java/creator/loki/feature/radio/RadioManager.java @@ -2,10 +2,15 @@ package creator.loki.feature.radio; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import com.google.inject.Inject; +import com.google.inject.Singleton; import creator.loki.feature.music.core.LavalinkManager; import creator.loki.feature.music.manage.GuildMusicManager; +import creator.loki.feature.music.repository.QueueRepository; import creator.loki.util.LoggerDecorator; +import creator.loki.util.VoiceHelper; import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler; import dev.arbjerg.lavalink.client.Link; import dev.arbjerg.lavalink.client.player.TrackLoaded; @@ -14,23 +19,31 @@ import net.dv8tion.jda.api.entities.Guild; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@Singleton public class RadioManager { private static final Logger logger = LoggerFactory.getLogger(RadioManager.class); + private static final long VOICE_TIMEOUT_MS = 5000; private final Map radioStates = new ConcurrentHashMap<>(); + private final QueueRepository queueRepository; private LavalinkManager lavalinkManager; private JDA radioJda; - public void setLavalinkManager(LavalinkManager lm) { - this.lavalinkManager = lm; + @Inject + public RadioManager(QueueRepository queueRepository) { + this.queueRepository = queueRepository; } - public void setRadioJda(JDA radioJda) { - this.radioJda = radioJda; - } + public void setLavalinkManager(LavalinkManager lm) { this.lavalinkManager = lm; } + + public void setRadioJda(JDA radioJda) { this.radioJda = radioJda; } public void setRadio(long guildId, String streamUrl) { - radioStates.computeIfAbsent(guildId, id -> new GuildRadioState()).streamUrl = streamUrl; + radioStates.compute(guildId, (id, state) -> { + if (state == null) state = new GuildRadioState(); + state.streamUrl = streamUrl; + return state; + }); LoggerDecorator.music(logger, "Radio URL set for guild " + guildId + ": " + streamUrl); } @@ -43,86 +56,65 @@ public class RadioManager { GuildRadioState state = radioStates.get(guildId); if (state == null || state.streamUrl == null || state.streamUrl.isBlank()) return; if (radioJda == null || lavalinkManager == null) return; - + if (isRadioActive(guildId) && state.musicManager != null) { + stopRadio(guildId); + } + var finalState = radioStates.computeIfAbsent(guildId, id -> new GuildRadioState()); + finalState.streamUrl = getRadioUrl(guildId); Guild guild = radioJda.getGuildById(guildId); if (guild == null) return; - - guild.getJDA().getDirectAudioController().connect( - guild.getVoiceChannelById(channelId)); - - for (int i = 0; i < 10; i++) { - if (guild.getMember(guild.getJDA().getSelfUser()).getVoiceState().inAudioChannel()) break; - try { - Thread.sleep(200); - } catch (InterruptedException e) { - return; - } + guild.getJDA().getDirectAudioController().connect(guild.getVoiceChannelById(channelId)); + var self = guild.getMember(guild.getJDA().getSelfUser()); + if (!VoiceHelper.waitForCondition(() -> self.getVoiceState().inAudioChannel(), VOICE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + LoggerDecorator.warn(logger, "Radio: voice connection timeout for guild " + guildId); + return; } - var link = lavalinkManager.getOrCreateLink(guildId); - for (int i = 0; i < 10; i++) { - if (link.getCachedPlayer() != null) break; - try { - Thread.sleep(200); - } catch (InterruptedException e) { - return; - } + if (!VoiceHelper.waitForCondition(() -> link.getCachedPlayer() != null, VOICE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + LoggerDecorator.warn(logger, "Radio: Lavalink player timeout for guild " + guildId); + return; } - - var mm = new GuildMusicManager(guildId, lavalinkManager, null); - state.musicManager = mm; - - lavalinkManager - .getOrCreateLink(guildId) - .loadItem(state.streamUrl) - .subscribe( - new AbstractAudioLoadResultHandler() { - @Override - public void ontrackLoaded(TrackLoaded r) { - mm.getScheduler().enqueue(r.getTrack()); - LoggerDecorator.music(logger, "Radio started in guild " + guildId); - } - - @Override - public void onPlaylistLoaded( - dev.arbjerg.lavalink.client.player.PlaylistLoaded r) { - if (!r.getTracks().isEmpty()) { - mm.getScheduler().enqueue(r.getTracks().get(0)); - LoggerDecorator.music(logger, "Radio started in guild " + guildId); - } - } - - @Override - public void onSearchResultLoaded( - dev.arbjerg.lavalink.client.player.SearchResult r) { - if (!r.getTracks().isEmpty()) { - mm.getScheduler().enqueue(r.getTracks().get(0)); - LoggerDecorator.music(logger, "Radio started in guild " + guildId); - } - } - - @Override - public void noMatches() { - LoggerDecorator.warn(logger, "No radio stream: " + state.streamUrl); - } - - @Override - public void loadFailed( - dev.arbjerg.lavalink.client.player.LoadFailed e) { - LoggerDecorator.warn(logger, "Failed to load radio: " - + (e.getException().getMessage() != null - ? e.getException().getMessage() : "unknown")); - } - }); + var mm = new GuildMusicManager(guildId, lavalinkManager, queueRepository); + int vol = finalState.volume; + var url = finalState.streamUrl; + finalState.musicManager = mm; + lavalinkManager.getOrCreateLink(guildId).loadItem(url) + .subscribe(new AbstractAudioLoadResultHandler() { + @Override + public void ontrackLoaded(TrackLoaded r) { + if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe(); + mm.getScheduler().enqueue(r.getTrack()); + LoggerDecorator.music(logger, "Radio started in guild " + guildId); + } + @Override + public void onPlaylistLoaded(dev.arbjerg.lavalink.client.player.PlaylistLoaded r) { + if (!r.getTracks().isEmpty()) { + if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe(); + mm.getScheduler().enqueue(r.getTracks().get(0)); + LoggerDecorator.music(logger, "Radio started in guild " + guildId); + } + } + @Override + public void onSearchResultLoaded(dev.arbjerg.lavalink.client.player.SearchResult r) { + if (!r.getTracks().isEmpty()) { + if (vol != 100) link.createOrUpdatePlayer().setVolume(vol).subscribe(); + mm.getScheduler().enqueue(r.getTracks().get(0)); + LoggerDecorator.music(logger, "Radio started in guild " + guildId); + } + } + @Override + public void noMatches() { LoggerDecorator.warn(logger, "No radio stream: " + url); } + @Override + public void loadFailed(dev.arbjerg.lavalink.client.player.LoadFailed e) { + LoggerDecorator.warn(logger, "Failed to load radio: " + (e.getException().getMessage() != null ? e.getException().getMessage() : "unknown")); + } + }); } public void stopRadio(long guildId) { GuildRadioState state = radioStates.remove(guildId); if (state == null) return; - if (state.musicManager != null) { - state.musicManager.stop(); - state.musicManager.destroy(); - } + if (state.musicManager != null) { state.musicManager.stop(); state.musicManager.destroy(); } if (lavalinkManager != null) { Link link = lavalinkManager.getClient().getLinkIfCached(guildId); if (link != null) link.destroy().subscribe(); @@ -135,16 +127,13 @@ public class RadioManager { } public boolean isRadioActive(long guildId) { - return radioStates.containsKey(guildId); + GuildRadioState state = radioStates.get(guildId); + return state != null && state.streamUrl != null; } - public boolean hasRadioBot() { - return radioJda != null; - } + public boolean hasRadioBot() { return radioJda != null; } - public JDA getRadioJda() { - return radioJda; - } + public JDA getRadioJda() { return radioJda; } public int getVolume(long guildId) { GuildRadioState state = radioStates.get(guildId); @@ -152,13 +141,13 @@ public class RadioManager { } public void setVolume(long guildId, int volume) { - GuildRadioState state = radioStates.computeIfAbsent(guildId, id -> new GuildRadioState()); + GuildRadioState state = radioStates.get(guildId); + if (state == null) return; state.volume = Math.max(0, Math.min(100, volume)); if (lavalinkManager != null) { var link = lavalinkManager.getClient().getLinkIfCached(guildId); if (link != null && link.getCachedPlayer() != null) { - link.createOrUpdatePlayer() - .setVolume(state.volume) + link.createOrUpdatePlayer().setVolume(state.volume) .subscribe(null, err -> logger.warn("Lavalink: {}", err.getMessage())); } } diff --git a/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java b/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java index 7a89684..27b26d1 100644 --- a/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java +++ b/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java @@ -8,15 +8,16 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.stream.Collectors; import javax.imageio.ImageIO; import creator.loki.feature.counter.DailyStats; import creator.loki.feature.counter.repository.CounterStatsRepository; +import net.dv8tion.jda.api.OnlineStatus; import net.dv8tion.jda.api.entities.Guild; public class ServerStatsService { + private static final int CHART_WIDTH = 800; private static final int CHART_HEIGHT = 400; private static final int MARGIN = 60; @@ -38,62 +39,56 @@ public class ServerStatsService { } public byte[] generateChart(List history) throws Exception { - BufferedImage image = new BufferedImage(CHART_WIDTH, CHART_HEIGHT, BufferedImage.TYPE_INT_RGB); - Graphics2D g = image.createGraphics(); - + var image = new BufferedImage(CHART_WIDTH, CHART_HEIGHT, BufferedImage.TYPE_INT_RGB); + var g = image.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - g.setColor(COLOR_BG); g.fillRect(0, 0, CHART_WIDTH, CHART_HEIGHT); - - int maxVal = + var maxVal = history.stream().mapToInt(s -> Math.max(s.getJoined(), s.getLeft())).max().orElse(1); - - int plotW = CHART_WIDTH - 2 * MARGIN; - int plotH = CHART_HEIGHT - 2 * MARGIN; - int barW = Math.max(4, plotW / (history.size() * 2 + 1)); - + var plotW = CHART_WIDTH - 2 * MARGIN; + var plotH = CHART_HEIGHT - 2 * MARGIN; + var barW = Math.max(4, plotW / (history.size() * 2 + 1)); g.setColor(COLOR_GRID); - for (int i = 0; i <= 4; i++) { - int y = MARGIN + plotH - (plotH * i / 4); + for (var i = 0; i <= 4; i++) { + var y = MARGIN + plotH - (plotH * i / 4); g.drawLine(MARGIN, y, CHART_WIDTH - MARGIN, y); } - - Font labelFont = new Font("Arial", Font.PLAIN, 11); - Font titleFont = new Font("Arial", Font.BOLD, 16); - - g.setColor(COLOR_TEXT); - g.setFont(titleFont); - g.drawString("Активность сервера (присоединились / вышли)", 160, 30); - - g.setFont(labelFont); - g.setColor(COLOR_TEXT); - for (int i = 0; i <= 4; i++) { - int y = MARGIN + plotH - (plotH * i / 4); - String label = String.valueOf(maxVal * i / 4); - g.drawString(label, 5, y + 4); - } - - int maxBars = Math.min(history.size(), 30); + drawLabels(g, plotH, maxVal); + var maxBars = Math.min(history.size(), 30); history = history.subList(history.size() - maxBars, history.size()); + drawData(g, history, maxVal, plotH, barW); + drawLegend(g); + g.dispose(); + var baos = new ByteArrayOutputStream(); + ImageIO.write(image, "PNG", baos); + return baos.toByteArray(); + } - int step = Math.max(1, history.size() / 15); - - for (int i = 0; i < history.size(); i++) { - DailyStats stats = history.get(i); - int x = MARGIN + i * (barW * 2 + 2); - - int joinedH = (int) ((double) stats.getJoined() / maxVal * plotH); - int leftH = (int) ((double) stats.getLeft() / maxVal * plotH); + private void drawLabels(Graphics2D g, int plotH, int maxVal) { + g.setColor(COLOR_TEXT); + g.setFont(new Font("Arial", Font.BOLD, 16)); + g.drawString("Активность сервера (присоединились / вышли)", 160, 30); + g.setFont(new Font("Arial", Font.PLAIN, 11)); + for (var i = 0; i <= 4; i++) { + var y = MARGIN + plotH - (plotH * i / 4); + g.drawString(String.valueOf(maxVal * i / 4), 5, y + 4); + } + } + private void drawData(Graphics2D g, List history, int maxVal, int plotH, int barW) { + var step = Math.max(1, history.size() / 15); + for (var i = 0; i < history.size(); i++) { + var stats = history.get(i); + var x = MARGIN + i * (barW * 2 + 2); + var joinedH = (int) ((double) stats.getJoined() / maxVal * plotH); + var leftH = (int) ((double) stats.getLeft() / maxVal * plotH); g.setColor(COLOR_JOINED); g.fillRect(x, MARGIN + plotH - joinedH, barW, joinedH); - g.setColor(COLOR_LEFT); g.fillRect(x + barW + 2, MARGIN + plotH - leftH, barW, leftH); - if (i % step == 0) { g.setColor(COLOR_TEXT); g.rotate(-Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15); @@ -101,60 +96,48 @@ public class ServerStatsService { g.rotate(Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15); } } + } + private void drawLegend(Graphics2D g) { g.setFont(new Font("Arial", Font.PLAIN, 12)); g.setColor(COLOR_JOINED); g.fillRect(CHART_WIDTH - 170, 10, 12, 12); g.setColor(COLOR_TEXT); g.drawString("Присоединились", CHART_WIDTH - 152, 21); - g.setColor(COLOR_LEFT); g.fillRect(CHART_WIDTH - 60, 10, 12, 12); g.setColor(COLOR_TEXT); g.drawString("Вышли", CHART_WIDTH - 42, 21); - - g.dispose(); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ImageIO.write(image, "PNG", baos); - return baos.toByteArray(); } public byte[] generateCsv(Guild guild, List history) { - String header = + var members = guild.getMembers(); + var owner = guild.getOwner(); + var online = members.stream().filter(m -> m.getOnlineStatus() == OnlineStatus.ONLINE).count(); + var bots = members.stream().filter(m -> m.getUser().isBot()).count(); + var info = new StringBuilder(); + info.append(escapeCsv(guild.getName())).append(","); + info.append(guild.getId()).append(","); + info.append(owner != null ? escapeCsv(owner.getUser().getGlobalName()) : "").append(","); + info.append(guild.getTimeCreated().format(DateTimeFormatter.ISO_DATE)).append(","); + info.append(guild.getMemberCount()).append(","); + info.append(online).append(","); + info.append(bots).append(","); + info.append(guild.getTextChannels().size()).append(","); + info.append(guild.getVoiceChannels().size()).append(","); + info.append(guild.getRoles().size()).append("\n"); + var dailyRows = new StringBuilder(); + for (var s : history) { + dailyRows.append( + s.getDate() + "," + s.getJoined() + "," + s.getLeft() + "," + s.getDifference() + "\n"); + } + var totalJoined = history.stream().mapToInt(DailyStats::getJoined).sum(); + var totalLeft = history.stream().mapToInt(DailyStats::getLeft).sum(); + var totals = String.format("%ntotal,%d,%d,%d", totalJoined, totalLeft, totalJoined - totalLeft); + var header = "server_name,server_id,owner,created_at,total_members,online,bots,text_channels,voice_channels,roles\n"; - String info = - String.format( - "%s,%s,%s,%s,%d,%d,%d,%d,%d,%d%n", - escapeCsv(guild.getName()), - guild.getId(), - guild.getOwner() != null ? escapeCsv(guild.getOwner().getUser().getGlobalName()) : "", - guild.getTimeCreated().format(DateTimeFormatter.ISO_DATE), - guild.getMemberCount(), - guild.getMembers().stream() - .filter(m -> m.getOnlineStatus() == net.dv8tion.jda.api.OnlineStatus.ONLINE) - .count(), - guild.getMembers().stream().filter(m -> m.getUser().isBot()).count(), - guild.getTextChannels().size(), - guild.getVoiceChannels().size(), - guild.getRoles().size()); - - String dailyHeader = "\ndate,joined,left,difference\n"; - String dailyRows = - history.stream() - .map( - s -> - String.format( - "%s,%d,%d,%d", s.getDate(), s.getJoined(), s.getLeft(), s.getDifference())) - .collect(Collectors.joining("\n")); - - int totalJoined = history.stream().mapToInt(DailyStats::getJoined).sum(); - int totalLeft = history.stream().mapToInt(DailyStats::getLeft).sum(); - String totals = - String.format("%ntotal,%d,%d,%d", totalJoined, totalLeft, totalJoined - totalLeft); - - String csv = header + info + dailyHeader + dailyRows + totals; - return csv.getBytes(java.nio.charset.StandardCharsets.UTF_8); + return (header + info + "\ndate,joined,left,difference\n" + dailyRows + totals) + .getBytes(java.nio.charset.StandardCharsets.UTF_8); } private static String escapeCsv(String value) { diff --git a/src/main/java/creator/loki/feature/social/commands/CuddleCommand.java b/src/main/java/creator/loki/feature/social/commands/CuddleCommand.java deleted file mode 100644 index 2f4ebfb..0000000 --- a/src/main/java/creator/loki/feature/social/commands/CuddleCommand.java +++ /dev/null @@ -1,84 +0,0 @@ -package creator.loki.feature.social.commands; - -import java.awt.Color; -import java.util.List; - -import creator.loki.command.Command; -import creator.loki.command.CommandCategories; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import creator.loki.integration.nekos.NekosLifeApi; -import net.dv8tion.jda.api.entities.Member; - -/** Обниматься с пользователем с GIF */ -public class CuddleCommand implements Command { - - private final CooldownManager cooldownManager; - - public CuddleCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "cuddle"; - } - - @Override - public String getDescription() { - return "Обниматься с пользователем"; - } - - @Override - public String getUsage() { - return "cuddle <@user>"; - } - - @Override - public String getCategory() { - return CommandCategories.GIF_INTERACTIVE; - } - - @Override - public void execute(CommandContext context) { - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "cuddle")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "cuddle"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - if (mentioned.isEmpty()) { - context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); - return; - } - - Member target = mentioned.get(0); - - cooldownManager.applyCooldown(context.getAuthor().getId(), "cuddle", 5); - - String gifUrl = NekosLifeApi.fetchImage("cuddle"); - - if (gifUrl == null) { - context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); - return; - } - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withDescription( - "🥰 " - + context.getAuthor().getAsMention() - + " обнимается с " - + target.getAsMention() - + "! 💕") - .withImage(gifUrl) - .withColor(new Color(255, 0, 255)) - .withFooter("Powered by nekos.life") - .build()) - .queue(); - } -} diff --git a/src/main/java/creator/loki/feature/social/commands/FeedCommand.java b/src/main/java/creator/loki/feature/social/commands/FeedCommand.java deleted file mode 100644 index c32ae74..0000000 --- a/src/main/java/creator/loki/feature/social/commands/FeedCommand.java +++ /dev/null @@ -1,84 +0,0 @@ -package creator.loki.feature.social.commands; - -import java.awt.Color; -import java.util.List; - -import creator.loki.command.Command; -import creator.loki.command.CommandCategories; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import creator.loki.integration.nekos.NekosLifeApi; -import net.dv8tion.jda.api.entities.Member; - -/** Покормить пользователя с GIF */ -public class FeedCommand implements Command { - - private final CooldownManager cooldownManager; - - public FeedCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "feed"; - } - - @Override - public String getDescription() { - return "Покормить пользователя"; - } - - @Override - public String getUsage() { - return "feed <@user>"; - } - - @Override - public String getCategory() { - return CommandCategories.GIF_INTERACTIVE; - } - - @Override - public void execute(CommandContext context) { - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "feed")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "feed"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - if (mentioned.isEmpty()) { - context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); - return; - } - - Member target = mentioned.get(0); - - cooldownManager.applyCooldown(context.getAuthor().getId(), "feed", 5); - - String gifUrl = NekosLifeApi.fetchImage("feed"); - - if (gifUrl == null) { - context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); - return; - } - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withDescription( - "🍽️ " - + context.getAuthor().getAsMention() - + " кормит " - + target.getAsMention() - + "! 😋") - .withImage(gifUrl) - .withColor(new Color(255, 200, 100)) - .withFooter("Powered by nekos.life") - .build()) - .queue(); - } -} diff --git a/src/main/java/creator/loki/feature/social/commands/HugCommand.java b/src/main/java/creator/loki/feature/social/commands/HugCommand.java deleted file mode 100644 index ab9278c..0000000 --- a/src/main/java/creator/loki/feature/social/commands/HugCommand.java +++ /dev/null @@ -1,69 +0,0 @@ -package creator.loki.feature.social.commands; - -import java.awt.Color; -import java.util.List; - -import creator.loki.command.Command; -import creator.loki.command.CommandCategories; -import creator.loki.command.CommandContext; -import creator.loki.embed.EmbedFactory; -import creator.loki.integration.nekos.NekosLifeApi; -import net.dv8tion.jda.api.entities.Member; - -public class HugCommand implements Command { - - @Override - public String getName() { - return "hug"; - } - - @Override - public String getDescription() { - return "Обнять пользователя"; - } - - @Override - public String getUsage() { - return "hug <@user>"; - } - - @Override - public String getCategory() { - return CommandCategories.GIF_INTERACTIVE; - } - - @Override - public void execute(CommandContext context) { - List mentioned = context.getMessage().getMentions().getMembers(); - if (mentioned.isEmpty()) { - context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); - return; - } - - Member target = mentioned.get(0); - - // Получаем GIF из nekos.life API - String gifUrl = NekosLifeApi.fetchImage("hug"); - - if (gifUrl == null) { - context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); - return; - } - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withDescription( - "🤗 " - + context.getAuthor().getAsMention() - + " обнял " - + target.getAsMention() - + " 💕") - .withImage(gifUrl) - .withColor(new Color(255, 182, 193)) - .withFooter("Powered by nekos.life") - .build()) - .queue(); - } -} diff --git a/src/main/java/creator/loki/feature/social/commands/KissCommand.java b/src/main/java/creator/loki/feature/social/commands/KissCommand.java deleted file mode 100644 index 35437f0..0000000 --- a/src/main/java/creator/loki/feature/social/commands/KissCommand.java +++ /dev/null @@ -1,101 +0,0 @@ -package creator.loki.feature.social.commands; - -import java.awt.Color; -import java.util.List; - -import creator.loki.command.Command; -import creator.loki.command.CommandCategories; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import creator.loki.integration.nekos.NekosLifeApi; -import net.dv8tion.jda.api.entities.Member; - -/** Поцелуй пользователя с GIF */ -public class KissCommand implements Command { - - private final CooldownManager cooldownManager; - - public KissCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "kiss"; - } - - @Override - public String getDescription() { - return "Поцеловать пользователя"; - } - - @Override - public String getUsage() { - return "kiss <@user>"; - } - - @Override - public String getCategory() { - return CommandCategories.GIF_INTERACTIVE; - } - - @Override - public void execute(CommandContext context) { - // Проверка cooldown (5 секунд) - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "kiss")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "kiss"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - - if (mentioned.isEmpty()) { - context - .getChannel() - .sendMessage("❌ Укажи кого поцеловать! Использование: `!kiss @user`") - .queue(); - return; - } - - Member target = mentioned.get(0); - - if (target.getId().equals(context.getAuthor().getId())) { - context.getChannel().sendMessage("❌ Нельзя целовать самого себя!").queue(); - return; - } - - if (target.getUser().isBot()) { - context.getChannel().sendMessage("❌ Боты не умеют целоваться!").queue(); - return; - } - - // Применяем cooldown - cooldownManager.applyCooldown(context.getAuthor().getId(), "kiss", 5); - - // Получаем GIF из nekos.life API - String gifUrl = NekosLifeApi.fetchImage("kiss"); - - if (gifUrl == null) { - context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); - return; - } - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withDescription( - "💋 " - + context.getAuthor().getAsMention() - + " целует " - + target.getAsMention() - + " ❤️") - .withImage(gifUrl) - .withColor(new Color(255, 105, 180)) - .withFooter("Powered by nekos.life") - .build()) - .queue(); - } -} diff --git a/src/main/java/creator/loki/feature/social/commands/PokeCommand.java b/src/main/java/creator/loki/feature/social/commands/PokeCommand.java deleted file mode 100644 index c13004f..0000000 --- a/src/main/java/creator/loki/feature/social/commands/PokeCommand.java +++ /dev/null @@ -1,84 +0,0 @@ -package creator.loki.feature.social.commands; - -import java.awt.Color; -import java.util.List; - -import creator.loki.command.Command; -import creator.loki.command.CommandCategories; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import creator.loki.integration.nekos.NekosLifeApi; -import net.dv8tion.jda.api.entities.Member; - -/** Тыкнуть пользователя с GIF */ -public class PokeCommand implements Command { - - private final CooldownManager cooldownManager; - - public PokeCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "poke"; - } - - @Override - public String getDescription() { - return "Тыкнуть пользователя"; - } - - @Override - public String getUsage() { - return "poke <@user>"; - } - - @Override - public String getCategory() { - return CommandCategories.GIF_INTERACTIVE; - } - - @Override - public void execute(CommandContext context) { - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "poke")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "poke"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - if (mentioned.isEmpty()) { - context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); - return; - } - - Member target = mentioned.get(0); - - cooldownManager.applyCooldown(context.getAuthor().getId(), "poke", 5); - - String gifUrl = NekosLifeApi.fetchImage("poke"); - - if (gifUrl == null) { - context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); - return; - } - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withDescription( - "👉 " - + context.getAuthor().getAsMention() - + " тыкает в " - + target.getAsMention() - + "! 😏") - .withImage(gifUrl) - .withColor(new Color(0, 128, 128)) - .withFooter("Powered by nekos.life") - .build()) - .queue(); - } -} diff --git a/src/main/java/creator/loki/feature/social/commands/SlapCommand.java b/src/main/java/creator/loki/feature/social/commands/SlapCommand.java deleted file mode 100644 index 68eed5d..0000000 --- a/src/main/java/creator/loki/feature/social/commands/SlapCommand.java +++ /dev/null @@ -1,84 +0,0 @@ -package creator.loki.feature.social.commands; - -import java.awt.Color; -import java.util.List; - -import creator.loki.command.Command; -import creator.loki.command.CommandCategories; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import creator.loki.integration.nekos.NekosLifeApi; -import net.dv8tion.jda.api.entities.Member; - -/** Шлепнуть пользователя с GIF */ -public class SlapCommand implements Command { - - private final CooldownManager cooldownManager; - - public SlapCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "slap"; - } - - @Override - public String getDescription() { - return "Шлепнуть пользователя"; - } - - @Override - public String getUsage() { - return "slap <@user>"; - } - - @Override - public String getCategory() { - return CommandCategories.GIF_INTERACTIVE; - } - - @Override - public void execute(CommandContext context) { - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "slap")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "slap"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - if (mentioned.isEmpty()) { - context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); - return; - } - - Member target = mentioned.get(0); - - cooldownManager.applyCooldown(context.getAuthor().getId(), "slap", 5); - - String gifUrl = NekosLifeApi.fetchImage("slap"); - - if (gifUrl == null) { - context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); - return; - } - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withDescription( - "👋 " - + context.getAuthor().getAsMention() - + " шлепает " - + target.getAsMention() - + "! 😠") - .withImage(gifUrl) - .withColor(new Color(255, 165, 0)) - .withFooter("Powered by nekos.life") - .build()) - .queue(); - } -} diff --git a/src/main/java/creator/loki/feature/social/commands/SocialCommand.java b/src/main/java/creator/loki/feature/social/commands/SocialCommand.java new file mode 100644 index 0000000..29ced62 --- /dev/null +++ b/src/main/java/creator/loki/feature/social/commands/SocialCommand.java @@ -0,0 +1,140 @@ +package creator.loki.feature.social.commands; + +import java.awt.Color; +import java.util.List; + +import creator.loki.command.Command; +import creator.loki.command.CommandCategories; +import creator.loki.command.CommandContext; +import creator.loki.core.security.CooldownManager; +import creator.loki.embed.EmbedFactory; +import creator.loki.integration.nekos.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +public class SocialCommand implements Command { + + public enum Flavor { + HUG("hug", "Обнять пользователя", "🤗", "обнял", new Color(255, 182, 193), false, false), + KISS("kiss", "Поцеловать пользователя", "💋", "целует", new Color(255, 105, 180), true, true), + SLAP("slap", "Шлепнуть пользователя", "👋", "шлепает", new Color(255, 165, 0), false, false), + POKE("poke", "Тыкнуть пользователя", "👉", "тыкает в", new Color(0, 128, 128), false, false), + TICKLE( + "tickle", "Пощекотать пользователя", "🤗", "щекочет", new Color(0, 255, 0), false, false), + CUDDLE( + "cuddle", + "Обниматься с пользователем", + "🥰", + "обнимается с", + new Color(255, 0, 255), + false, + false), + FEED("feed", "Покормить пользователя", "🍽️", "кормит", new Color(255, 200, 100), false, false); + + final String name; + final String description; + final String emoji; + final String verb; + final Color color; + final boolean checkSelf; + final boolean checkBot; + + Flavor( + String name, + String description, + String emoji, + String verb, + Color color, + boolean checkSelf, + boolean checkBot) { + this.name = name; + this.description = description; + this.emoji = emoji; + this.verb = verb; + this.color = color; + this.checkSelf = checkSelf; + this.checkBot = checkBot; + } + } + + private final Flavor flavor; + private final CooldownManager cooldownManager; + + public SocialCommand(Flavor flavor, CooldownManager cooldownManager) { + this.flavor = flavor; + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return flavor.name; + } + + @Override + public String getDescription() { + return flavor.description; + } + + @Override + public String getUsage() { + return flavor.name + " <@user>"; + } + + @Override + public String getCategory() { + return CommandCategories.GIF_INTERACTIVE; + } + + @Override + public void execute(CommandContext context) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), flavor.name)) { + long remaining = + cooldownManager.getRemainingSeconds(context.getAuthor().getId(), flavor.name); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + if (flavor.checkSelf && target.getId().equals(context.getAuthor().getId())) { + context.getChannel().sendMessage("❌ Нельзя использовать на себе!").queue(); + return; + } + + if (flavor.checkBot && target.getUser().isBot()) { + context.getChannel().sendMessage("❌ Боты не поддерживают это действие!").queue(); + return; + } + + cooldownManager.applyCooldown(context.getAuthor().getId(), flavor.name, 5); + + String gifUrl = NekosLifeApi.fetchImage(flavor.name); + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context + .getChannel() + .sendMessageEmbeds( + EmbedFactory.create() + .withDescription( + flavor.emoji + + " " + + context.getAuthor().getAsMention() + + " " + + flavor.verb + + " " + + target.getAsMention()) + .withImage(gifUrl) + .withColor(flavor.color) + .withFooter("Powered by nekos.life") + .build()) + .queue(); + } +} diff --git a/src/main/java/creator/loki/feature/social/commands/TickleCommand.java b/src/main/java/creator/loki/feature/social/commands/TickleCommand.java deleted file mode 100644 index 77b5fe6..0000000 --- a/src/main/java/creator/loki/feature/social/commands/TickleCommand.java +++ /dev/null @@ -1,84 +0,0 @@ -package creator.loki.feature.social.commands; - -import java.awt.Color; -import java.util.List; - -import creator.loki.command.Command; -import creator.loki.command.CommandCategories; -import creator.loki.command.CommandContext; -import creator.loki.core.security.CooldownManager; -import creator.loki.embed.EmbedFactory; -import creator.loki.integration.nekos.NekosLifeApi; -import net.dv8tion.jda.api.entities.Member; - -/** Пощекотать пользователя с GIF */ -public class TickleCommand implements Command { - - private final CooldownManager cooldownManager; - - public TickleCommand(CooldownManager cooldownManager) { - this.cooldownManager = cooldownManager; - } - - @Override - public String getName() { - return "tickle"; - } - - @Override - public String getDescription() { - return "Пощекотать пользователя"; - } - - @Override - public String getUsage() { - return "tickle <@user>"; - } - - @Override - public String getCategory() { - return CommandCategories.GIF_INTERACTIVE; - } - - @Override - public void execute(CommandContext context) { - if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "tickle")) { - long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "tickle"); - context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); - return; - } - - List mentioned = context.getMessage().getMentions().getMembers(); - if (mentioned.isEmpty()) { - context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); - return; - } - - Member target = mentioned.get(0); - - cooldownManager.applyCooldown(context.getAuthor().getId(), "tickle", 5); - - String gifUrl = NekosLifeApi.fetchImage("tickle"); - - if (gifUrl == null) { - context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); - return; - } - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withDescription( - "🤗 " - + context.getAuthor().getAsMention() - + " щекочет " - + target.getAsMention() - + "!") - .withImage(gifUrl) - .withColor(new Color(0, 255, 0)) - .withFooter("Powered by nekos.life") - .build()) - .queue(); - } -} diff --git a/src/main/java/creator/loki/feature/voice/VoiceButtonHandler.java b/src/main/java/creator/loki/feature/voice/VoiceButtonHandler.java index 82617e0..01a90ba 100644 --- a/src/main/java/creator/loki/feature/voice/VoiceButtonHandler.java +++ b/src/main/java/creator/loki/feature/voice/VoiceButtonHandler.java @@ -4,13 +4,13 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import creator.loki.util.LoggerDecorator; import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.components.label.Label; +import net.dv8tion.jda.api.components.textinput.TextInput; +import net.dv8tion.jda.api.components.textinput.TextInputStyle; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; -import net.dv8tion.jda.api.components.label.Label; -import net.dv8tion.jda.api.components.textinput.TextInput; -import net.dv8tion.jda.api.components.textinput.TextInputStyle; import net.dv8tion.jda.api.modals.Modal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/creator/loki/feature/voice/VoiceChannelManager.java b/src/main/java/creator/loki/feature/voice/VoiceChannelManager.java index 2acba3d..192eaca 100644 --- a/src/main/java/creator/loki/feature/voice/VoiceChannelManager.java +++ b/src/main/java/creator/loki/feature/voice/VoiceChannelManager.java @@ -9,19 +9,22 @@ import creator.loki.feature.voice.repository.VoicePresetRepository; import creator.loki.util.LoggerDecorator; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.components.actionrow.ActionRow; +import net.dv8tion.jda.api.components.buttons.Button; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; -import net.dv8tion.jda.api.components.actionrow.ActionRow; -import net.dv8tion.jda.api.components.buttons.Button; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class VoiceChannelManager { private static final Logger logger = LoggerFactory.getLogger(VoiceChannelManager.class); + private static final EnumSet OWNER_PERMS = + EnumSet.of( + Permission.MANAGE_CHANNEL, Permission.VOICE_CONNECT, + Permission.VOICE_MOVE_OTHERS, Permission.VOICE_MUTE_OTHERS); private final VoiceChannelRepository channelRepo; private final VoicePresetRepository presetRepo; - private String triggerChannelId; private String categoryId; @@ -41,56 +44,36 @@ public class VoiceChannelManager { public VoiceChannel createPersonalChannel(Member member, Guild guild) { try { - String channelName = "Канал: " + member.getEffectiveName(); - - VoiceChannel voiceChannel = - guild - .createVoiceChannel(channelName) - .setParent(guild.getCategoryById(categoryId)) - .addPermissionOverride( - member, - EnumSet.of( - Permission.MANAGE_CHANNEL, - Permission.VOICE_CONNECT, - Permission.VOICE_MOVE_OTHERS, - Permission.VOICE_MUTE_OTHERS), - null) - .complete(); - - channelRepo.saveChannelData(new VoiceChannelData(voiceChannel.getId(), member.getId())); - + var channel = buildPersonalChannel(member, guild); + channelRepo.saveChannelData(new VoiceChannelData(channel.getId(), member.getId())); LoggerDecorator.voice( logger, - "Создан личный канал " - + voiceChannel.getId() - + " для пользователя " - + member.getEffectiveName()); - - applyUserPreset(voiceChannel, member.getId()); - - return voiceChannel; + "Создан личный канал %s для пользователя %s" + .formatted(channel.getId(), member.getEffectiveName())); + applyUserPreset(channel, member.getId()); + return channel; } catch (Exception e) { LoggerDecorator.error(logger, "Ошибка при создании личного канала", e); return null; } } + private VoiceChannel buildPersonalChannel(Member member, Guild guild) { + return guild + .createVoiceChannel("Канал: " + member.getEffectiveName()) + .setParent(guild.getCategoryById(categoryId)) + .addPermissionOverride(member, OWNER_PERMS, null) + .complete(); + } + public void deletePersonalChannel(String channelId, Guild guild) { + channelRepo.deleteChannelData(channelId); try { - VoiceChannel channel = guild.getVoiceChannelById(channelId); + var channel = guild.getVoiceChannelById(channelId); if (channel != null) { channel .delete() - .queue( - success -> { - channelRepo.deleteChannelData(channelId); - LoggerDecorator.voice(logger, "Удален личный канал " + channelId); - }, - error -> - LoggerDecorator.error( - logger, "Ошибка при удалении канала " + channelId, error)); - } else { - channelRepo.deleteChannelData(channelId); + .queue(success -> LoggerDecorator.voice(logger, "Удален личный канал " + channelId)); } } catch (Exception e) { LoggerDecorator.error(logger, "Ошибка при удалении личного канала", e); @@ -110,43 +93,32 @@ public class VoiceChannelManager { } public void updatePresetSetting(String userId, String setting, Object value) { - VoicePreset preset = getUserPreset(userId); - switch (setting) { - case "name" -> preset.setName((String) value); - case "limit" -> preset.setLimit((Integer) value); - case "bitrate" -> preset.setBitrate((Integer) value); - case "locked" -> preset.setLocked((Boolean) value); - default -> { - /* ignore */ - } - } + var preset = getUserPreset(userId); + if ("name".equals(setting)) preset.setName((String) value); + else if ("limit".equals(setting)) preset.setLimit((Integer) value); + else if ("bitrate".equals(setting)) preset.setBitrate((Integer) value); + else if ("locked".equals(setting)) preset.setLocked((Boolean) value); saveUserPreset(userId, preset); } public void sendControlPanel(VoiceChannel channel, Member owner) { - EmbedBuilder embed = + var limitText = channel.getUserLimit() == 0 ? "∞" : String.valueOf(channel.getUserLimit()); + var embed = new EmbedBuilder() .setTitle("🎛️ Управление каналом") .setDescription("Ваш личный голосовой канал") .addField("Владелец", owner.getAsMention(), true) - .addField( - "Лимит", - channel.getUserLimit() == 0 ? "∞" : String.valueOf(channel.getUserLimit()), - true) + .addField("Лимит", limitText, true) .setColor(Color.CYAN) .setFooter("Управляйте каналом с помощью кнопок ниже"); - - String cid = channel.getId(); - Button lockBtn = Button.primary("voice_lock:" + cid, "🔒 Замок"); - Button limitBtn = Button.secondary("voice_limit:" + cid, "👥 Лимит"); - Button renameBtn = Button.secondary("voice_rename:" + cid, "✏️ Название"); - Button bitrateBtn = Button.secondary("voice_bitrate:" + cid, "🎵 Битрейт"); - Button deleteBtn = Button.danger("voice_delete:" + cid, "🗑️ Удалить"); - - channel - .sendMessageEmbeds(embed.build()) - .addComponents(ActionRow.of(lockBtn, limitBtn, renameBtn, bitrateBtn, deleteBtn)) - .queue(); + var row = + ActionRow.of( + Button.primary("voice_lock:" + channel.getId(), "🔒 Замок"), + Button.secondary("voice_limit:" + channel.getId(), "👥 Лимит"), + Button.secondary("voice_rename:" + channel.getId(), "✏️ Название"), + Button.secondary("voice_bitrate:" + channel.getId(), "🎵 Битрейт"), + Button.danger("voice_delete:" + channel.getId(), "🗑️ Удалить")); + channel.sendMessageEmbeds(embed.build()).addComponents(row).queue(); } public void saveUserPreset(String userId, VoicePreset preset) { @@ -155,28 +127,19 @@ public class VoiceChannelManager { } public VoicePreset getUserPreset(String userId) { - VoicePreset preset = presetRepo.getPreset(userId); - return preset != null ? preset : new VoicePreset(); + return presetRepo.getPreset(userId) != null ? presetRepo.getPreset(userId) : new VoicePreset(); } public void applyUserPreset(VoiceChannel channel, String userId) { - VoicePreset preset = getUserPreset(userId); - + var preset = getUserPreset(userId); try { - if (preset.getName() != null && !preset.getName().isEmpty()) { + 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.getLimit() >= 0) channel.getManager().setUserLimit(preset.getLimit()).queue(); + if (preset.getBitrate() > 0) channel.getManager().setBitrate(preset.getBitrate()).queue(); if (preset.isLocked()) { - channel - .upsertPermissionOverride(channel.getGuild().getPublicRole()) - .deny(Permission.VOICE_CONNECT) - .queue(); + var publicRole = channel.getGuild().getPublicRole(); + channel.upsertPermissionOverride(publicRole).deny(Permission.VOICE_CONNECT).queue(); } LoggerDecorator.voice( logger, "Применён пресет пользователя " + userId + " к каналу " + channel.getId()); diff --git a/src/main/java/creator/loki/feature/voice/VoiceSetupPrefixCommand.java b/src/main/java/creator/loki/feature/voice/VoiceSetupPrefixCommand.java index 3bf4170..0b859e1 100644 --- a/src/main/java/creator/loki/feature/voice/VoiceSetupPrefixCommand.java +++ b/src/main/java/creator/loki/feature/voice/VoiceSetupPrefixCommand.java @@ -9,10 +9,9 @@ import creator.loki.embed.EmbedFactory; import creator.loki.feature.config.repository.ConfigRepository; import creator.loki.util.GuildChannelResolve; import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.entities.channel.concrete.Category; -import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; public class VoiceSetupPrefixCommand implements Command { + private final VoiceChannelManager voiceChannelManager; private final ConfigRepository configRepo; @@ -48,13 +47,11 @@ public class VoiceSetupPrefixCommand implements Command { context.getChannel().sendMessage("❌ У вас нет прав администратора!").queue(); return; } - - List args = context.getArgs(); + var args = context.getArgs(); if (args.isEmpty()) { sendHelp(context); return; } - switch (args.get(0).toLowerCase()) { case "trigger" -> handleTrigger(context, args); case "category" -> handleCategory(context, args); @@ -65,17 +62,12 @@ public class VoiceSetupPrefixCommand implements Command { private void handleTrigger(CommandContext context, List args) { if (args.size() < 2) { - context - .getChannel() - .sendMessage( - "❌ Укажите голосовой канал: `#канал`, `<#id>` или числовой id." - + "\nПример: `!voice-setup trigger 123456789012345678`") - .queue(); + var msg = + "❌ Укажите голосовой канал: `#канал`, `<#id>` или числовой id.\nПример: `!voice-setup trigger 123456789012345678`"; + context.getChannel().sendMessage(msg).queue(); return; } - - VoiceChannel channel = - GuildChannelResolve.voice(context.getGuild(), context.getMessage(), args, 1); + var channel = GuildChannelResolve.voice(context.getGuild(), context.getMessage(), args, 1); if (channel == null) { context .getChannel() @@ -85,34 +77,24 @@ public class VoiceSetupPrefixCommand implements Command { return; } configRepo.set("voice.trigger_channel_id", channel.getId()); - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("✅ Канал-триггер установлен") - .withDescription( - "Теперь при входе в " - + channel.getAsMention() - + " будет создаваться личный канал") - .withColor(Color.GREEN) - .build()) - .queue(); + var embed = + EmbedFactory.create() + .withTitle("✅ Канал-триггер установлен") + .withDescription( + "Теперь при входе в " + channel.getAsMention() + " будет создаваться личный канал") + .withColor(Color.GREEN) + .build(); + context.getChannel().sendMessageEmbeds(embed).queue(); } private void handleCategory(CommandContext context, List args) { if (args.size() < 2) { - context - .getChannel() - .sendMessage( - "❌ Укажите категорию: `#категория`, `<#id>` или числовой id." - + "\nПример: `!voice-setup category 123456789012345678`") - .queue(); + var msg = + "❌ Укажите категорию: `#категория`, `<#id>` или числовой id.\nПример: `!voice-setup category 123456789012345678`"; + context.getChannel().sendMessage(msg).queue(); return; } - - Category category = - GuildChannelResolve.category(context.getGuild(), context.getMessage(), args, 1); + var category = GuildChannelResolve.category(context.getGuild(), context.getMessage(), args, 1); if (category == null) { context .getChannel() @@ -122,60 +104,41 @@ public class VoiceSetupPrefixCommand implements Command { return; } configRepo.set("voice.category_id", category.getId()); - - context - .getChannel() - .sendMessageEmbeds( - EmbedFactory.create() - .withTitle("✅ Категория установлена") - .withDescription( - "Личные каналы будут создаваться в категории: **" + category.getName() + "**") - .withColor(Color.GREEN) - .build()) - .queue(); + var embed = + EmbedFactory.create() + .withTitle("✅ Категория установлена") + .withDescription( + "Личные каналы будут создаваться в категории: **" + category.getName() + "**") + .withColor(Color.GREEN) + .build(); + context.getChannel().sendMessageEmbeds(embed).queue(); } private void handleInfo(CommandContext context) { - String triggerId = configRepo.get("voice.trigger_channel_id"); - String categoryId = configRepo.get("voice.category_id"); - + var triggerId = configRepo.get("voice.trigger_channel_id"); + var categoryId = configRepo.get("voice.category_id"); var embed = EmbedFactory.create() .withTitle("🎤 Конфигурация системы личных каналов") .withColor(Color.BLUE); - - if (triggerId != null) { - embed.addField("Канал-триггер", "<#" + triggerId + ">", false); - } else { - embed.addField("Канал-триггер", "❌ Не установлен", false); - } - + if (triggerId != null) embed.addField("Канал-триггер", "<#" + triggerId + ">", false); + else embed.addField("Канал-триггер", "❌ Не установлен", false); if (categoryId != null) { - Category category = context.getGuild().getCategoryById(categoryId); + var category = context.getGuild().getCategoryById(categoryId); embed.addField("Категория", category != null ? category.getName() : "❌ Не найдена", false); - } else { - embed.addField("Категория", "❌ Не установлена", false); - } - + } else embed.addField("Категория", "❌ Не установлена", false); context.getChannel().sendMessageEmbeds(embed.build()).queue(); } private void sendHelp(CommandContext context) { + var desc = + "**Доступные команды:**\n• `!voice-setup trigger` - `#войс` или числовой id канала\n• `!voice-setup category` - `#категория` или id\n• `!voice-setup info` - Показать текущую конфигурацию\n\nТекстовые каналы для управления создаются автоматически при создании войса.\nТолько для администраторов!"; context .getChannel() .sendMessageEmbeds( EmbedFactory.create() .withTitle("🎤 Настройка голосовых каналов") - .withDescription( - """ - **Доступные команды:** - • `!voice-setup trigger` - `#войс` или числовой id канала - • `!voice-setup category` - `#категория` или id - • `!voice-setup info` - Показать текущую конфигурацию - - Текстовые каналы для управления создаются автоматически при создании войса. - Только для администраторов! - """) + .withDescription(desc) .withColor(Color.BLUE) .build()) .queue(); diff --git a/src/main/java/creator/loki/integration/nekos/NekosLifeApi.java b/src/main/java/creator/loki/integration/nekos/NekosLifeApi.java index 54bec5d..91604e0 100644 --- a/src/main/java/creator/loki/integration/nekos/NekosLifeApi.java +++ b/src/main/java/creator/loki/integration/nekos/NekosLifeApi.java @@ -1,57 +1,69 @@ package creator.loki.integration.nekos; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; +import java.io.IOException; +import java.util.concurrent.TimeUnit; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** Утилита для работы с nekos.life API */ +@Singleton public class NekosLifeApi { private static final Logger logger = LoggerFactory.getLogger(NekosLifeApi.class); private static final String API_BASE = "https://nekos.life/api/v2/img/"; - private static final HttpClient CLIENT = - HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); - /** Получить URL изображения из nekos.life API с повторными попытками */ - public static String fetchImage(String category) { + private static NekosLifeApi instance; + + private final OkHttpClient client; + + @Inject + public NekosLifeApi() { + this.client = + new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(); + NekosLifeApi.instance = this; + } + + NekosLifeApi(OkHttpClient client) { + this.client = client; + NekosLifeApi.instance = this; + } + + public String getImage(String category) { int maxRetries = 3; - int retryDelay = 1000; + long retryDelay = 1000; for (int attempt = 1; attempt <= maxRetries; attempt++) { - try { - HttpRequest request = - HttpRequest.newBuilder() - .uri(URI.create(API_BASE + category)) - .timeout(Duration.ofSeconds(10)) - .GET() - .build(); + Request request = new Request.Builder().url(API_BASE + category).get().build(); - HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() == 200) { - JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject(); - return json.get("url").getAsString(); - } else if (response.statusCode() >= 500) { - logger.warn( - "Nekos.life API error: {} (attempt {}/{})", - response.statusCode(), - attempt, - maxRetries); + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + if (response.code() >= 500) { + logger.warn( + "Nekos.life API error: {} (attempt {}/{})", response.code(), attempt, maxRetries); + } else { + logger.error("Nekos.life API error: {}", response.code()); + return null; + } } else { - logger.error("Nekos.life API error: {}", response.statusCode()); - return null; + ResponseBody body = response.body(); + if (body == null) { + logger.warn("Empty response body from nekos.life (attempt {}/{})", attempt, maxRetries); + continue; + } + JsonObject json = JsonParser.parseString(body.string()).getAsJsonObject(); + return json.get("url").getAsString(); } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.error("Interrupted while accessing nekos.life API", e); - return null; - } catch (Exception e) { + } catch (IOException e) { logger.error( "Error fetching image from nekos.life (attempt {}/{})", attempt, maxRetries, e); } @@ -70,4 +82,11 @@ public class NekosLifeApi { logger.error("Failed to fetch image after {} attempts", maxRetries); return null; } + + public static String fetchImage(String category) { + if (instance == null) { + instance = new NekosLifeApi(); + } + return instance.getImage(category); + } } diff --git a/src/main/java/creator/loki/util/VoiceHelper.java b/src/main/java/creator/loki/util/VoiceHelper.java new file mode 100644 index 0000000..d2648a2 --- /dev/null +++ b/src/main/java/creator/loki/util/VoiceHelper.java @@ -0,0 +1,39 @@ +package creator.loki.util; + +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; + +public final class VoiceHelper { + private static final int RETRIES = 10; + private static final long DELAY_MS = 200; + + private VoiceHelper() {} + + public static boolean waitForCondition(BooleanSupplier condition) { + for (int i = 0; i < RETRIES; i++) { + if (condition.getAsBoolean()) return true; + try { + Thread.sleep(DELAY_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + return false; + } + + public static boolean waitForCondition(BooleanSupplier condition, long timeout, TimeUnit unit) { + long deadline = System.nanoTime() + unit.toNanos(timeout); + long delay = Math.min(DELAY_MS, unit.toMillis(timeout) / RETRIES); + while (System.nanoTime() < deadline) { + if (condition.getAsBoolean()) return true; + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + return false; + } +} diff --git a/src/test/java/creator/loki/core/registry/SmokeCommandRegistrationTest.java b/src/test/java/creator/loki/core/registry/SmokeCommandRegistrationTest.java index b41d89e..a5b4f7c 100644 --- a/src/test/java/creator/loki/core/registry/SmokeCommandRegistrationTest.java +++ b/src/test/java/creator/loki/core/registry/SmokeCommandRegistrationTest.java @@ -15,6 +15,7 @@ import creator.loki.feature.counter.repository.CounterStatsRepository; import creator.loki.feature.economy.EconomyCommandRegistrar; import creator.loki.feature.economy.service.*; import creator.loki.feature.entertainment.EntertainmentCommandRegistrar; +import creator.loki.feature.entertainment.service.RatingService; import creator.loki.feature.level.LevelCommandRegistrar; import creator.loki.feature.level.LevelRoleService; import creator.loki.feature.level.ResetLevelsCommand; @@ -101,7 +102,10 @@ class SmokeCommandRegistrationTest { var entertainmentRegistrar = new EntertainmentCommandRegistrar( - mock(CooldownManager.class), mock(ShipPairRepository.class), mock(BotConfig.class)); + mock(CooldownManager.class), + mock(RatingService.class), + mock(ShipPairRepository.class), + mock(BotConfig.class)); entertainmentRegistrar.register(registry); var utilityRegistrar = diff --git a/src/test/java/creator/loki/feature/entertainment/commands/RatingLevelTest.java b/src/test/java/creator/loki/feature/entertainment/commands/RatingLevelTest.java index 314af20..e77e46c 100644 --- a/src/test/java/creator/loki/feature/entertainment/commands/RatingLevelTest.java +++ b/src/test/java/creator/loki/feature/entertainment/commands/RatingLevelTest.java @@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.*; import java.awt.Color; import java.util.List; +import creator.loki.feature.entertainment.commands.rating.RatingConfig; +import creator.loki.feature.entertainment.commands.rating.RatingLevel; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/creator/loki/feature/moderation/commands/SlowmodeCommandTest.java b/src/test/java/creator/loki/feature/moderation/commands/SlowmodeCommandTest.java index 2f3b8a9..d3b2737 100644 --- a/src/test/java/creator/loki/feature/moderation/commands/SlowmodeCommandTest.java +++ b/src/test/java/creator/loki/feature/moderation/commands/SlowmodeCommandTest.java @@ -9,16 +9,20 @@ import java.util.function.Consumer; import creator.loki.command.CommandContext; import creator.loki.feature.moderation.commands.utils.SlowmodeCommand; +import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.SelfUser; +import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.managers.channel.concrete.TextChannelManager; import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -35,7 +39,17 @@ class SlowmodeCommandTest { @Mock Member member; @Mock Member selfMember; @Mock Guild guild; + @Mock JDA jda; @Mock MessageCreateAction sendEmbedAction; + + @BeforeEach + void setUp() { + lenient().when(guild.getJDA()).thenReturn(jda); + SelfUser selfUser = mock(SelfUser.class); + lenient().when(jda.getSelfUser()).thenReturn(selfUser); + lenient().when(guild.getMember(selfUser)).thenReturn(selfMember); + } + @Mock TextChannelManager setSlowmodeAction; @Mock TextChannelManager channelManager; @@ -95,7 +109,7 @@ class SlowmodeCommandTest { lenient().when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendEmbedAction); when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); - when(guild.getSelfMember()).thenReturn(selfMember); + when(guild.getMember(any(User.class))).thenReturn(selfMember); when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(false); var context = new CommandContext(message, List.of(), channel, member, guild); @@ -111,7 +125,7 @@ class SlowmodeCommandTest { when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendEmbedAction); when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); - when(guild.getSelfMember()).thenReturn(selfMember); + when(guild.getMember(any(User.class))).thenReturn(selfMember); when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); when(channel.getSlowmode()).thenReturn(10); @@ -129,7 +143,7 @@ class SlowmodeCommandTest { when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendEmbedAction); when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); - when(guild.getSelfMember()).thenReturn(selfMember); + when(guild.getMember(any(User.class))).thenReturn(selfMember); when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); @@ -147,7 +161,7 @@ class SlowmodeCommandTest { when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction); when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); - when(guild.getSelfMember()).thenReturn(selfMember); + when(guild.getMember(any(User.class))).thenReturn(selfMember); when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); when(channel.getAsMention()).thenReturn("#general"); @@ -170,7 +184,7 @@ class SlowmodeCommandTest { when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction); when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); - when(guild.getSelfMember()).thenReturn(selfMember); + when(guild.getMember(any(User.class))).thenReturn(selfMember); when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); @@ -187,7 +201,7 @@ class SlowmodeCommandTest { when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction); when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); - when(guild.getSelfMember()).thenReturn(selfMember); + when(guild.getMember(any(User.class))).thenReturn(selfMember); when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); @@ -205,7 +219,7 @@ class SlowmodeCommandTest { when(channelManager.setSlowmode(anyInt())).thenReturn(setSlowmodeAction); when(member.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); - when(guild.getSelfMember()).thenReturn(selfMember); + when(guild.getMember(any(User.class))).thenReturn(selfMember); when(selfMember.hasPermission(any(GuildChannel.class), eq(Permission.MANAGE_CHANNEL))) .thenReturn(true); diff --git a/src/test/java/creator/loki/feature/profile/RankCardGeneratorTest.java b/src/test/java/creator/loki/feature/profile/RankCardGeneratorTest.java index 17beea0..d31e2be 100644 --- a/src/test/java/creator/loki/feature/profile/RankCardGeneratorTest.java +++ b/src/test/java/creator/loki/feature/profile/RankCardGeneratorTest.java @@ -32,9 +32,9 @@ class RankCardGeneratorTest { } @Test - void backgroundNotFoundThrowsException() { + void backgroundNotFoundReturnsFallback() throws Exception { var data = mockProfile(1, 0, 100, "nonexistent.png"); - assertThrows(Exception.class, () -> new RankCardGenerator().generate(data)); + assertNotNull(new RankCardGenerator().generate(data)); } private static ProfileData mockProfile(int level, int xp, int maxXp, String background) {