fix: ratings reliability + ratingstop/ratingbottom board commands
- 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
This commit is contained in:
parent
7cf4b9debb
commit
78645e5727
8 changed files with 245 additions and 7 deletions
|
|
@ -7,6 +7,8 @@ import net.dv8tion.jda.api.JDABuilder;
|
||||||
import net.dv8tion.jda.api.OnlineStatus;
|
import net.dv8tion.jda.api.OnlineStatus;
|
||||||
import net.dv8tion.jda.api.entities.Activity;
|
import net.dv8tion.jda.api.entities.Activity;
|
||||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
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;
|
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
||||||
|
|
||||||
public final class JdaFactory {
|
public final class JdaFactory {
|
||||||
|
|
@ -18,7 +20,9 @@ public final class JdaFactory {
|
||||||
.setStatus(OnlineStatus.ONLINE)
|
.setStatus(OnlineStatus.ONLINE)
|
||||||
.setActivity(Activity.playing(activity))
|
.setActivity(Activity.playing(activity))
|
||||||
.enableIntents(List.of(intents))
|
.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) {
|
public static JDA awaitJda(final JDA jda) {
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ public class GenerateRatingsCommand implements Command {
|
||||||
var guildId = context.getGuild().getId();
|
var guildId = context.getGuild().getId();
|
||||||
var memberCount = 0;
|
var memberCount = 0;
|
||||||
var generatedCount = 0;
|
var generatedCount = 0;
|
||||||
for (Member member : context.getGuild().getMembers()) {
|
for (Member member : context.getGuild().loadMembers().get()) {
|
||||||
if (member.getUser().isBot()) {
|
if (member.getUser().isBot()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class RatingService {
|
public class RatingService {
|
||||||
private static final Random RANDOM = new Random();
|
|
||||||
private static final Duration COOLDOWN = Duration.ofSeconds(5);
|
private static final Duration COOLDOWN = Duration.ofSeconds(5);
|
||||||
|
|
||||||
private final CooldownManager cooldownManager;
|
private final CooldownManager cooldownManager;
|
||||||
|
|
@ -25,8 +24,7 @@ public class RatingService {
|
||||||
public int getDeterministicValue(
|
public int getDeterministicValue(
|
||||||
final String userId, final int salt, final int min, final int max) {
|
final String userId, final int salt, final int min, final int max) {
|
||||||
long seed = Long.parseLong(userId) + salt;
|
long seed = Long.parseLong(userId) + salt;
|
||||||
RANDOM.setSeed(seed);
|
return new Random(seed).nextInt(max - min + 1) + min;
|
||||||
return RANDOM.nextInt(max - min + 1) + min;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Member resolveTarget(final CommandContext context) {
|
public Member resolveTarget(final CommandContext context) {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ public final class LogEmbeds {
|
||||||
.setTitle("🔨 Пользователь забанен")
|
.setTitle("🔨 Пользователь забанен")
|
||||||
.addField("Пользователь", userMention, true)
|
.addField("Пользователь", userMention, true)
|
||||||
.addField("Модератор", moderatorMention, true)
|
.addField("Модератор", moderatorMention, true)
|
||||||
.addField("Причина", reason, false)
|
.addField("Причина", reason != null ? reason : "Не указана", false)
|
||||||
.setColor(Color.RED)
|
.setColor(Color.RED)
|
||||||
.setThumbnail(avatarUrl);
|
.setThumbnail(avatarUrl);
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +71,7 @@ public final class LogEmbeds {
|
||||||
.setTitle("👢 Пользователь кикнут")
|
.setTitle("👢 Пользователь кикнут")
|
||||||
.addField("Пользователь", userMention, true)
|
.addField("Пользователь", userMention, true)
|
||||||
.addField("Модератор", moderatorMention, true)
|
.addField("Модератор", moderatorMention, true)
|
||||||
.addField("Причина", reason, false)
|
.addField("Причина", reason != null ? reason : "Не указана", false)
|
||||||
.setColor(Color.ORANGE)
|
.setColor(Color.ORANGE)
|
||||||
.setThumbnail(avatarUrl);
|
.setThumbnail(avatarUrl);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<String> 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<RatingsRepository.RatingEntry> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,9 @@ package creator.loki.feature.rating.repository;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import creator.loki.database.Database;
|
import creator.loki.database.Database;
|
||||||
|
|
@ -134,4 +136,45 @@ public class RatingsRepository {
|
||||||
logger.error("Ошибка удаления всех рейтингов на сервере", e);
|
logger.error("Ошибка удаления всех рейтингов на сервере", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Пользователь и его значение рейтинга */
|
||||||
|
public record RatingEntry(String userId, int value) {}
|
||||||
|
|
||||||
|
/** Топ-N пользователей с самым высоким значением указанного типа рейтинга */
|
||||||
|
public List<RatingEntry> getTopRatings(
|
||||||
|
final String guildId, final String ratingType, final int limit) {
|
||||||
|
return getRatingBoard(guildId, ratingType, limit, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Топ-N пользователей с самым низким значением указанного типа рейтинга */
|
||||||
|
public List<RatingEntry> getBottomRatings(
|
||||||
|
final String guildId, final String ratingType, final int limit) {
|
||||||
|
return getRatingBoard(guildId, ratingType, limit, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<RatingEntry> 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<RatingEntry> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import creator.loki.feature.info.commands.PingCommand;
|
||||||
import creator.loki.feature.info.commands.ServerStatsCommand;
|
import creator.loki.feature.info.commands.ServerStatsCommand;
|
||||||
import creator.loki.feature.metrics.MetricsCollector;
|
import creator.loki.feature.metrics.MetricsCollector;
|
||||||
import creator.loki.feature.metrics.commands.MetricsCommand;
|
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.rating.repository.RatingsRepository;
|
||||||
import creator.loki.feature.reputation.MinusRepCommand;
|
import creator.loki.feature.reputation.MinusRepCommand;
|
||||||
import creator.loki.feature.reputation.RepCommand;
|
import creator.loki.feature.reputation.RepCommand;
|
||||||
|
|
@ -78,6 +79,8 @@ public class UtilityCommandRegistrar implements CommandRegistrar {
|
||||||
registry.register(new CounterSetupPrefixCommand(counterManager, configRepository));
|
registry.register(new CounterSetupPrefixCommand(counterManager, configRepository));
|
||||||
|
|
||||||
registry.register(new GenerateRatingsCommand(ratingsRepository, config));
|
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 RepCommand(reputationRepository, cooldownManager));
|
||||||
registry.register(new MinusRepCommand(reputationRepository, cooldownManager));
|
registry.register(new MinusRepCommand(reputationRepository, cooldownManager));
|
||||||
|
|
|
||||||
|
|
@ -199,4 +199,64 @@ class RatingsRepositoryTest {
|
||||||
repository.deleteAllRatings("nonexistent");
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue