feat: remove AnimalApi cache + !ratingoverview for super-admin
- AnimalApi: drop Caffeine URL cache (30s TTL made !neko return the same photo) - RatingsRepository.getExtremeRating(guild, type, excluded, highest) with NOT IN - !ratingoverview (super-admin only): per-category max/min, excluding bots + super-admin - RatingsRepositoryTest: 7 new tests for getExtremeRating
This commit is contained in:
parent
bf14e03b21
commit
ec25d9e8d3
6 changed files with 242 additions and 35 deletions
|
|
@ -4,8 +4,6 @@ import java.io.IOException;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.inject.Inject;
|
||||
|
|
@ -20,18 +18,15 @@ import org.slf4j.LoggerFactory;
|
|||
/**
|
||||
* Клиент к публичным API животных и аниме-картинок. Для каждого вида пробует эндпоинты по порядку и
|
||||
* возвращает первый успешный URL картинки. Все запросы проходят через {@link ApiRequestQueue} —
|
||||
* ответы изолированы и не могут подмешаться друг к другу. Успешные URL кэшируются на короткое
|
||||
* время, чтобы повторные команды отвечали мгновенно и не перегружали внешние API.
|
||||
* ответы изолированы и не могут подмешаться друг к другу.
|
||||
*/
|
||||
@Singleton
|
||||
public class AnimalApi {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AnimalApi.class);
|
||||
private static final String NEKOS_BASE = "https://nekos.life/api/v2/img/";
|
||||
private static final long CACHE_TTL_SECONDS = 30;
|
||||
|
||||
private final ApiRequestQueue queue;
|
||||
private final OkHttpClient client;
|
||||
private final Cache<String, String> cache;
|
||||
|
||||
@Inject
|
||||
public AnimalApi(final ApiRequestQueue queue) {
|
||||
|
|
@ -41,24 +36,11 @@ public class AnimalApi {
|
|||
AnimalApi(final ApiRequestQueue queue, final OkHttpClient client) {
|
||||
this.queue = queue;
|
||||
this.client = client;
|
||||
this.cache =
|
||||
Caffeine.newBuilder()
|
||||
.maximumSize(64)
|
||||
.expireAfterWrite(CACHE_TTL_SECONDS, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Возвращает URL картинки для вида или null, если все эндпоинты недоступны. */
|
||||
public String fetchImage(final AnimalKind kind) {
|
||||
String cached = cache.getIfPresent(kind.name());
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
String url = queue.submit(() -> fetchImageBlocking(kind));
|
||||
if (url != null) {
|
||||
cache.put(kind.name(), url);
|
||||
}
|
||||
return url;
|
||||
return queue.submit(() -> fetchImageBlocking(kind));
|
||||
}
|
||||
|
||||
private String fetchImageBlocking(final AnimalKind kind) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
package creator.loki.feature.rating.commands.board;
|
||||
|
||||
import java.util.HashSet;
|
||||
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.config.BotConfig;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.rating.repository.RatingsRepository;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
||||
/** Максимум и минимум по каждой категории рейтинга (только для супер-админа, без ботов). */
|
||||
public class RatingOverviewCommand implements Command {
|
||||
|
||||
private static final List<String> TYPES =
|
||||
List.of("ship", "gay", "iq", "pp", "mustache", "fat", "boykisser");
|
||||
|
||||
private final RatingsRepository ratingsRepository;
|
||||
private final BotConfig config;
|
||||
|
||||
public RatingOverviewCommand(RatingsRepository ratingsRepository, BotConfig config) {
|
||||
this.ratingsRepository = ratingsRepository;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ratingoverview";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Максимумы и минимумы рейтингов по всем категориям (только для супер-админа)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "ratingoverview";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.SUPER_ADMIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final CommandContext context) {
|
||||
if (!config.getSuperAdminId().equals(context.getAuthor().getUser().getId())) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessage("❌ Команда доступна только супер-администратору бота.")
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
Guild guild = context.getGuild();
|
||||
if (guild == null) {
|
||||
context.getChannel().sendMessage("❌ Команда работает только на сервере.").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> excluded = new HashSet<>();
|
||||
excluded.add(config.getSuperAdminId());
|
||||
for (Member member : guild.getMembers()) {
|
||||
if (member.getUser().isBot()) {
|
||||
excluded.add(member.getUser().getId());
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder description = new StringBuilder();
|
||||
for (String type : TYPES) {
|
||||
RatingsRepository.RatingEntry max =
|
||||
ratingsRepository.getExtremeRating(guild.getId(), type, excluded, true);
|
||||
RatingsRepository.RatingEntry min =
|
||||
ratingsRepository.getExtremeRating(guild.getId(), type, excluded, false);
|
||||
if (max == null || min == null) {
|
||||
continue;
|
||||
}
|
||||
description
|
||||
.append("**")
|
||||
.append(type)
|
||||
.append("**\n🏆 <@")
|
||||
.append(max.userId())
|
||||
.append("> — **")
|
||||
.append(max.value())
|
||||
.append("**\n📉 <@")
|
||||
.append(min.userId())
|
||||
.append("> — **")
|
||||
.append(min.value())
|
||||
.append("**\n\n");
|
||||
}
|
||||
|
||||
if (description.isEmpty()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessage("❌ Пока никто не имеет рейтингов. Сначала `!generateratings`.")
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("📊 Сводка рейтингов")
|
||||
.withDescription(description.toString())
|
||||
.withFooter("Без учёта ботов и супер-админа")
|
||||
.build())
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import java.sql.PreparedStatement;
|
|||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -152,6 +154,40 @@ public class RatingsRepository {
|
|||
return getRatingBoard(guildId, ratingType, limit, false);
|
||||
}
|
||||
|
||||
/** Крайнее значение рейтинга (максимум или минимум) без учёта исключённых пользователей. */
|
||||
public RatingEntry getExtremeRating(
|
||||
final String guildId,
|
||||
final String ratingType,
|
||||
final Collection<String> excludedUserIds,
|
||||
final boolean highest) {
|
||||
StringBuilder sql =
|
||||
new StringBuilder(
|
||||
"SELECT user_id, value FROM ratings WHERE guild_id = ? AND rating_type = ?");
|
||||
List<String> params = new ArrayList<>(List.of(guildId, ratingType));
|
||||
if (excludedUserIds != null && !excludedUserIds.isEmpty()) {
|
||||
sql.append(" AND user_id NOT IN (")
|
||||
.append(String.join(",", Collections.nCopies(excludedUserIds.size(), "?")))
|
||||
.append(")");
|
||||
params.addAll(excludedUserIds);
|
||||
}
|
||||
sql.append(" ORDER BY value ").append(highest ? "DESC" : "ASC").append(" LIMIT 1");
|
||||
|
||||
try (PreparedStatement stmt = database.prepare(sql.toString())) {
|
||||
for (int i = 0; i < params.size(); i++) {
|
||||
stmt.setString(i + 1, params.get(i));
|
||||
}
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return new RatingEntry(rs.getString("user_id"), rs.getInt("value"));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.error("Ошибка получения крайнего рейтинга {} для сервера {}", ratingType, guildId, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<RatingEntry> getRatingBoard(
|
||||
final String guildId, final String ratingType, final int limit, final boolean descending) {
|
||||
String sql =
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ 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.commands.board.RatingOverviewCommand;
|
||||
import creator.loki.feature.rating.repository.RatingsRepository;
|
||||
import creator.loki.feature.reputation.MinusRepCommand;
|
||||
import creator.loki.feature.reputation.RepCommand;
|
||||
|
|
@ -81,6 +82,7 @@ public class UtilityCommandRegistrar implements CommandRegistrar {
|
|||
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 RatingOverviewCommand(ratingsRepository, config));
|
||||
|
||||
registry.register(new RepCommand(reputationRepository, cooldownManager));
|
||||
registry.register(new MinusRepCommand(reputationRepository, cooldownManager));
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import creator.loki.database.Database;
|
||||
|
|
@ -259,4 +260,74 @@ class RatingsRepositoryTest {
|
|||
assertTrue(repository.getBottomRatings("guild1", "iq", 10).isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Extreme {
|
||||
@Test
|
||||
void highestWithoutExclusions() {
|
||||
repository.setRating("guild1", "user1", "iq", 100);
|
||||
repository.setRating("guild1", "user2", "iq", 150);
|
||||
|
||||
var max = repository.getExtremeRating("guild1", "iq", List.of(), true);
|
||||
assertEquals("user2", max.userId());
|
||||
assertEquals(150, max.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void lowestWithoutExclusions() {
|
||||
repository.setRating("guild1", "user1", "iq", 100);
|
||||
repository.setRating("guild1", "user2", "iq", 50);
|
||||
|
||||
var min = repository.getExtremeRating("guild1", "iq", List.of(), false);
|
||||
assertEquals("user2", min.userId());
|
||||
assertEquals(50, min.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void highestSkipsExcludedUsers() {
|
||||
repository.setRating("guild1", "user1", "iq", 100);
|
||||
repository.setRating("guild1", "bot1", "iq", 200);
|
||||
repository.setRating("guild1", "admin", "iq", 180);
|
||||
|
||||
var max = repository.getExtremeRating("guild1", "iq", List.of("bot1", "admin"), true);
|
||||
assertEquals("user1", max.userId());
|
||||
assertEquals(100, max.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void lowestSkipsExcludedUsers() {
|
||||
repository.setRating("guild1", "user1", "iq", 5);
|
||||
repository.setRating("guild1", "bot1", "iq", 1);
|
||||
repository.setRating("guild1", "admin", "iq", 3);
|
||||
|
||||
var min = repository.getExtremeRating("guild1", "iq", List.of("bot1", "admin"), false);
|
||||
assertEquals("user1", min.userId());
|
||||
assertEquals(5, min.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsNullWhenAllExcluded() {
|
||||
repository.setRating("guild1", "user1", "iq", 100);
|
||||
|
||||
var max = repository.getExtremeRating("guild1", "iq", List.of("user1"), true);
|
||||
assertNull(max);
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsNullWhenNoRatings() {
|
||||
assertNull(repository.getExtremeRating("guild1", "iq", List.of(), true));
|
||||
assertNull(repository.getExtremeRating("guild1", "iq", List.of("user1"), false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void filtersByGuildAndType() {
|
||||
repository.setRating("guild1", "user1", "iq", 100);
|
||||
repository.setRating("guild2", "user1", "iq", 200);
|
||||
repository.setRating("guild1", "user1", "gay", 90);
|
||||
|
||||
assertEquals(100, repository.getExtremeRating("guild1", "iq", List.of(), true).value());
|
||||
assertEquals(200, repository.getExtremeRating("guild2", "iq", List.of(), true).value());
|
||||
assertEquals(90, repository.getExtremeRating("guild1", "gay", List.of(), true).value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue