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.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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.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<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.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));
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue