From e5e727b6c444a0ea6f204c9be59b725066486812 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Wed, 19 Aug 2026 12:00:27 +0200 Subject: [PATCH] refactor: split remaining >150-line files (voice, economy, image, serverstats, admin, core) - VoiceButtonHandler: extract VoiceModals (limit/rename/bitrate) - VoiceChannelManager: extract VoiceChannelActions (preset apply, control panel, save/get) - VoiceSetupPrefixCommand: extract VoiceSetupEmbeds - BlackjackService: extract card logic into BlackjackEngine - ImageService: extract BooruParser + ImageUrls - ServerStatsService: extract StatsChartRenderer - GenerateRatingsCommand: extract RatingsEmbeds - BotInitializer: extract AuxiliaryBotFactory (music/radio JDA) - RateLimiter: extract TokenBucket into own file All main files now <=150 lines (project rule). Build + 438 tests green. --- .../loki/core/AuxiliaryBotFactory.java | 69 ++++++++++++ .../creator/loki/core/BotInitializer.java | 45 +------- .../loki/core/security/RateLimiter.java | 50 --------- .../loki/core/security/TokenBucket.java | 51 +++++++++ .../commands/GenerateRatingsCommand.java | 51 ++------- .../feature/admin/commands/RatingsEmbeds.java | 48 +++++++++ .../economy/service/game/BlackjackEngine.java | 36 +++++++ .../service/game/BlackjackService.java | 57 +++------- .../feature/image/service/BooruParser.java | 65 +++++++++++ .../feature/image/service/ImageService.java | 94 ++-------------- .../loki/feature/image/service/ImageUrls.java | 28 +++++ .../serverstats/ServerStatsService.java | 98 +---------------- .../serverstats/StatsChartRenderer.java | 101 ++++++++++++++++++ .../voice/handler/VoiceButtonHandler.java | 52 +-------- .../feature/voice/handler/VoiceModals.java | 53 +++++++++ .../voice/service/VoiceChannelActions.java | 75 +++++++++++++ .../voice/service/VoiceChannelManager.java | 60 ++--------- .../voice/service/VoiceSetupEmbeds.java | 62 +++++++++++ .../service/VoiceSetupPrefixCommand.java | 63 +++-------- 19 files changed, 653 insertions(+), 505 deletions(-) create mode 100644 src/main/java/creator/loki/core/AuxiliaryBotFactory.java create mode 100644 src/main/java/creator/loki/core/security/TokenBucket.java create mode 100644 src/main/java/creator/loki/feature/admin/commands/RatingsEmbeds.java create mode 100644 src/main/java/creator/loki/feature/economy/service/game/BlackjackEngine.java create mode 100644 src/main/java/creator/loki/feature/image/service/BooruParser.java create mode 100644 src/main/java/creator/loki/feature/image/service/ImageUrls.java create mode 100644 src/main/java/creator/loki/feature/serverstats/StatsChartRenderer.java create mode 100644 src/main/java/creator/loki/feature/voice/handler/VoiceModals.java create mode 100644 src/main/java/creator/loki/feature/voice/service/VoiceChannelActions.java create mode 100644 src/main/java/creator/loki/feature/voice/service/VoiceSetupEmbeds.java diff --git a/src/main/java/creator/loki/core/AuxiliaryBotFactory.java b/src/main/java/creator/loki/core/AuxiliaryBotFactory.java new file mode 100644 index 0000000..863a12a --- /dev/null +++ b/src/main/java/creator/loki/core/AuxiliaryBotFactory.java @@ -0,0 +1,69 @@ +package creator.loki.core; + +import creator.loki.config.BotConfig; +import creator.loki.feature.music.core.LavalinkManager; +import creator.loki.feature.music.core.PlayerManager; +import creator.loki.feature.music.listener.MusicBotListener; +import creator.loki.feature.music.repository.QueueRepository; +import creator.loki.feature.radio.listener.RadioBotListener; +import creator.loki.feature.radio.service.RadioManager; +import creator.loki.util.LoggerDecorator; +import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.requests.GatewayIntent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class AuxiliaryBotFactory { + private static final Logger logger = LoggerFactory.getLogger(AuxiliaryBotFactory.class); + + private AuxiliaryBotFactory() {} + + static JDA createMusicJDA( + final BotConfig config, + final QueueRepository queueRepository, + final LavalinkManager lavalinkManager) { + if (config.getMusicToken() == null || config.getMusicToken().isBlank()) { + return null; + } + LoggerDecorator.startup(logger, "Инициализация music бота..."); + LavalinkManager ml = + LavalinkManagerFactory.create(config, queueRepository, config.getMusicToken()); + var builder = + JdaFactory.createBuilder( + config.getMusicToken(), + "🎵 m!play", + GatewayIntent.GUILD_MESSAGES, + GatewayIntent.MESSAGE_CONTENT, + GatewayIntent.GUILD_VOICE_STATES); + builder.addEventListeners(new MusicBotListener(ml, queueRepository, new PlayerManager(ml))); + return buildWithLavalink(builder, ml); + } + + static JDA createRadioJDA( + final BotConfig config, + final LavalinkManager radioLavalink, + final RadioManager radioManager) { + if (config.getRadioToken() == null || config.getRadioToken().isBlank()) { + return null; + } + LoggerDecorator.startup(logger, "Инициализация radio бота..."); + var builder = + JdaFactory.createBuilder( + config.getRadioToken(), + "📻 r!play", + GatewayIntent.GUILD_MESSAGES, + GatewayIntent.MESSAGE_CONTENT, + GatewayIntent.GUILD_VOICE_STATES); + builder.addEventListeners(new RadioBotListener(radioManager)); + return buildWithLavalink(builder, radioLavalink); + } + + private static JDA buildWithLavalink(final JDABuilder builder, final LavalinkManager ll) { + if (ll.getClient() != null) { + builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ll.getClient())); + } + return JdaFactory.awaitJda(builder.build()); + } +} diff --git a/src/main/java/creator/loki/core/BotInitializer.java b/src/main/java/creator/loki/core/BotInitializer.java index b9e2982..57ab886 100644 --- a/src/main/java/creator/loki/core/BotInitializer.java +++ b/src/main/java/creator/loki/core/BotInitializer.java @@ -7,13 +7,9 @@ import creator.loki.feature.counter.service.listener.MemberCounterListener; import creator.loki.feature.metrics.HealthHttpServer; import creator.loki.feature.moderation.MuteScheduler; import creator.loki.feature.music.core.LavalinkManager; -import creator.loki.feature.music.core.PlayerManager; -import creator.loki.feature.music.listener.MusicBotListener; import creator.loki.feature.music.repository.QueueRepository; -import creator.loki.feature.radio.listener.RadioBotListener; import creator.loki.feature.radio.service.RadioManager; import creator.loki.util.LoggerDecorator; -import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.requests.GatewayIntent; @@ -74,13 +70,13 @@ public class BotInitializer { } }); - JDA musicJda = createMusicJDA(); + JDA musicJda = AuxiliaryBotFactory.createMusicJDA(config, queueRepository, lavalinkManager); JDA radioJda = null; if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) { sleep(3000); LavalinkManager radioLavalink = LavalinkManagerFactory.create(config, queueRepository, config.getRadioToken()); - radioJda = createRadioJDA(radioLavalink); + radioJda = AuxiliaryBotFactory.createRadioJDA(config, radioLavalink, radioManager); if (radioJda != null) { radioManager.setRadioJda(radioJda); radioManager.setLavalinkManager(radioLavalink); @@ -105,43 +101,10 @@ public class BotInitializer { return buildWithLavalink(builder, lavalinkManager); } - private JDA createMusicJDA() { - if (config.getMusicToken() == null || config.getMusicToken().isBlank()) { - return null; - } - LoggerDecorator.startup(logger, "Инициализация music бота..."); - LavalinkManager ml = - LavalinkManagerFactory.create(config, queueRepository, config.getMusicToken()); - var builder = - JdaFactory.createBuilder( - config.getMusicToken(), - "🎵 m!play", - GatewayIntent.GUILD_MESSAGES, - GatewayIntent.MESSAGE_CONTENT, - GatewayIntent.GUILD_VOICE_STATES); - builder.addEventListeners(new MusicBotListener(ml, queueRepository, new PlayerManager(ml))); - return buildWithLavalink(builder, ml); - } - - private JDA createRadioJDA(final LavalinkManager radioLavalink) { - if (config.getRadioToken() == null || config.getRadioToken().isBlank()) { - return null; - } - LoggerDecorator.startup(logger, "Инициализация radio бота..."); - var builder = - JdaFactory.createBuilder( - config.getRadioToken(), - "📻 r!play", - GatewayIntent.GUILD_MESSAGES, - GatewayIntent.MESSAGE_CONTENT, - GatewayIntent.GUILD_VOICE_STATES); - builder.addEventListeners(new RadioBotListener(radioManager)); - return buildWithLavalink(builder, radioLavalink); - } - private static JDA buildWithLavalink(final JDABuilder builder, final LavalinkManager ll) { if (ll.getClient() != null) { - builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ll.getClient())); + builder.setVoiceDispatchInterceptor( + new dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener(ll.getClient())); } return JdaFactory.awaitJda(builder.build()); } diff --git a/src/main/java/creator/loki/core/security/RateLimiter.java b/src/main/java/creator/loki/core/security/RateLimiter.java index 7b08a48..b723d9f 100644 --- a/src/main/java/creator/loki/core/security/RateLimiter.java +++ b/src/main/java/creator/loki/core/security/RateLimiter.java @@ -98,54 +98,4 @@ public class RateLimiter implements Lifecycle { } LoggerDecorator.success(logger, "RateLimiter завершён"); } - - /** Token Bucket для rate limiting */ - private static class TokenBucket { - private final int capacity; - private final double refillRate; // токенов в секунду - private double tokens; - private long lastRefillTime; - private long lastAccessTime; - - TokenBucket(int capacity, double refillRate) { - this.capacity = capacity; - this.refillRate = refillRate; - this.tokens = capacity; - this.lastRefillTime = System.currentTimeMillis(); - this.lastAccessTime = System.currentTimeMillis(); - } - - public synchronized boolean tryConsume() { - refill(); - lastAccessTime = System.currentTimeMillis(); - - if (tokens >= 1.0) { - tokens -= 1.0; - return true; - } - return false; - } - - private void refill() { - long now = System.currentTimeMillis(); - long timePassed = now - lastRefillTime; - double tokensToAdd = (timePassed / 1000.0) * refillRate; - - tokens = Math.min(capacity, tokens + tokensToAdd); - lastRefillTime = now; - } - - public synchronized long getWaitTime() { - refill(); - if (tokens >= 1.0) { - return 0; - } - double tokensNeeded = 1.0 - tokens; - return (long) Math.ceil(tokensNeeded / refillRate); - } - - public long getLastAccessTime() { - return lastAccessTime; - } - } } diff --git a/src/main/java/creator/loki/core/security/TokenBucket.java b/src/main/java/creator/loki/core/security/TokenBucket.java new file mode 100644 index 0000000..1e832ea --- /dev/null +++ b/src/main/java/creator/loki/core/security/TokenBucket.java @@ -0,0 +1,51 @@ +package creator.loki.core.security; + +/** Token Bucket для rate limiting */ +final class TokenBucket { + private final int capacity; + private final double refillRate; // токенов в секунду + private double tokens; + private long lastRefillTime; + private long lastAccessTime; + + TokenBucket(int capacity, double refillRate) { + this.capacity = capacity; + this.refillRate = refillRate; + this.tokens = capacity; + this.lastRefillTime = System.currentTimeMillis(); + this.lastAccessTime = System.currentTimeMillis(); + } + + public synchronized boolean tryConsume() { + refill(); + lastAccessTime = System.currentTimeMillis(); + + if (tokens >= 1.0) { + tokens -= 1.0; + return true; + } + return false; + } + + private void refill() { + long now = System.currentTimeMillis(); + long timePassed = now - lastRefillTime; + double tokensToAdd = (timePassed / 1000.0) * refillRate; + + tokens = Math.min(capacity, tokens + tokensToAdd); + lastRefillTime = now; + } + + public synchronized long getWaitTime() { + refill(); + if (tokens >= 1.0) { + return 0; + } + double tokensNeeded = 1.0 - tokens; + return (long) Math.ceil(tokensNeeded / refillRate); + } + + public long getLastAccessTime() { + return lastAccessTime; + } +} 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 b18c036..bc21692 100644 --- a/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java +++ b/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java @@ -1,16 +1,13 @@ package creator.loki.feature.admin.commands; -import java.awt.Color; import java.util.Random; import creator.loki.command.Command; import creator.loki.command.CommandCategories; import creator.loki.command.CommandContext; 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; @@ -51,7 +48,7 @@ public class GenerateRatingsCommand implements Command { context .getChannel() .sendMessageEmbeds( - errorEmbed( + RatingsEmbeds.error( "❌ Недостаточно прав", "Эта команда доступна только супер-администратору бота.")) .queue(); return; @@ -64,7 +61,7 @@ public class GenerateRatingsCommand implements Command { context .getChannel() .sendMessageEmbeds( - warnEmbed( + RatingsEmbeds.warn( "⏳ Генерация рейтингов...", "Начинаю генерацию всех rating значений для участников сервера...")) .complete(); @@ -83,7 +80,9 @@ public class GenerateRatingsCommand implements Command { } generatedCount++; } - initial.editMessageEmbeds(successEmbed(context, memberCount, generatedCount)).queue(); + initial + .editMessageEmbeds(RatingsEmbeds.success(context, memberCount, generatedCount)) + .queue(); logger.info( "Супер-админ {} сгенерировал рейтинги для {} участников", context.getAuthor().getUser().getName(), @@ -92,7 +91,8 @@ public class GenerateRatingsCommand implements Command { logger.error("Ошибка при генерации рейтингов", e); initial .editMessageEmbeds( - errorEmbed("❌ Ошибка", "Произошла ошибка при генерации рейтингов: " + e.getMessage())) + RatingsEmbeds.error( + "❌ Ошибка", "Произошла ошибка при генерации рейтингов: " + e.getMessage())) .queue(); } } @@ -116,41 +116,4 @@ public class GenerateRatingsCommand implements Command { }; ratingsRepository.setRating(guildId, userId, ratingType, value); } - - private MessageEmbed errorEmbed(final String title, final String description) { - return EmbedFactory.create() - .withTitle(title) - .withDescription(description) - .withColor(Color.RED) - .build(); - } - - private MessageEmbed warnEmbed(final String title, final String description) { - return EmbedFactory.create() - .withTitle(title) - .withDescription(description) - .withColor(Color.YELLOW) - .build(); - } - - private MessageEmbed successEmbed( - final CommandContext context, final int memberCount, final 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/admin/commands/RatingsEmbeds.java b/src/main/java/creator/loki/feature/admin/commands/RatingsEmbeds.java new file mode 100644 index 0000000..5a67b16 --- /dev/null +++ b/src/main/java/creator/loki/feature/admin/commands/RatingsEmbeds.java @@ -0,0 +1,48 @@ +package creator.loki.feature.admin.commands; + +import java.awt.Color; + +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import net.dv8tion.jda.api.entities.MessageEmbed; + +final class RatingsEmbeds { + private RatingsEmbeds() {} + + static MessageEmbed error(final String title, final String description) { + return EmbedFactory.create() + .withTitle(title) + .withDescription(description) + .withColor(Color.RED) + .build(); + } + + static MessageEmbed warn(final String title, final String description) { + return EmbedFactory.create() + .withTitle(title) + .withDescription(description) + .withColor(Color.YELLOW) + .build(); + } + + static MessageEmbed success( + final CommandContext context, final int memberCount, final 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/service/game/BlackjackEngine.java b/src/main/java/creator/loki/feature/economy/service/game/BlackjackEngine.java new file mode 100644 index 0000000..121117e --- /dev/null +++ b/src/main/java/creator/loki/feature/economy/service/game/BlackjackEngine.java @@ -0,0 +1,36 @@ +package creator.loki.feature.economy.service.game; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +final class BlackjackEngine { + private static final String[] DECK = { + "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A" + }; + + private BlackjackEngine() {} + + static String drawCard() { + return DECK[ThreadLocalRandom.current().nextInt(DECK.length)]; + } + + static int calculateSum(final List cards) { + var sum = 0; + var aces = 0; + for (var c : cards) { + switch (c) { + case "J", "Q", "K" -> sum += 10; + case "A" -> { + sum += 11; + aces++; + } + default -> sum += Integer.parseInt(c); + } + } + while (sum > 21 && aces > 0) { + sum -= 10; + aces--; + } + return sum; + } +} diff --git a/src/main/java/creator/loki/feature/economy/service/game/BlackjackService.java b/src/main/java/creator/loki/feature/economy/service/game/BlackjackService.java index 7ca73d8..cc90a7c 100644 --- a/src/main/java/creator/loki/feature/economy/service/game/BlackjackService.java +++ b/src/main/java/creator/loki/feature/economy/service/game/BlackjackService.java @@ -3,20 +3,11 @@ package creator.loki.feature.economy.service.game; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ThreadLocalRandom; import creator.loki.feature.economy.repository.EconomyRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class BlackjackService { - private static final Logger logger = LoggerFactory.getLogger(BlackjackService.class); - - private static final String[] DECK = { - "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A" - }; - private final EconomyRepository repository; private final ConcurrentHashMap activeGames = new ConcurrentHashMap<>(); @@ -38,11 +29,13 @@ public class BlackjackService { repository.save(account.withCash(account.getCash() - bet)); - var playerCards = new ArrayList<>(List.of(drawCard(), drawCard())); - var dealerCards = new ArrayList<>(List.of(drawCard(), drawCard())); + var playerCards = + new ArrayList<>(List.of(BlackjackEngine.drawCard(), BlackjackEngine.drawCard())); + var dealerCards = + new ArrayList<>(List.of(BlackjackEngine.drawCard(), BlackjackEngine.drawCard())); - var playerSum = calculateSum(playerCards); - var dealerSum = calculateSum(dealerCards); + var playerSum = BlackjackEngine.calculateSum(playerCards); + var dealerSum = BlackjackEngine.calculateSum(dealerCards); var playerBj = playerSum == 21; var dealerBj = dealerSum == 21; @@ -62,8 +55,8 @@ public class BlackjackService { return null; } - game.playerCards().add(drawCard()); - var sum = calculateSum(game.playerCards()); + game.playerCards().add(BlackjackEngine.drawCard()); + var sum = BlackjackEngine.calculateSum(game.playerCards()); var over = sum > 21; if (over) { @@ -74,7 +67,7 @@ public class BlackjackService { game.playerCards(), game.dealerCards(), sum, - calculateSum(game.dealerCards()), + BlackjackEngine.calculateSum(game.dealerCards()), game.bet(), over, 0); @@ -87,12 +80,12 @@ public class BlackjackService { } var dealerCards = new ArrayList<>(game.dealerCards()); - while (calculateSum(dealerCards) < 16) { - dealerCards.add(drawCard()); + while (BlackjackEngine.calculateSum(dealerCards) < 16) { + dealerCards.add(BlackjackEngine.drawCard()); } - var playerSum = calculateSum(game.playerCards()); - var dealerSum = calculateSum(dealerCards); + var playerSum = BlackjackEngine.calculateSum(game.playerCards()); + var dealerSum = BlackjackEngine.calculateSum(dealerCards); var payout = dealerSum > 21 || playerSum > dealerSum ? game.bet() * 2 : 0; creditUser(userId, payout); @@ -109,30 +102,6 @@ public class BlackjackService { repository.save(account.withCash(account.getCash() + amount)); } - private String drawCard() { - return DECK[ThreadLocalRandom.current().nextInt(DECK.length)]; - } - - private int calculateSum(final List cards) { - var sum = 0; - var aces = 0; - for (var c : cards) { - switch (c) { - case "J", "Q", "K" -> sum += 10; - case "A" -> { - sum += 11; - aces++; - } - default -> sum += Integer.parseInt(c); - } - } - while (sum > 21 && aces > 0) { - sum -= 10; - aces--; - } - return sum; - } - private BlackjackState buildState( final List playerCards, final List dealerCards, diff --git a/src/main/java/creator/loki/feature/image/service/BooruParser.java b/src/main/java/creator/loki/feature/image/service/BooruParser.java new file mode 100644 index 0000000..3e019ff --- /dev/null +++ b/src/main/java/creator/loki/feature/image/service/BooruParser.java @@ -0,0 +1,65 @@ +package creator.loki.feature.image.service; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +final class BooruParser { + private BooruParser() {} + + static List parseE621(final String body, final int limit) { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + JsonArray posts = json.getAsJsonArray("posts"); + if (posts == null || posts.isEmpty()) { + return List.of(); + } + List urls = new ArrayList<>(); + for (JsonElement el : posts) { + JsonObject post = el.getAsJsonObject(); + JsonObject file = post.getAsJsonObject("file"); + if (file != null && file.has("url") && !file.get("url").isJsonNull()) { + String imageUrl = file.get("url").getAsString(); + if (!imageUrl.isEmpty()) { + urls.add(imageUrl); + } + } + if (urls.size() >= limit) { + break; + } + } + return urls; + } + + static List parseBooru(final String body, final int code, final int limit) { + if (body.isBlank() || !body.startsWith("[")) { + return List.of(); + } + JsonArray posts = JsonParser.parseString(body).getAsJsonArray(); + List urls = new ArrayList<>(); + for (JsonElement el : posts) { + JsonObject post = el.getAsJsonObject(); + String fileUrl = post.get("file_url").getAsString(); + if (fileUrl != null && !fileUrl.isEmpty()) { + if (fileUrl.startsWith("//")) { + fileUrl = "https:" + fileUrl; + } + urls.add(fileUrl); + } + if (urls.size() >= limit) { + break; + } + } + return urls; + } + + static String encodeTags(String tags) { + if (tags == null || tags.isBlank()) { + return ""; + } + return tags.trim().replace(" ", "+").replace("#", "%23"); + } +} diff --git a/src/main/java/creator/loki/feature/image/service/ImageService.java b/src/main/java/creator/loki/feature/image/service/ImageService.java index 3a84cc8..b3ac1a7 100644 --- a/src/main/java/creator/loki/feature/image/service/ImageService.java +++ b/src/main/java/creator/loki/feature/image/service/ImageService.java @@ -1,14 +1,9 @@ package creator.loki.feature.image.service; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import com.google.inject.Inject; import com.google.inject.Singleton; import creator.loki.config.BotConfig; @@ -21,7 +16,6 @@ import org.slf4j.LoggerFactory; @Singleton public class ImageService { private static final Logger logger = LoggerFactory.getLogger(ImageService.class); - private static final String E621_API = "https://e621.net/posts.json"; private final OkHttpClient client; @@ -40,37 +34,13 @@ public class ImageService { public List fetchE621(String tags, int limit, int page) { int capped = Math.min(limit, 100); - String url = E621_API + "?tags=" + encodeTags(tags) + "&limit=" + capped + "&page=" + page; - Request request = - new Request.Builder().url(url).header("User-Agent", "LokiBot/1.0 (by loki)").get().build(); - - try (Response response = client.newCall(request).execute()) { + String url = ImageUrls.e621(BooruParser.encodeTags(tags), capped, page); + try (Response response = call(url)) { if (!response.isSuccessful() || response.body() == null) { logger.error("e621 API error: {} {}", response.code(), response.message()); return List.of(); } - String body = response.body().string(); - JsonObject json = JsonParser.parseString(body).getAsJsonObject(); - JsonArray posts = json.getAsJsonArray("posts"); - if (posts == null || posts.isEmpty()) { - return List.of(); - } - - List urls = new ArrayList<>(); - for (JsonElement el : posts) { - JsonObject post = el.getAsJsonObject(); - JsonObject file = post.getAsJsonObject("file"); - if (file != null && file.has("url") && !file.get("url").isJsonNull()) { - String imageUrl = file.get("url").getAsString(); - if (!imageUrl.isEmpty()) { - urls.add(imageUrl); - } - } - if (urls.size() >= limit) { - break; - } - } - return urls; + return BooruParser.parseE621(response.body().string(), limit); } catch (IOException e) { logger.error("Error fetching from e621", e); return List.of(); @@ -83,13 +53,7 @@ public class ImageService { public List fetchGelbooru(String tags, int limit, int page) { int capped = Math.min(limit, 100); - String url = - "https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1&tags=" - + encodeTags(tags) - + "&limit=" - + capped - + "&pid=" - + (page - 1); + String url = ImageUrls.gelbooru(BooruParser.encodeTags(tags), capped, page); return fetchBooruJson(url, limit); } @@ -99,62 +63,26 @@ public class ImageService { public List fetchYandere(String tags, int limit, int page) { int capped = Math.min(limit, 100); - String url = - "https://yande.re/post.json?tags=" - + encodeTags(tags) - + "&limit=" - + capped - + "&page=" - + page; + String url = ImageUrls.yandere(BooruParser.encodeTags(tags), capped, page); return fetchBooruJson(url, limit); } private List fetchBooruJson(final String url, final int limit) { - Request request = - new Request.Builder().url(url).header("User-Agent", "LokiBot/1.0 (by loki)").get().build(); - - try (Response response = client.newCall(request).execute()) { + try (Response response = call(url)) { if (!response.isSuccessful() || response.body() == null) { logger.error("Booru API error: {} {}", response.code(), response.message()); return List.of(); } - return parseBooruResponse(response.body().string(), response.code(), limit); + return BooruParser.parseBooru(response.body().string(), response.code(), limit); } catch (IOException e) { logger.error("Error fetching from booru", e); return List.of(); } } - private List parseBooruResponse(final String body, final int code, final int limit) { - if (body.isBlank() || !body.startsWith("[")) { - logger.warn( - "Booru API returned non-JSON response (code {}): {}", - code, - body.length() > 200 ? body.substring(0, 200) : body); - return List.of(); - } - JsonArray posts = JsonParser.parseString(body).getAsJsonArray(); - List urls = new ArrayList<>(); - for (JsonElement el : posts) { - JsonObject post = el.getAsJsonObject(); - String fileUrl = post.get("file_url").getAsString(); - if (fileUrl != null && !fileUrl.isEmpty()) { - if (fileUrl.startsWith("//")) { - fileUrl = "https:" + fileUrl; - } - urls.add(fileUrl); - } - if (urls.size() >= limit) { - break; - } - } - return urls; - } - - private String encodeTags(String tags) { - if (tags == null || tags.isBlank()) { - return ""; - } - return tags.trim().replace(" ", "+").replace("#", "%23"); + private Response call(final String url) throws IOException { + Request request = + new Request.Builder().url(url).header("User-Agent", "LokiBot/1.0 (by loki)").get().build(); + return client.newCall(request).execute(); } } diff --git a/src/main/java/creator/loki/feature/image/service/ImageUrls.java b/src/main/java/creator/loki/feature/image/service/ImageUrls.java new file mode 100644 index 0000000..1f88306 --- /dev/null +++ b/src/main/java/creator/loki/feature/image/service/ImageUrls.java @@ -0,0 +1,28 @@ +package creator.loki.feature.image.service; + +final class ImageUrls { + private static final String E621_API = "https://e621.net/posts.json"; + + private ImageUrls() {} + + static String e621(String tags, int limit, int page) { + return E621_API + "?tags=" + encode(tags) + "&limit=" + limit + "&page=" + page; + } + + static String gelbooru(String tags, int limit, int page) { + return "https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1&tags=" + + encode(tags) + + "&limit=" + + limit + + "&pid=" + + (page - 1); + } + + static String yandere(String tags, int limit, int page) { + return "https://yande.re/post.json?tags=" + encode(tags) + "&limit=" + limit + "&page=" + page; + } + + private static String encode(String tags) { + return BooruParser.encodeTags(tags); + } +} diff --git a/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java b/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java index 7144b7a..815404f 100644 --- a/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java +++ b/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java @@ -1,34 +1,17 @@ package creator.loki.feature.serverstats; -import java.awt.Color; -import java.awt.Font; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.time.format.DateTimeFormatter; import java.util.List; -import javax.imageio.ImageIO; - import creator.loki.feature.counter.repository.CounterStatsRepository; import creator.loki.feature.counter.service.format.DailyStats; 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; - private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd.MM"); - private static final Color COLOR_JOINED = new Color(46, 204, 113); - private static final Color COLOR_LEFT = new Color(231, 76, 60); - private static final Color COLOR_BG = new Color(44, 47, 51); - private static final Color COLOR_TEXT = new Color(220, 220, 220); - private static final Color COLOR_GRID = new Color(70, 70, 70); - private final CounterStatsRepository counterStatsRepository; + private final StatsChartRenderer chartRenderer = new StatsChartRenderer(); public ServerStatsService(CounterStatsRepository counterStatsRepository) { this.counterStatsRepository = counterStatsRepository; @@ -39,80 +22,7 @@ public class ServerStatsService { } public byte[] generateChart(List history) throws Exception { - 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); - var maxVal = - history.stream().mapToInt(s -> Math.max(s.getJoined(), s.getLeft())).max().orElse(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 (var i = 0; i <= 4; i++) { - var y = MARGIN + plotH - (plotH * i / 4); - g.drawLine(MARGIN, y, CHART_WIDTH - MARGIN, y); - } - drawLabels(g, plotH, maxVal); - var maxBars = Math.min(history.size(), 30); - var displayHistory = history.subList(history.size() - maxBars, history.size()); - drawData(g, displayHistory, maxVal, plotH, barW); - drawLegend(g); - g.dispose(); - var baos = new ByteArrayOutputStream(); - ImageIO.write(image, "PNG", baos); - return baos.toByteArray(); - } - - private void drawLabels(final Graphics2D g, final int plotH, final 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( - final Graphics2D g, - final List history, - final int maxVal, - final int plotH, - final 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); - g.drawString(stats.getDate().substring(5), x + barW, CHART_HEIGHT - MARGIN + 15); - g.rotate(Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15); - } - } - } - - private void drawLegend(final 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); + return chartRenderer.render(history); } public byte[] generateCsv(final Guild guild, final List history) { @@ -142,7 +52,7 @@ public class ServerStatsService { var header = "server_name,server_id,owner,created_at,total_members,online,bots,text_channels,voice_channels,roles\n"; return (header + info + "\ndate,joined,left,difference\n" + dailyRows + totals) - .getBytes(java.nio.charset.StandardCharsets.UTF_8); + .getBytes(StandardCharsets.UTF_8); } private static String escapeCsv(String value) { diff --git a/src/main/java/creator/loki/feature/serverstats/StatsChartRenderer.java b/src/main/java/creator/loki/feature/serverstats/StatsChartRenderer.java new file mode 100644 index 0000000..8c04efc --- /dev/null +++ b/src/main/java/creator/loki/feature/serverstats/StatsChartRenderer.java @@ -0,0 +1,101 @@ +package creator.loki.feature.serverstats; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.util.List; + +import javax.imageio.ImageIO; + +import creator.loki.feature.counter.service.format.DailyStats; + +final class StatsChartRenderer { + private static final int CHART_WIDTH = 800; + private static final int CHART_HEIGHT = 400; + private static final int MARGIN = 60; + private static final Color COLOR_JOINED = new Color(46, 204, 113); + private static final Color COLOR_LEFT = new Color(231, 76, 60); + private static final Color COLOR_BG = new Color(44, 47, 51); + private static final Color COLOR_TEXT = new Color(220, 220, 220); + private static final Color COLOR_GRID = new Color(70, 70, 70); + + byte[] render(final List history) throws Exception { + 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); + var maxVal = + history.stream().mapToInt(s -> Math.max(s.getJoined(), s.getLeft())).max().orElse(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 (var i = 0; i <= 4; i++) { + var y = MARGIN + plotH - (plotH * i / 4); + g.drawLine(MARGIN, y, CHART_WIDTH - MARGIN, y); + } + drawLabels(g, plotH, maxVal); + var maxBars = Math.min(history.size(), 30); + var displayHistory = history.subList(history.size() - maxBars, history.size()); + drawData(g, displayHistory, maxVal, plotH, barW); + drawLegend(g); + g.dispose(); + var baos = new ByteArrayOutputStream(); + ImageIO.write(image, "PNG", baos); + return baos.toByteArray(); + } + + private void drawLabels(final Graphics2D g, final int plotH, final 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( + final Graphics2D g, + final List history, + final int maxVal, + final int plotH, + final 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); + g.drawString(stats.getDate().substring(5), x + barW, CHART_HEIGHT - MARGIN + 15); + g.rotate(Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15); + } + } + } + + private void drawLegend(final 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); + } +} diff --git a/src/main/java/creator/loki/feature/voice/handler/VoiceButtonHandler.java b/src/main/java/creator/loki/feature/voice/handler/VoiceButtonHandler.java index 534c1fd..b4639ce 100644 --- a/src/main/java/creator/loki/feature/voice/handler/VoiceButtonHandler.java +++ b/src/main/java/creator/loki/feature/voice/handler/VoiceButtonHandler.java @@ -5,14 +5,10 @@ import com.google.inject.Singleton; import creator.loki.feature.voice.service.VoiceChannelManager; 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.modals.Modal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -96,57 +92,15 @@ public class VoiceButtonHandler extends ListenerAdapter { } private void handleLimit(final ButtonInteractionEvent event, final VoiceChannel channel) { - TextInput limitInput = - TextInput.create("limit", TextInputStyle.SHORT) - .setPlaceholder("0-99 (0 = без лимита)") - .setRequired(true) - .setMinLength(1) - .setMaxLength(2) - .setValue(String.valueOf(channel.getUserLimit())) - .build(); - - Modal modal = - Modal.create("voice_limit:" + channel.getId(), "Установить лимит") - .addComponents(Label.of("Лимит пользователей", limitInput)) - .build(); - - event.replyModal(modal).queue(); + event.replyModal(VoiceModals.limit(channel)).queue(); } private void handleRename(final ButtonInteractionEvent event, final VoiceChannel channel) { - TextInput nameInput = - TextInput.create("name", TextInputStyle.SHORT) - .setPlaceholder("Введите название канала") - .setRequired(true) - .setMinLength(1) - .setMaxLength(100) - .setValue(channel.getName()) - .build(); - - Modal modal = - Modal.create("voice_rename:" + channel.getId(), "Переименовать канал") - .addComponents(Label.of("Новое название", nameInput)) - .build(); - - event.replyModal(modal).queue(); + event.replyModal(VoiceModals.rename(channel)).queue(); } private void handleBitrate(final ButtonInteractionEvent event, final VoiceChannel channel) { - TextInput bitrateInput = - TextInput.create("bitrate", TextInputStyle.SHORT) - .setPlaceholder("8-384") - .setRequired(true) - .setMinLength(1) - .setMaxLength(3) - .setValue(String.valueOf(channel.getBitrate() / 1000)) - .build(); - - Modal modal = - Modal.create("voice_bitrate:" + channel.getId(), "🎨 Установить битрейт") - .addComponents(Label.of("Битрейт (kbps)", bitrateInput)) - .build(); - - event.replyModal(modal).queue(); + event.replyModal(VoiceModals.bitrate(channel)).queue(); } private void handleDelete(final ButtonInteractionEvent event, final VoiceChannel channel) { diff --git a/src/main/java/creator/loki/feature/voice/handler/VoiceModals.java b/src/main/java/creator/loki/feature/voice/handler/VoiceModals.java new file mode 100644 index 0000000..88258e9 --- /dev/null +++ b/src/main/java/creator/loki/feature/voice/handler/VoiceModals.java @@ -0,0 +1,53 @@ +package creator.loki.feature.voice.handler; + +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.channel.concrete.VoiceChannel; +import net.dv8tion.jda.api.modals.Modal; + +final class VoiceModals { + private VoiceModals() {} + + static Modal limit(final VoiceChannel channel) { + TextInput limitInput = + TextInput.create("limit", TextInputStyle.SHORT) + .setPlaceholder("0-99 (0 = без лимита)") + .setRequired(true) + .setMinLength(1) + .setMaxLength(2) + .setValue(String.valueOf(channel.getUserLimit())) + .build(); + return Modal.create("voice_limit:" + channel.getId(), "Установить лимит") + .addComponents(Label.of("Лимит пользователей", limitInput)) + .build(); + } + + static Modal rename(final VoiceChannel channel) { + TextInput nameInput = + TextInput.create("name", TextInputStyle.SHORT) + .setPlaceholder("Введите название канала") + .setRequired(true) + .setMinLength(1) + .setMaxLength(100) + .setValue(channel.getName()) + .build(); + return Modal.create("voice_rename:" + channel.getId(), "Переименовать канал") + .addComponents(Label.of("Новое название", nameInput)) + .build(); + } + + static Modal bitrate(final VoiceChannel channel) { + TextInput bitrateInput = + TextInput.create("bitrate", TextInputStyle.SHORT) + .setPlaceholder("8-384") + .setRequired(true) + .setMinLength(1) + .setMaxLength(3) + .setValue(String.valueOf(channel.getBitrate() / 1000)) + .build(); + return Modal.create("voice_bitrate:" + channel.getId(), "🎨 Установить битрейт") + .addComponents(Label.of("Битрейт (kbps)", bitrateInput)) + .build(); + } +} diff --git a/src/main/java/creator/loki/feature/voice/service/VoiceChannelActions.java b/src/main/java/creator/loki/feature/voice/service/VoiceChannelActions.java new file mode 100644 index 0000000..d7a5da1 --- /dev/null +++ b/src/main/java/creator/loki/feature/voice/service/VoiceChannelActions.java @@ -0,0 +1,75 @@ +package creator.loki.feature.voice.service; + +import java.awt.Color; + +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.Member; +import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class VoiceChannelActions { + private static final Logger logger = LoggerFactory.getLogger(VoiceChannelActions.class); + private final VoicePresetRepository presetRepo; + + VoiceChannelActions(final VoicePresetRepository presetRepo) { + this.presetRepo = presetRepo; + } + + void applyUserPreset(final VoiceChannel channel, final String userId) { + var preset = getPreset(userId); + try { + if (preset.getName() != null && !preset.getName().isEmpty()) { + channel.getManager().setName(preset.getName()).queue(); + } + if (preset.getLimit() >= 0) { + channel.getManager().setUserLimit(preset.getLimit()).queue(); + } + if (preset.getBitrate() > 0) { + channel.getManager().setBitrate(preset.getBitrate()).queue(); + } + if (preset.isLocked()) { + var publicRole = channel.getGuild().getPublicRole(); + channel.upsertPermissionOverride(publicRole).deny(Permission.VOICE_CONNECT).queue(); + } + LoggerDecorator.voice( + logger, "Применён пресет пользователя " + userId + " к каналу " + channel.getId()); + } catch (Exception e) { + LoggerDecorator.error(logger, "Ошибка при применении пресета", e); + } + } + + void sendControlPanel(final VoiceChannel channel, final Member owner) { + var limitText = channel.getUserLimit() == 0 ? "∞" : String.valueOf(channel.getUserLimit()); + var embed = + new EmbedBuilder() + .setTitle("🎛️ Управление каналом") + .setDescription("Ваш личный голосовой канал") + .addField("Владелец", owner.getAsMention(), true) + .addField("Лимит", limitText, true) + .setColor(Color.CYAN) + .setFooter("Управляйте каналом с помощью кнопок ниже"); + 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(); + } + + void savePreset(final String userId, final VoicePreset preset) { + presetRepo.savePreset(userId, preset); + LoggerDecorator.voice(logger, "Сохранён пресет для пользователя " + userId); + } + + VoicePreset getPreset(final String userId) { + return presetRepo.getPreset(userId) != null ? presetRepo.getPreset(userId) : new VoicePreset(); + } +} diff --git a/src/main/java/creator/loki/feature/voice/service/VoiceChannelManager.java b/src/main/java/creator/loki/feature/voice/service/VoiceChannelManager.java index 9dfbc1a..c4f241c 100644 --- a/src/main/java/creator/loki/feature/voice/service/VoiceChannelManager.java +++ b/src/main/java/creator/loki/feature/voice/service/VoiceChannelManager.java @@ -1,6 +1,5 @@ package creator.loki.feature.voice.service; -import java.awt.Color; import java.util.EnumSet; import creator.loki.feature.config.repository.ConfigRepository; @@ -8,10 +7,7 @@ import creator.loki.feature.voice.VoiceChannelData; import creator.loki.feature.voice.repository.VoiceChannelRepository; 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; @@ -25,7 +21,7 @@ public class VoiceChannelManager { Permission.MANAGE_CHANNEL, Permission.VOICE_CONNECT, Permission.VOICE_MOVE_OTHERS, Permission.VOICE_MUTE_OTHERS); private final VoiceChannelRepository channelRepo; - private final VoicePresetRepository presetRepo; + private final VoiceChannelActions channelActions; private String triggerChannelId; private String categoryId; @@ -34,7 +30,7 @@ public class VoiceChannelManager { VoicePresetRepository presetRepo, final ConfigRepository configRepo) { this.channelRepo = channelRepo; - this.presetRepo = presetRepo; + this.channelActions = new VoiceChannelActions(presetRepo); this.triggerChannelId = configRepo.get("voice.trigger_channel_id"); this.categoryId = configRepo.get("voice.category_id"); } @@ -51,7 +47,7 @@ public class VoiceChannelManager { logger, "Создан личный канал %s для пользователя %s" .formatted(channel.getId(), member.getEffectiveName())); - applyUserPreset(channel, member.getId()); + channelActions.applyUserPreset(channel, member.getId()); return channel; } catch (Exception e) { LoggerDecorator.error(logger, "Ошибка при создании личного канала", e); @@ -107,55 +103,19 @@ public class VoiceChannelManager { saveUserPreset(userId, preset); } - public void sendControlPanel(final VoiceChannel channel, final Member owner) { - var limitText = channel.getUserLimit() == 0 ? "∞" : String.valueOf(channel.getUserLimit()); - var embed = - new EmbedBuilder() - .setTitle("🎛️ Управление каналом") - .setDescription("Ваш личный голосовой канал") - .addField("Владелец", owner.getAsMention(), true) - .addField("Лимит", limitText, true) - .setColor(Color.CYAN) - .setFooter("Управляйте каналом с помощью кнопок ниже"); - 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(final String userId, VoicePreset preset) { - presetRepo.savePreset(userId, preset); - LoggerDecorator.voice(logger, "Сохранён пресет для пользователя " + userId); + channelActions.savePreset(userId, preset); } public VoicePreset getUserPreset(final String userId) { - return presetRepo.getPreset(userId) != null ? presetRepo.getPreset(userId) : new VoicePreset(); + return channelActions.getPreset(userId); } public void applyUserPreset(final VoiceChannel channel, final String userId) { - var preset = getUserPreset(userId); - try { - if (preset.getName() != null && !preset.getName().isEmpty()) { - channel.getManager().setName(preset.getName()).queue(); - } - if (preset.getLimit() >= 0) { - channel.getManager().setUserLimit(preset.getLimit()).queue(); - } - if (preset.getBitrate() > 0) { - channel.getManager().setBitrate(preset.getBitrate()).queue(); - } - if (preset.isLocked()) { - var publicRole = channel.getGuild().getPublicRole(); - channel.upsertPermissionOverride(publicRole).deny(Permission.VOICE_CONNECT).queue(); - } - LoggerDecorator.voice( - logger, "Применён пресет пользователя " + userId + " к каналу " + channel.getId()); - } catch (Exception e) { - LoggerDecorator.error(logger, "Ошибка при применении пресета", e); - } + channelActions.applyUserPreset(channel, userId); + } + + public void sendControlPanel(final VoiceChannel channel, final Member owner) { + channelActions.sendControlPanel(channel, owner); } } diff --git a/src/main/java/creator/loki/feature/voice/service/VoiceSetupEmbeds.java b/src/main/java/creator/loki/feature/voice/service/VoiceSetupEmbeds.java new file mode 100644 index 0000000..919a7c9 --- /dev/null +++ b/src/main/java/creator/loki/feature/voice/service/VoiceSetupEmbeds.java @@ -0,0 +1,62 @@ +package creator.loki.feature.voice.service; + +import java.awt.Color; + +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import net.dv8tion.jda.api.entities.MessageEmbed; + +final class VoiceSetupEmbeds { + private VoiceSetupEmbeds() {} + + static MessageEmbed triggerSet(final String mention) { + return EmbedFactory.create() + .withTitle("✅ Канал-триггер установлен") + .withDescription("Теперь при входе в " + mention + " будет создаваться личный канал") + .withColor(Color.GREEN) + .build(); + } + + static MessageEmbed categorySet(final String name) { + return EmbedFactory.create() + .withTitle("✅ Категория установлена") + .withDescription("Личные каналы будут создаваться в категории: **" + name + "**") + .withColor(Color.GREEN) + .build(); + } + + static MessageEmbed info( + final CommandContext context, final String triggerId, final String categoryId) { + var embed = + EmbedFactory.create() + .withTitle("🎤 Конфигурация системы личных каналов") + .withColor(Color.BLUE); + if (triggerId != null) { + embed.addField("Канал-триггер", "<#" + triggerId + ">", false); + } else { + embed.addField("Канал-триггер", "❌ Не установлен", false); + } + if (categoryId != null) { + var category = context.getGuild().getCategoryById(categoryId); + embed.addField("Категория", category != null ? category.getName() : "❌ Не найдена", false); + } else { + embed.addField("Категория", "❌ Не установлена", false); + } + return embed.build(); + } + + static MessageEmbed help() { + var desc = + "**Доступные команды:**\n" + + "• `!voice-setup trigger` - `#войс` или числовой id канала\n" + + "• `!voice-setup category` - `#категория` или id\n" + + "• `!voice-setup info` - Показать текущую конфигурацию\n\n" + + "Текстовые каналы для управления создаются автоматически" + + " при создании войса.\nТолько для администраторов!"; + return EmbedFactory.create() + .withTitle("🎤 Настройка голосовых каналов") + .withDescription(desc) + .withColor(Color.BLUE) + .build(); + } +} diff --git a/src/main/java/creator/loki/feature/voice/service/VoiceSetupPrefixCommand.java b/src/main/java/creator/loki/feature/voice/service/VoiceSetupPrefixCommand.java index 738b7ed..674056d 100644 --- a/src/main/java/creator/loki/feature/voice/service/VoiceSetupPrefixCommand.java +++ b/src/main/java/creator/loki/feature/voice/service/VoiceSetupPrefixCommand.java @@ -1,12 +1,10 @@ package creator.loki.feature.voice.service; -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.feature.config.repository.ConfigRepository; import creator.loki.util.GuildChannelResolve; import net.dv8tion.jda.api.Permission; @@ -79,14 +77,10 @@ public class VoiceSetupPrefixCommand implements Command { return; } configRepo.set("voice.trigger_channel_id", channel.getId()); - var embed = - EmbedFactory.create() - .withTitle("✅ Канал-триггер установлен") - .withDescription( - "Теперь при входе в " + channel.getAsMention() + " будет создаваться личный канал") - .withColor(Color.GREEN) - .build(); - context.getChannel().sendMessageEmbeds(embed).queue(); + context + .getChannel() + .sendMessageEmbeds(VoiceSetupEmbeds.triggerSet(channel.getAsMention())) + .queue(); } private void handleCategory(final CommandContext context, final List args) { @@ -107,53 +101,22 @@ public class VoiceSetupPrefixCommand implements Command { return; } configRepo.set("voice.category_id", category.getId()); - var embed = - EmbedFactory.create() - .withTitle("✅ Категория установлена") - .withDescription( - "Личные каналы будут создаваться в категории: **" + category.getName() + "**") - .withColor(Color.GREEN) - .build(); - context.getChannel().sendMessageEmbeds(embed).queue(); + context + .getChannel() + .sendMessageEmbeds(VoiceSetupEmbeds.categorySet(category.getName())) + .queue(); } private void handleInfo(final CommandContext context) { 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 (categoryId != null) { - var category = context.getGuild().getCategoryById(categoryId); - embed.addField("Категория", category != null ? category.getName() : "❌ Не найдена", false); - } else { - embed.addField("Категория", "❌ Не установлена", false); - } - context.getChannel().sendMessageEmbeds(embed.build()).queue(); + context + .getChannel() + .sendMessageEmbeds(VoiceSetupEmbeds.info(context, triggerId, categoryId)) + .queue(); } private void sendHelp(final 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(desc) - .withColor(Color.BLUE) - .build()) - .queue(); + context.getChannel().sendMessageEmbeds(VoiceSetupEmbeds.help()).queue(); } }