From 78645e572713db8b23e1b5e5c05680afcbc219ea Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Wed, 19 Aug 2026 13:52:54 +0200 Subject: [PATCH] fix: ratings reliability + ratingstop/ratingbottom board commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - generateratings: MemberCachePolicy.ALL + ChunkingFilter.ALL + guild.loadMembers() (JDA cached only 2 members on small guilds by default) - RatingService: new Random(seed) per call instead of shared Random.setSeed (race) - LogEmbeds ban/kick: null reason -> 'Не указана' - RatingsRepository.getTopRatings/getBottomRatings (ORDER BY value, LIMIT) - RatingBoardCommand: ratingstop/ratingbottom, top-3 medals, guild-scoped --- .../java/creator/loki/core/JdaFactory.java | 6 +- .../commands/GenerateRatingsCommand.java | 2 +- .../entertainment/service/RatingService.java | 4 +- .../feature/moderation/log/LogEmbeds.java | 4 +- .../commands/board/RatingBoardCommand.java | 130 ++++++++++++++++++ .../rating/repository/RatingsRepository.java | 43 ++++++ .../utility/UtilityCommandRegistrar.java | 3 + .../repository/RatingsRepositoryTest.java | 60 ++++++++ 8 files changed, 245 insertions(+), 7 deletions(-) create mode 100644 src/main/java/creator/loki/feature/rating/commands/board/RatingBoardCommand.java diff --git a/src/main/java/creator/loki/core/JdaFactory.java b/src/main/java/creator/loki/core/JdaFactory.java index 4624da4..4e268a0 100644 --- a/src/main/java/creator/loki/core/JdaFactory.java +++ b/src/main/java/creator/loki/core/JdaFactory.java @@ -7,6 +7,8 @@ import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.OnlineStatus; import net.dv8tion.jda.api.entities.Activity; import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.ChunkingFilter; +import net.dv8tion.jda.api.utils.MemberCachePolicy; import net.dv8tion.jda.api.utils.cache.CacheFlag; public final class JdaFactory { @@ -18,7 +20,9 @@ public final class JdaFactory { .setStatus(OnlineStatus.ONLINE) .setActivity(Activity.playing(activity)) .enableIntents(List.of(intents)) - .enableCache(CacheFlag.VOICE_STATE); + .enableCache(CacheFlag.VOICE_STATE) + .setMemberCachePolicy(MemberCachePolicy.ALL) + .setChunkingFilter(ChunkingFilter.ALL); } public static JDA awaitJda(final JDA 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 bc21692..fcef1d3 100644 --- a/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java +++ b/src/main/java/creator/loki/feature/admin/commands/GenerateRatingsCommand.java @@ -69,7 +69,7 @@ public class GenerateRatingsCommand implements Command { var guildId = context.getGuild().getId(); var memberCount = 0; var generatedCount = 0; - for (Member member : context.getGuild().getMembers()) { + for (Member member : context.getGuild().loadMembers().get()) { if (member.getUser().isBot()) { continue; } diff --git a/src/main/java/creator/loki/feature/entertainment/service/RatingService.java b/src/main/java/creator/loki/feature/entertainment/service/RatingService.java index 584d07b..7400a67 100644 --- a/src/main/java/creator/loki/feature/entertainment/service/RatingService.java +++ b/src/main/java/creator/loki/feature/entertainment/service/RatingService.java @@ -12,7 +12,6 @@ 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; @@ -25,8 +24,7 @@ public class RatingService { public int getDeterministicValue( final String userId, final int salt, final int min, final int max) { long seed = Long.parseLong(userId) + salt; - RANDOM.setSeed(seed); - return RANDOM.nextInt(max - min + 1) + min; + return new Random(seed).nextInt(max - min + 1) + min; } public Member resolveTarget(final CommandContext context) { diff --git a/src/main/java/creator/loki/feature/moderation/log/LogEmbeds.java b/src/main/java/creator/loki/feature/moderation/log/LogEmbeds.java index f3cc3e5..c1fd883 100644 --- a/src/main/java/creator/loki/feature/moderation/log/LogEmbeds.java +++ b/src/main/java/creator/loki/feature/moderation/log/LogEmbeds.java @@ -47,7 +47,7 @@ public final class LogEmbeds { .setTitle("🔨 Пользователь забанен") .addField("Пользователь", userMention, true) .addField("Модератор", moderatorMention, true) - .addField("Причина", reason, false) + .addField("Причина", reason != null ? reason : "Не указана", false) .setColor(Color.RED) .setThumbnail(avatarUrl); } @@ -71,7 +71,7 @@ public final class LogEmbeds { .setTitle("👢 Пользователь кикнут") .addField("Пользователь", userMention, true) .addField("Модератор", moderatorMention, true) - .addField("Причина", reason, false) + .addField("Причина", reason != null ? reason : "Не указана", false) .setColor(Color.ORANGE) .setThumbnail(avatarUrl); } diff --git a/src/main/java/creator/loki/feature/rating/commands/board/RatingBoardCommand.java b/src/main/java/creator/loki/feature/rating/commands/board/RatingBoardCommand.java new file mode 100644 index 0000000..2c031e0 --- /dev/null +++ b/src/main/java/creator/loki/feature/rating/commands/board/RatingBoardCommand.java @@ -0,0 +1,130 @@ +package creator.loki.feature.rating.commands.board; + +import java.awt.Color; +import java.util.List; +import java.util.Set; + +import creator.loki.command.Command; +import creator.loki.command.CommandCategories; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.feature.rating.repository.RatingsRepository; + +/** Топ/анти-топ пользователей по значению рейтинга (после !generateratings). */ +public class RatingBoardCommand implements Command { + + private static final Set TYPES = + Set.of("ship", "gay", "iq", "pp", "mustache", "fat", "boykisser"); + private static final String DEFAULT_TYPE = "iq"; + private static final int BOARD_SIZE = 10; + + public enum Flavor { + TOP("ratingstop", "Топ-10 по значению рейтинга", "🏆", new Color(255, 215, 0), true), + BOTTOM( + "ratingbottom", "Анти-топ-10 по значению рейтинга", "📉", new Color(176, 196, 222), false); + + final String name; + final String description; + final String emoji; + final Color color; + final boolean descending; + + Flavor(String name, String description, String emoji, Color color, boolean descending) { + this.name = name; + this.description = description; + this.emoji = emoji; + this.color = color; + this.descending = descending; + } + } + + private final Flavor flavor; + private final RatingsRepository ratingsRepository; + + public RatingBoardCommand(Flavor flavor, RatingsRepository ratingsRepository) { + this.flavor = flavor; + this.ratingsRepository = ratingsRepository; + } + + @Override + public String getName() { + return flavor.name; + } + + @Override + public String getDescription() { + return flavor.description; + } + + @Override + public String getUsage() { + return flavor.name + " [тип]"; + } + + @Override + public String getCategory() { + return CommandCategories.MEASUREMENTS; + } + + @Override + public void execute(final CommandContext context) { + if (context.getGuild() == null) { + context.getChannel().sendMessage("❌ Команда работает только на сервере.").queue(); + return; + } + + String type = context.getArgs().isEmpty() ? DEFAULT_TYPE : context.getArgs().get(0); + if (!TYPES.contains(type)) { + context + .getChannel() + .sendMessage("❌ Неизвестный тип: `" + type + "`. Доступно: " + String.join(", ", TYPES)) + .queue(); + return; + } + + List board = + flavor.descending + ? ratingsRepository.getTopRatings(context.getGuild().getId(), type, BOARD_SIZE) + : ratingsRepository.getBottomRatings(context.getGuild().getId(), type, BOARD_SIZE); + + if (board.isEmpty()) { + context + .getChannel() + .sendMessage("❌ Пока никто не имеет рейтинга `" + type + "`. Сначала `!generateratings`.") + .queue(); + return; + } + + StringBuilder description = new StringBuilder(); + for (int i = 0; i < board.size(); i++) { + RatingsRepository.RatingEntry entry = board.get(i); + String medal = + switch (i) { + case 0 -> "🥇"; + case 1 -> "🥈"; + case 2 -> "🥉"; + default -> "⭐"; + }; + description + .append(medal) + .append(" **#") + .append(i + 1) + .append("** <@") + .append(entry.userId()) + .append("> — **") + .append(entry.value()) + .append("**\n"); + } + + context + .getChannel() + .sendMessageEmbeds( + EmbedFactory.create() + .withTitle(flavor.emoji + " " + flavor.name + " · " + type) + .withDescription(description.toString()) + .withColor(flavor.color) + .withFooter("Типы: " + String.join(", ", TYPES)) + .build()) + .queue(); + } +} diff --git a/src/main/java/creator/loki/feature/rating/repository/RatingsRepository.java b/src/main/java/creator/loki/feature/rating/repository/RatingsRepository.java index dd1c7d2..4c27e45 100644 --- a/src/main/java/creator/loki/feature/rating/repository/RatingsRepository.java +++ b/src/main/java/creator/loki/feature/rating/repository/RatingsRepository.java @@ -3,7 +3,9 @@ package creator.loki.feature.rating.repository; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import creator.loki.database.Database; @@ -134,4 +136,45 @@ public class RatingsRepository { logger.error("Ошибка удаления всех рейтингов на сервере", e); } } + + /** Пользователь и его значение рейтинга */ + public record RatingEntry(String userId, int value) {} + + /** Топ-N пользователей с самым высоким значением указанного типа рейтинга */ + public List getTopRatings( + final String guildId, final String ratingType, final int limit) { + return getRatingBoard(guildId, ratingType, limit, true); + } + + /** Топ-N пользователей с самым низким значением указанного типа рейтинга */ + public List getBottomRatings( + final String guildId, final String ratingType, final int limit) { + return getRatingBoard(guildId, ratingType, limit, false); + } + + private List getRatingBoard( + final String guildId, final String ratingType, final int limit, final boolean descending) { + String sql = + "SELECT user_id, value FROM ratings WHERE guild_id = ? AND rating_type = ?" + + " ORDER BY value " + + (descending ? "DESC" : "ASC") + + " LIMIT ?"; + List entries = new ArrayList<>(); + + try (PreparedStatement stmt = database.prepare(sql)) { + stmt.setString(1, guildId); + stmt.setString(2, ratingType); + stmt.setInt(3, limit); + + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + entries.add(new RatingEntry(rs.getString("user_id"), rs.getInt("value"))); + } + } + } catch (SQLException e) { + logger.error("Ошибка получения борда рейтинга {} для сервера {}", ratingType, guildId, e); + } + + return entries; + } } diff --git a/src/main/java/creator/loki/feature/utility/UtilityCommandRegistrar.java b/src/main/java/creator/loki/feature/utility/UtilityCommandRegistrar.java index c3e6baf..d98c0fa 100644 --- a/src/main/java/creator/loki/feature/utility/UtilityCommandRegistrar.java +++ b/src/main/java/creator/loki/feature/utility/UtilityCommandRegistrar.java @@ -17,6 +17,7 @@ import creator.loki.feature.info.commands.PingCommand; import creator.loki.feature.info.commands.ServerStatsCommand; import creator.loki.feature.metrics.MetricsCollector; import creator.loki.feature.metrics.commands.MetricsCommand; +import creator.loki.feature.rating.commands.board.RatingBoardCommand; import creator.loki.feature.rating.repository.RatingsRepository; import creator.loki.feature.reputation.MinusRepCommand; import creator.loki.feature.reputation.RepCommand; @@ -78,6 +79,8 @@ public class UtilityCommandRegistrar implements CommandRegistrar { registry.register(new CounterSetupPrefixCommand(counterManager, configRepository)); registry.register(new GenerateRatingsCommand(ratingsRepository, config)); + registry.register(new RatingBoardCommand(RatingBoardCommand.Flavor.TOP, ratingsRepository)); + registry.register(new RatingBoardCommand(RatingBoardCommand.Flavor.BOTTOM, ratingsRepository)); registry.register(new RepCommand(reputationRepository, cooldownManager)); registry.register(new MinusRepCommand(reputationRepository, cooldownManager)); diff --git a/src/test/java/creator/loki/feature/rating/repository/RatingsRepositoryTest.java b/src/test/java/creator/loki/feature/rating/repository/RatingsRepositoryTest.java index ea6c697..3a2aaba 100644 --- a/src/test/java/creator/loki/feature/rating/repository/RatingsRepositoryTest.java +++ b/src/test/java/creator/loki/feature/rating/repository/RatingsRepositoryTest.java @@ -199,4 +199,64 @@ class RatingsRepositoryTest { repository.deleteAllRatings("nonexistent"); } } + + @Nested + class RatingBoard { + @Test + void topRatingsDescending() { + repository.setRating("guild1", "user1", "iq", 100); + repository.setRating("guild1", "user2", "iq", 150); + repository.setRating("guild1", "user3", "iq", 50); + + var top = repository.getTopRatings("guild1", "iq", 10); + assertEquals(3, top.size()); + assertEquals("user2", top.get(0).userId()); + assertEquals(150, top.get(0).value()); + assertEquals("user3", top.get(2).userId()); + assertEquals(50, top.get(2).value()); + } + + @Test + void topRatingsRespectsLimit() { + repository.setRating("guild1", "user1", "iq", 100); + repository.setRating("guild1", "user2", "iq", 150); + repository.setRating("guild1", "user3", "iq", 50); + + var top = repository.getTopRatings("guild1", "iq", 2); + assertEquals(2, top.size()); + assertEquals("user2", top.get(0).userId()); + assertEquals("user1", top.get(1).userId()); + } + + @Test + void bottomRatingsAscending() { + repository.setRating("guild1", "user1", "iq", 100); + repository.setRating("guild1", "user2", "iq", 150); + repository.setRating("guild1", "user3", "iq", 50); + + var bottom = repository.getBottomRatings("guild1", "iq", 10); + assertEquals(3, bottom.size()); + assertEquals("user3", bottom.get(0).userId()); + assertEquals(50, bottom.get(0).value()); + assertEquals("user2", bottom.get(2).userId()); + assertEquals(150, bottom.get(2).value()); + } + + @Test + void boardFiltersByGuildAndType() { + repository.setRating("guild1", "user1", "iq", 100); + repository.setRating("guild2", "user1", "iq", 200); + repository.setRating("guild1", "user1", "gay", 90); + + assertEquals(1, repository.getTopRatings("guild1", "iq", 10).size()); + assertEquals(200, repository.getTopRatings("guild2", "iq", 10).get(0).value()); + assertEquals(90, repository.getTopRatings("guild1", "gay", 10).get(0).value()); + } + + @Test + void boardEmptyWhenNoRatings() { + assertTrue(repository.getTopRatings("guild1", "iq", 10).isEmpty()); + assertTrue(repository.getBottomRatings("guild1", "iq", 10).isEmpty()); + } + } }