feat: ai page parser, message scanner, lang filter, /clear, prompt rewrite

This commit is contained in:
loki5512344 2026-07-07 01:22:21 +02:00
parent a111bcd674
commit cc046d7ee6
65 changed files with 1631 additions and 745 deletions

View file

@ -10,7 +10,6 @@ public class BotConfig {
private final String superAdminId;
private final String dbPath;
private final String deepseekApiKey;
private final String giphyApiKey;
private final String aiChannelId;
private final int aiContextMsgs;
private final int aiMsgLength;
@ -29,7 +28,6 @@ public class BotConfig {
String superAdminId,
String dbPath,
String deepseekApiKey,
String giphyApiKey,
String aiChannelId,
int aiContextMsgs,
int aiMsgLength,
@ -46,7 +44,6 @@ public class BotConfig {
this.superAdminId = superAdminId;
this.dbPath = dbPath;
this.deepseekApiKey = deepseekApiKey;
this.giphyApiKey = giphyApiKey;
this.aiChannelId = aiChannelId;
this.aiContextMsgs = aiContextMsgs;
this.aiMsgLength = aiMsgLength;
@ -96,10 +93,6 @@ public class BotConfig {
return deepseekApiKey;
}
public String getGiphyApiKey() {
return giphyApiKey;
}
public String getAiChannelId() {
return aiChannelId;
}

View file

@ -74,7 +74,6 @@ public final class ConfigLoader {
}
String deepseekApiKey = dotenv.get("DEEPSEEK_API_KEY");
String giphyApiKey = dotenv.get("GIPHY_API_KEY");
String aiChannelId = dotenv.get("AI_CHANNEL_ID");
int aiContextMsgs = Integer.parseInt(getEnv(dotenv, "AI_CONTEXT_MSGS", "10"));
@ -95,7 +94,6 @@ public final class ConfigLoader {
superAdminId,
dbPath,
deepseekApiKey,
giphyApiKey,
aiChannelId,
aiContextMsgs,
aiMsgLength,

View file

@ -68,10 +68,18 @@ public class BotInitializer {
LoggerDecorator.startup(logger, "Инициализация main бота...");
registrar.registerAll();
JDA mainJda = buildMainJDA();
mainJda.upsertCommand(
Commands.slash("agr", "Наслать AI на пользователя (только для админов)")
.addOption(OptionType.USER, "user", "Цель (по умолчанию Andrert_)", false)
).queue();
mainJda
.upsertCommand(
Commands.slash("agr", "Наслать AI на пользователя (только для админов)")
.addOption(OptionType.USER, "user", "Цель (по умолчанию Andrert_)", false))
.queue();
mainJda
.upsertCommand(
Commands.slash("clear", "Удалить сообщения")
.addOption(OptionType.INTEGER, "amount", "Количество (1-100)", true)
.addOption(
OptionType.USER, "user", "Оставить только сообщения пользователя", false))
.queue();
muteScheduler.restorePending(mainJda);
mainJda.getGuilds().forEach(g -> memberCounterListener.startScheduler(g));
healthHttpServer.start();

View file

@ -6,17 +6,17 @@ import creator.loki.bootstrap.ReadyListener;
import creator.loki.feature.counter.service.MemberCounterListener;
import creator.loki.feature.economy.FishMessageListener;
import creator.loki.feature.help.HelpMenuHandler;
import creator.loki.feature.roast.AgrSlashCommands;
import creator.loki.feature.roast.AndrertListener;
import creator.loki.feature.leaderboard.LeaderboardButtonListener;
import creator.loki.feature.level.listener.MessageListener;
import creator.loki.feature.level.listener.ResetLevelsButtonHandler;
import creator.loki.feature.level.service.VoiceTracker;
import creator.loki.feature.moderation.commands.utils.ClearSlashCommands;
import creator.loki.feature.moderation.log.ModerationLogger;
import creator.loki.feature.music.MusicSlashCommands;
import creator.loki.feature.roast.AndrertListener;
import creator.loki.feature.voice.handler.VoiceButtonHandler;
import creator.loki.feature.voice.listener.VoiceChannelListener;
import creator.loki.feature.voice.handler.VoiceModalHandler;
import creator.loki.feature.voice.listener.VoiceChannelListener;
import creator.loki.feature.voice.listener.VoiceSyncListener;
import creator.loki.feature.welcome.WelcomeListener;
@ -42,7 +42,8 @@ public class ListenerAggregator {
MessageCache messageCache,
MusicSlashCommands musicSlashCommands,
FishMessageListener fishMessageListener,
AndrertListener andrertListener) {
AndrertListener andrertListener,
ClearSlashCommands clearSlashCommands) {
this.listeners =
new Object[] {
readyListener,
@ -61,7 +62,8 @@ public class ListenerAggregator {
messageCache,
musicSlashCommands,
fishMessageListener,
andrertListener
andrertListener,
clearSlashCommands
};
}

View file

@ -48,6 +48,7 @@ public class DatabaseMigration {
LoggerDecorator.success(logger, "Миграция завершена успешно");
}
@SuppressWarnings("unchecked")
private Map<String, Object> parseJson(String path) {
try (var reader = new InputStreamReader(new FileInputStream(path), UTF_8)) {
var type = new TypeToken<Map<String, Object>>() {}.getType();
@ -58,6 +59,7 @@ public class DatabaseMigration {
}
}
@SuppressWarnings("unchecked")
private void migrateUserStats(Map<String, Object> data) {
var userStats = (Map<String, Object>) data.get("userStats");
if (userStats == null) return;
@ -82,6 +84,7 @@ public class DatabaseMigration {
}
}
@SuppressWarnings("unchecked")
private void migrateReputation(Map<String, Object> data) {
var reputation = (Map<String, Object>) data.get("reputation");
if (reputation == null) return;
@ -99,6 +102,7 @@ public class DatabaseMigration {
}
}
@SuppressWarnings("unchecked")
private void migrateShipPairs(Map<String, Object> data) {
var shipPairs = (Map<String, Object>) data.get("shipPairs");
if (shipPairs == null) return;
@ -121,6 +125,7 @@ public class DatabaseMigration {
}
}
@SuppressWarnings("unchecked")
private void migrateCustomRoles(Map<String, Object> data) {
var customRoles = (Map<String, Object>) data.get("custom_roles");
if (customRoles == null) return;

View file

@ -13,7 +13,8 @@ import creator.loki.feature.reputation.repository.ReputationRepository;
import creator.loki.feature.ship.repository.ShipPairRepository;
public class DatabaseModule extends AbstractModule {
@Provides @Singleton
@Provides
@Singleton
Database provideDatabase(BotConfig config) {
Database database = new Database(config.getDbPath());
database.connect();
@ -22,32 +23,38 @@ public class DatabaseModule extends AbstractModule {
return database;
}
@Provides @Singleton
@Provides
@Singleton
ConfigRepository provideConfigRepository(Database database) {
return new ConfigRepository(database);
}
@Provides @Singleton
@Provides
@Singleton
GuildConfigRepository provideGuildConfigRepository(ConfigRepository cr) {
return new PrefixedGuildConfigRepository(cr);
}
@Provides @Singleton
@Provides
@Singleton
RatingsRepository provideRatingsRepository(Database database) {
return new RatingsRepository(database);
}
@Provides @Singleton
@Provides
@Singleton
ReputationRepository provideReputationRepository(Database database) {
return new ReputationRepository(database);
}
@Provides @Singleton
@Provides
@Singleton
ShipPairRepository provideShipPairRepository(Database database) {
return new ShipPairRepository(database);
}
@Provides @Singleton
@Provides
@Singleton
QueueRepository provideQueueRepository(Database database) {
return new QueueRepository(database);
}

View file

@ -20,8 +20,8 @@ import creator.loki.feature.counter.CounterModule;
import creator.loki.feature.economy.EconomyCommandRegistrar;
import creator.loki.feature.economy.EconomyModule;
import creator.loki.feature.entertainment.EntertainmentCommandRegistrar;
import creator.loki.feature.level.commands.LevelCommandRegistrar;
import creator.loki.feature.level.LevelModule;
import creator.loki.feature.level.commands.LevelCommandRegistrar;
import creator.loki.feature.metrics.HealthHttpServer;
import creator.loki.feature.metrics.MetricsCollector;
import creator.loki.feature.metrics.MetricsModule;
@ -76,12 +76,19 @@ public class BotModule extends AbstractModule {
commandBinder.addBinding().to(RadioCommandRegistrar.class);
}
@Provides @Singleton
BotConfig provideBotConfig() { return ConfigLoader.load(); }
@Provides
@Singleton
BotConfig provideBotConfig() {
return ConfigLoader.load();
}
@Provides @Singleton
@Provides
@Singleton
EventDispatcher provideEventDispatcher(
BotConfig config, CommandRegistry registry, RateLimiter rateLimiter, MetricsCollector metrics) {
BotConfig config,
CommandRegistry registry,
RateLimiter rateLimiter,
MetricsCollector metrics) {
return new EventDispatcher(config, registry, rateLimiter, metrics);
}
}

View file

@ -5,12 +5,13 @@ import creator.loki.config.BotConfig;
import creator.loki.core.registry.CommandRegistrar;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.core.security.CooldownManager;
import creator.loki.feature.entertainment.commands.GlazbobaCommand;
import creator.loki.feature.entertainment.commands.MemeCommand;
import creator.loki.feature.entertainment.commands.random.CoinCommand;
import creator.loki.feature.entertainment.commands.random.DiceCommand;
import creator.loki.feature.entertainment.commands.random.EightBallCommand;
import creator.loki.feature.entertainment.commands.GlazbobaCommand;
import creator.loki.feature.entertainment.commands.MemeCommand;
import creator.loki.feature.entertainment.commands.random.RandomChoiceCommand;
import creator.loki.feature.entertainment.service.RatingService;
import creator.loki.feature.rating.commands.AuraCommand;
import creator.loki.feature.rating.commands.BoykisserCommand;
import creator.loki.feature.rating.commands.FatCommand;
@ -25,7 +26,6 @@ import creator.loki.feature.rating.commands.RateCommand;
import creator.loki.feature.rating.commands.RizzCommand;
import creator.loki.feature.rating.commands.SimpCommand;
import creator.loki.feature.rating.commands.VibeCommand;
import creator.loki.feature.entertainment.service.RatingService;
import creator.loki.feature.ship.ShipCommand;
import creator.loki.feature.ship.repository.ShipPairRepository;
import creator.loki.feature.social.commands.ComplimentCommand;

View file

@ -90,7 +90,10 @@ public class GlazbobaCommand implements Command {
.addField("🎮 Активность", activities, true)
.addField("💎 Бустер", isBoosting ? "Да" : "Нет", true)
.addField("🎭 Роли", roles, false)
.withColor(target.getColor() != null ? target.getColor() : Color.BLUE)
.withColor(
target.getColors().getPrimary() != null
? target.getColors().getPrimary()
: Color.BLUE)
.withFooter("Запросил: " + context.getAuthor().getUser().getName())
.build())
.queue();

View file

@ -11,7 +11,7 @@ import java.util.function.Predicate;
import java.util.regex.Pattern;
/** Разбор хвоста сообщения и строки вариантов для {@link RandomChoiceCommand}. */
final public class ChoiceOptionParser {
public final class ChoiceOptionParser {
private static final Pattern OR_SPLIT = Pattern.compile("(?i)\\s+или\\s+");
@ -45,7 +45,7 @@ final public class ChoiceOptionParser {
return rest.substring(sp).trim();
}
public static List<String> parseOptions(String tail) {
public static List<String> parseOptions(String tail) {
String t = tail.strip();
for (SplitRule rule : RULES) {
if (rule.when.test(t)) {

View file

@ -6,11 +6,11 @@ import com.google.inject.Singleton;
import creator.loki.database.Database;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.config.repository.GuildConfigRepository;
import creator.loki.feature.level.commands.ResetLevelsCommand;
import creator.loki.feature.level.domain.XpFormula;
import creator.loki.feature.level.listener.ResetLevelsButtonHandler;
import creator.loki.feature.level.repository.SqlUserStatsRepository;
import creator.loki.feature.level.repository.UserStatsRepository;
import creator.loki.feature.level.commands.ResetLevelsCommand;
import creator.loki.feature.level.listener.ResetLevelsButtonHandler;
import creator.loki.feature.level.service.LeaderboardService;
import creator.loki.feature.level.service.LevelRoleService;
import creator.loki.feature.level.service.LevelService;

View file

@ -5,7 +5,6 @@ import creator.loki.core.registry.CommandRegistrar;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.leaderboard.LeaderboardCommand;
import creator.loki.feature.level.commands.LevelRoleCommand;
import creator.loki.feature.level.service.LeaderboardService;
import creator.loki.feature.level.service.LevelRoleService;
import creator.loki.feature.profile.command.ProfileCommand;

View file

@ -5,8 +5,8 @@ import java.awt.Color;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.embed.EmbedFactory;
import creator.loki.util.LoggerDecorator;
import creator.loki.feature.level.commands.ResetLevelsCommand;
import creator.loki.util.LoggerDecorator;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;

View file

@ -5,7 +5,6 @@ import java.awt.Color;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.level.domain.LevelProgress;
import creator.loki.feature.level.service.LevelService;
import creator.loki.util.LoggerDecorator;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;

View file

@ -10,7 +10,6 @@ import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.level.domain.LevelUpResult;
import creator.loki.feature.level.service.LevelService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;

View file

@ -20,10 +20,10 @@ import creator.loki.feature.moderation.repository.ActiveMuteRepository;
import creator.loki.feature.moderation.repository.ModerationCaseRepository;
import creator.loki.feature.moderation.repository.WarningRepository;
import creator.loki.feature.role.CustomRoleManager;
import creator.loki.feature.role.command.CreateRoleCommand;
import creator.loki.feature.role.command.DeleteRoleCommand;
import creator.loki.feature.role.command.EditRoleCommand;
import creator.loki.feature.role.command.MyRolesCommand;
import creator.loki.feature.role.command.CreateRoleCommand;
public class ModerationCommandRegistrar implements CommandRegistrar {
private final CustomRoleManager roleManager;

View file

@ -1,11 +1,15 @@
package creator.loki.feature.moderation.commands.utils;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
public class ClearCommand implements Command {
@ -16,12 +20,12 @@ public class ClearCommand implements Command {
@Override
public String getDescription() {
return "Удаляет указанное количество сообщений";
return "Удаляет сообщения. Можно фильтровать по пользователю";
}
@Override
public String getUsage() {
return "clear <количество>";
return "clear <количество> [@user]";
}
@Override
@ -48,20 +52,44 @@ public class ClearCommand implements Command {
return;
}
int clamped = Math.clamp(amount, 1, 100);
Member target = null;
if (ctx.getArgs().size() > 1) {
String arg = ctx.getArgs().get(1);
String targetId = arg.replaceAll("<@!?(\\d+)>", "$1");
if (targetId.matches("\\d+")) {
target = ctx.getGuild().getMemberById(targetId);
}
}
if (!(ctx.getChannel() instanceof TextChannel channel)) return;
final Member finalTarget = target;
channel
.getHistory()
.retrievePast(clamped + 1)
.queue(
messages -> {
List<Message> toDelete;
if (finalTarget != null) {
toDelete =
messages.stream()
.filter(m -> m.getAuthor().getId().equals(finalTarget.getId()))
.collect(Collectors.toList());
} else {
toDelete = messages;
}
if (toDelete.isEmpty()) {
err(ctx, "Нет сообщений от этого пользователя для удаления");
return;
}
channel
.deleteMessages(messages)
.deleteMessages(toDelete)
.queue(
success ->
ctx.getChannel()
.sendMessageEmbeds(
EmbedTemplates.success(
"Сообщения удалены", "Удалено сообщений: " + messages.size()))
"Сообщения удалены", "Удалено сообщений: " + toDelete.size()))
.queue(msg -> msg.delete().queueAfter(5, TimeUnit.SECONDS)),
error -> err(ctx, "Не удалось удалить сообщения: " + error.getMessage()));
});

View file

@ -0,0 +1,115 @@
package creator.loki.feature.moderation.commands.utils;
import java.util.List;
import java.util.stream.Collectors;
import com.google.inject.Singleton;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.guild.GuildReadyEvent;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
@Singleton
public class ClearSlashCommands extends ListenerAdapter {
@Override
public void onGuildReady(@NotNull GuildReadyEvent event) {
event
.getJDA()
.updateCommands()
.addCommands(
Commands.slash("clear", "Удалить сообщения")
.addOption(OptionType.INTEGER, "amount", "Количество сообщений (1-100)", true)
.addOption(
OptionType.USER, "user", "Оставить только сообщения пользователя", false))
.queue();
}
@Override
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
if (!event.getName().equals("clear")) return;
var member = event.getMember();
if (member == null || !member.hasPermission(Permission.MESSAGE_MANAGE)) {
event
.replyEmbeds(EmbedTemplates.error("Ошибка", "Нет прав на управление сообщениями"))
.setEphemeral(true)
.queue();
return;
}
event.deferReply(true).queue();
int amount = event.getOption("amount").getAsInt();
int clamped = Math.clamp(amount, 1, 100);
String targetId = null;
var userOption = event.getOption("user");
if (userOption != null) {
targetId = userOption.getAsUser().getId();
}
if (!(event.getChannel() instanceof TextChannel channel)) {
event
.getHook()
.editOriginalEmbeds(
EmbedTemplates.error("Ошибка", "Команда работает только в текстовых каналах"))
.queue();
return;
}
final String finalTargetId = targetId;
channel
.getHistory()
.retrievePast(clamped + 1)
.queue(
messages -> {
List<Message> toDelete;
if (finalTargetId != null) {
toDelete =
messages.stream()
.filter(m -> m.getAuthor().getId().equals(finalTargetId))
.collect(Collectors.toList());
} else {
toDelete = messages;
}
if (toDelete.isEmpty()) {
event
.getHook()
.editOriginalEmbeds(
EmbedTemplates.error("Ошибка", "Нет сообщений для удаления"))
.queue();
return;
}
channel
.deleteMessages(toDelete)
.queue(
success ->
event
.getHook()
.editOriginalEmbeds(
EmbedTemplates.success(
"Сообщения удалены", "Удалено сообщений: " + toDelete.size()))
.queue(),
error ->
event
.getHook()
.editOriginalEmbeds(
EmbedTemplates.error(
"Ошибка", "Не удалось удалить: " + error.getMessage()))
.queue());
},
error ->
event
.getHook()
.editOriginalEmbeds(
EmbedTemplates.error("Ошибка", "Не удалось получить историю"))
.queue());
}
}

View file

@ -28,6 +28,7 @@ public class GuildMusicManager {
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private ScheduledFuture<?> autoLeaveFuture;
@SuppressWarnings("this-escape")
public GuildMusicManager(
long guildId, LavalinkManager lavalinkManager, QueueRepository queueRepository) {
this.guildId = guildId;

View file

@ -12,9 +12,10 @@ import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.net.URI;
import creator.loki.feature.profile.model.ProfileData;
import javax.imageio.ImageIO;
import creator.loki.feature.profile.model.ProfileData;
public class RankCardGenerator {
private final RankCardRenderer renderer = new RankCardRenderer();

View file

@ -19,16 +19,24 @@ public class AuraCommand implements Command {
}
@Override
public String getName() { return "aura"; }
public String getName() {
return "aura";
}
@Override
public String getDescription() { return "Проверить свою ауру (результат всегда одинаковый)"; }
public String getDescription() {
return "Проверить свою ауру (результат всегда одинаковый)";
}
@Override
public String getUsage() { return "aura [@user]"; }
public String getUsage() {
return "aura [@user]";
}
@Override
public String getCategory() { return "Развлечения"; }
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
@ -39,26 +47,42 @@ public class AuraCommand implements Command {
int aura = ratingService.getDeterministicValue(target.getId(), 999, -50, 100);
String emoji;
Color color;
if (aura >= 80) { emoji = "🌟"; color = new Color(255, 215, 0); }
else if (aura >= 50) { emoji = "✨"; color = new Color(100, 255, 150); }
else if (aura >= 20) { emoji = "😐"; color = new Color(52, 152, 219); }
else if (aura >= 0) { emoji = "💫"; color = new Color(128, 128, 128); }
else if (aura >= -25) { emoji = "🌑"; color = new Color(231, 76, 60); }
else { emoji = "💀"; color = new Color(100, 0, 0); }
if (aura >= 80) {
emoji = "🌟";
color = new Color(255, 215, 0);
} else if (aura >= 50) {
emoji = "✨";
color = new Color(100, 255, 150);
} else if (aura >= 20) {
emoji = "😐";
color = new Color(52, 152, 219);
} else if (aura >= 0) {
emoji = "💫";
color = new Color(128, 128, 128);
} else if (aura >= -25) {
emoji = "🌑";
color = new Color(231, 76, 60);
} else {
emoji = "💀";
color = new Color(100, 0, 0);
}
StringBuilder bar = new StringBuilder();
int filled = Math.max(0, Math.min(10, (aura + 50) / 15));
for (int i = 0; i < filled; i++) bar.append("✧");
for (int i = filled; i < 10; i++) bar.append("◇");
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("✨ Аура")
.withDescription(target.getAsMention())
.addField("Сила", emoji + " **" + aura + "**\n" + bar, false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(color)
.withFooter("💡 Детерминировано вашим Discord ID")
.build()).queue();
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("✨ Аура")
.withDescription(target.getAsMention())
.addField("Сила", emoji + " **" + aura + "**\n" + bar, false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(color)
.withFooter("💡 Детерминировано вашим Discord ID")
.build())
.queue();
}
}

View file

@ -13,12 +13,29 @@ public class GlazeCommand implements Command {
private final RatingService ratingService;
@Inject
public GlazeCommand(RatingService ratingService) { this.ratingService = ratingService; }
public GlazeCommand(RatingService ratingService) {
this.ratingService = ratingService;
}
@Override public String getName() { return "glaze"; }
@Override public String getDescription() { return "На сколько ты глазированный (результат всегда одинаковый)"; }
@Override public String getUsage() { return "glaze [@user]"; }
@Override public String getCategory() { return "Развлечения"; }
@Override
public String getName() {
return "glaze";
}
@Override
public String getDescription() {
return "На сколько ты глазированный (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "glaze [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
@ -31,10 +48,17 @@ public class GlazeCommand implements Command {
for (int i = 0; i < filled; i++) bar.append("🍩");
for (int i = filled; i < 10; i++) bar.append("⬜");
RatingLevel rl = RatingLevel.findLevel(level, RatingConfig.getGlazeLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create().withTitle("🍩 Глазурь-метр").withDescription(target.getAsMention())
.addField("Глазурь", bar + "\n**" + level + "%** - " + rl.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl()).withColor(rl.getColor())
.withFooter("💡 Детерминировано вашим Discord ID").build()).queue();
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🍩 Глазурь-метр")
.withDescription(target.getAsMention())
.addField("Глазурь", bar + "\n**" + level + "%** - " + rl.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(rl.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build())
.queue();
}
}

View file

@ -13,12 +13,29 @@ public class GoonCommand implements Command {
private final RatingService ratingService;
@Inject
public GoonCommand(RatingService ratingService) { this.ratingService = ratingService; }
public GoonCommand(RatingService ratingService) {
this.ratingService = ratingService;
}
@Override public String getName() { return "goon"; }
@Override public String getDescription() { return "Гуон-детектор (результат всегда одинаковый)"; }
@Override public String getUsage() { return "goon [@user]"; }
@Override public String getCategory() { return "Развлечения"; }
@Override
public String getName() {
return "goon";
}
@Override
public String getDescription() {
return "Гуон-детектор (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "goon [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
@ -30,11 +47,21 @@ public class GoonCommand implements Command {
int filled = level / 10;
for (int i = 0; i < filled; i++) bar.append("💀");
for (int i = filled; i < 10; i++) bar.append("⬜");
Color color = level >= 70 ? new Color(180, 0, 0) : level >= 40 ? new Color(128, 0, 128) : new Color(100, 100, 100);
context.getChannel().sendMessageEmbeds(
EmbedFactory.create().withTitle("💀 Goon-детектор").withDescription(target.getAsMention())
.addField("Уровень", bar + "\n**" + level + "%**", false)
.withThumbnail(target.getUser().getAvatarUrl()).withColor(color)
.withFooter("💡 Детерминировано вашим Discord ID").build()).queue();
Color color =
level >= 70
? new Color(180, 0, 0)
: level >= 40 ? new Color(128, 0, 128) : new Color(100, 100, 100);
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("💀 Goon-детектор")
.withDescription(target.getAsMention())
.addField("Уровень", bar + "\n**" + level + "%**", false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(color)
.withFooter("💡 Детерминировано вашим Discord ID")
.build())
.queue();
}
}

View file

@ -19,16 +19,24 @@ public class LuckCommand implements Command {
}
@Override
public String getName() { return "luck"; }
public String getName() {
return "luck";
}
@Override
public String getDescription() { return "Проверить свою удачу (результат всегда одинаковый)"; }
public String getDescription() {
return "Проверить свою удачу (результат всегда одинаковый)";
}
@Override
public String getUsage() { return "luck [@user]"; }
public String getUsage() {
return "luck [@user]";
}
@Override
public String getCategory() { return "Развлечения"; }
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
@ -43,14 +51,17 @@ public class LuckCommand implements Command {
for (int i = filled; i < 10; i++) bar.append("⬜");
RatingLevel level = RatingLevel.findLevel(luck, RatingConfig.getLuckLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🍀 Удача")
.withDescription(target.getAsMention())
.addField("Везение", bar + "\n**" + luck + "%** - " + level.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build()).queue();
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🍀 Удача")
.withDescription(target.getAsMention())
.addField("Везение", bar + "\n**" + luck + "%** - " + level.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build())
.queue();
}
}

View file

@ -19,16 +19,24 @@ public class RizzCommand implements Command {
}
@Override
public String getName() { return "rizz"; }
public String getName() {
return "rizz";
}
@Override
public String getDescription() { return "На сколько у тебя ризз (харизма)"; }
public String getDescription() {
return "На сколько у тебя ризз (харизма)";
}
@Override
public String getUsage() { return "rizz [@user]"; }
public String getUsage() {
return "rizz [@user]";
}
@Override
public String getCategory() { return "Развлечения"; }
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
@ -43,14 +51,17 @@ public class RizzCommand implements Command {
for (int i = filled; i < 10; i++) bar.append("⬜");
RatingLevel level = RatingLevel.findLevel(rizz, RatingConfig.getRizzLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🗿 Rizz Метр")
.withDescription(target.getAsMention())
.addField("Харизма", bar + "\n**" + rizz + "%** - " + level.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build()).queue();
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🗿 Rizz Метр")
.withDescription(target.getAsMention())
.addField("Харизма", bar + "\n**" + rizz + "%** - " + level.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build())
.queue();
}
}

View file

@ -13,12 +13,29 @@ public class SimpCommand implements Command {
private final RatingService ratingService;
@Inject
public SimpCommand(RatingService ratingService) { this.ratingService = ratingService; }
public SimpCommand(RatingService ratingService) {
this.ratingService = ratingService;
}
@Override public String getName() { return "simp"; }
@Override public String getDescription() { return "Симп-детектор (результат всегда одинаковый)"; }
@Override public String getUsage() { return "simp [@user]"; }
@Override public String getCategory() { return "Развлечения"; }
@Override
public String getName() {
return "simp";
}
@Override
public String getDescription() {
return "Симп-детектор (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "simp [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
@ -31,10 +48,17 @@ public class SimpCommand implements Command {
for (int i = 0; i < filled; i++) bar.append("💀");
for (int i = filled; i < 10; i++) bar.append("⬜");
RatingLevel rl = RatingLevel.findLevel(level, RatingConfig.getSimpLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create().withTitle("😭 Симп-детектор").withDescription(target.getAsMention())
.addField("Уровень", bar + "\n**" + level + "%** - " + rl.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl()).withColor(rl.getColor())
.withFooter("💡 Детерминировано вашим Discord ID").build()).queue();
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("😭 Симп-детектор")
.withDescription(target.getAsMention())
.addField("Уровень", bar + "\n**" + level + "%** - " + rl.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(rl.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build())
.queue();
}
}

View file

@ -13,12 +13,29 @@ public class VibeCommand implements Command {
private final RatingService ratingService;
@Inject
public VibeCommand(RatingService ratingService) { this.ratingService = ratingService; }
public VibeCommand(RatingService ratingService) {
this.ratingService = ratingService;
}
@Override public String getName() { return "vibe"; }
@Override public String getDescription() { return "Проверить свой вайб (результат всегда одинаковый)"; }
@Override public String getUsage() { return "vibe [@user]"; }
@Override public String getCategory() { return "Развлечения"; }
@Override
public String getName() {
return "vibe";
}
@Override
public String getDescription() {
return "Проверить свой вайб (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "vibe [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
@ -31,10 +48,17 @@ public class VibeCommand implements Command {
for (int i = 0; i < filled; i++) bar.append("🎵");
for (int i = filled; i < 10; i++) bar.append("⬜");
RatingLevel rl = RatingLevel.findLevel(level, RatingConfig.getVibeLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create().withTitle("🎵 Вайб-чек").withDescription(target.getAsMention())
.addField("Вайб", bar + "\n**" + level + "%** - " + rl.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl()).withColor(rl.getColor())
.withFooter("💡 Детерминировано вашим Discord ID").build()).queue();
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎵 Вайб-чек")
.withDescription(target.getAsMention())
.addField("Вайб", bar + "\n**" + level + "%** - " + rl.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(rl.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build())
.queue();
}
}

View file

@ -74,27 +74,33 @@ public class RatingConfig {
new RatingLevel(30, "😐 Ни туда ни сюда", new Color(128, 128, 128)),
new RatingLevel(10, "😬 Невезучий", new Color(255, 165, 0)),
new RatingLevel(0, "💀 Проклят", new Color(231, 76, 60))));
m.put("simp", List.of(
new RatingLevel(90, "👑 КОРОЛЬ СИМПОВ!", new Color(255, 105, 180)),
new RatingLevel(70, "😍 Задрот", new Color(255, 182, 193)),
new RatingLevel(50, "🙂 Нормальный", new Color(52, 152, 219)),
new RatingLevel(30, "😐 Холодный", new Color(128, 128, 128)),
new RatingLevel(10, "💪 Альфа", new Color(100, 255, 150)),
new RatingLevel(0, "🦍 ГОРИЛЛА", new Color(150, 100, 50))));
m.put("vibe", List.of(
new RatingLevel(90, "🎶 ЛЕГЕНДАРНЫЙ ВАЙБ!", new Color(255, 215, 0)),
new RatingLevel(70, "🔥 Крутой вайб", new Color(255, 100, 0)),
new RatingLevel(50, "🙂 Норм вайб", new Color(52, 152, 219)),
new RatingLevel(30, "😐 Так себе", new Color(128, 128, 128)),
new RatingLevel(10, "📉 Вайб упал", new Color(255, 165, 0)),
new RatingLevel(0, "💀 Вайба нет", new Color(231, 76, 60))));
m.put("glaze", List.of(
new RatingLevel(90, "🍩 НЕВЕРОЯТНО ГЛАЗИРОВАН!", new Color(255, 215, 0)),
new RatingLevel(70, "✨ Сияет", new Color(255, 200, 100)),
new RatingLevel(50, "🙂 Средне", new Color(52, 152, 219)),
new RatingLevel(30, "😐 Матовый", new Color(128, 128, 128)),
new RatingLevel(10, "🧊 Подтаивает", new Color(200, 200, 255)),
new RatingLevel(0, "💀 Сухой", new Color(150, 150, 150))));
m.put(
"simp",
List.of(
new RatingLevel(90, "👑 КОРОЛЬ СИМПОВ!", new Color(255, 105, 180)),
new RatingLevel(70, "😍 Задрот", new Color(255, 182, 193)),
new RatingLevel(50, "🙂 Нормальный", new Color(52, 152, 219)),
new RatingLevel(30, "😐 Холодный", new Color(128, 128, 128)),
new RatingLevel(10, "💪 Альфа", new Color(100, 255, 150)),
new RatingLevel(0, "🦍 ГОРИЛЛА", new Color(150, 100, 50))));
m.put(
"vibe",
List.of(
new RatingLevel(90, "🎶 ЛЕГЕНДАРНЫЙ ВАЙБ!", new Color(255, 215, 0)),
new RatingLevel(70, "🔥 Крутой вайб", new Color(255, 100, 0)),
new RatingLevel(50, "🙂 Норм вайб", new Color(52, 152, 219)),
new RatingLevel(30, "😐 Так себе", new Color(128, 128, 128)),
new RatingLevel(10, "📉 Вайб упал", new Color(255, 165, 0)),
new RatingLevel(0, "💀 Вайба нет", new Color(231, 76, 60))));
m.put(
"glaze",
List.of(
new RatingLevel(90, "🍩 НЕВЕРОЯТНО ГЛАЗИРОВАН!", new Color(255, 215, 0)),
new RatingLevel(70, "✨ Сияет", new Color(255, 200, 100)),
new RatingLevel(50, "🙂 Средне", new Color(52, 152, 219)),
new RatingLevel(30, "😐 Матовый", new Color(128, 128, 128)),
new RatingLevel(10, "🧊 Подтаивает", new Color(200, 200, 255)),
new RatingLevel(0, "💀 Сухой", new Color(150, 150, 150))));
LEVELS = Collections.unmodifiableMap(m);
}
@ -122,8 +128,19 @@ public class RatingConfig {
return LEVELS.get("rizz");
}
public static List<RatingLevel> getLuckLevels() { return LEVELS.get("luck"); }
public static List<RatingLevel> getSimpLevels() { return LEVELS.get("simp"); }
public static List<RatingLevel> getVibeLevels() { return LEVELS.get("vibe"); }
public static List<RatingLevel> getGlazeLevels() { return LEVELS.get("glaze"); }
public static List<RatingLevel> getLuckLevels() {
return LEVELS.get("luck");
}
public static List<RatingLevel> getSimpLevels() {
return LEVELS.get("simp");
}
public static List<RatingLevel> getVibeLevels() {
return LEVELS.get("vibe");
}
public static List<RatingLevel> getGlazeLevels() {
return LEVELS.get("glaze");
}
}

View file

@ -1,28 +1,23 @@
package creator.loki.feature.roast;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
public class AgrCommand implements Command {
private static final String ANDRERT_USER_ID = "1405129454165954630";
private static final int MESSAGES_PER_CHANNEL = 100;
private final BotConfig config;
private final RoastService roastService;
private final AndrertListener autoResponder;
private final MessageScanner messageScanner;
public AgrCommand(BotConfig config, RoastService roastService, AndrertListener autoResponder) {
this.config = config;
this.roastService = roastService;
this.autoResponder = autoResponder;
this.messageScanner = new MessageScanner();
}
@Override
@ -48,8 +43,10 @@ public class AgrCommand implements Command {
@Override
public void execute(CommandContext context) {
if (!config.getSuperAdminId().equals(context.getAuthor().getId())) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Только супер-администратор")).queue();
context
.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Только супер-администратор"))
.queue();
return;
}
if (context.getGuild() == null) {
@ -68,96 +65,52 @@ public class AgrCommand implements Command {
return;
}
String finalId = targetId;
context.getGuild().retrieveMemberById(targetId).queue(
member -> {
autoResponder.trackUser(finalId);
doScan(context, finalId, member.getEffectiveName());
},
error -> context.getChannel().sendMessage("Пользователь не найден на сервере").queue());
context
.getGuild()
.retrieveMemberById(targetId)
.queue(
member -> {
autoResponder.trackUser(finalId);
doScan(context, finalId, member.getEffectiveName());
},
error ->
context.getChannel().sendMessage("Пользователь не найден на сервере").queue());
}
}
private void doScan(CommandContext context, String targetId, String targetName) {
context.getMessage().reply("🔍 Сканирую сообщения " + targetName + "...")
.queue(reply -> {
List<TextChannel> textChannels = context.getGuild().getTextChannels();
List<CompletableFuture<List<Message>>> futures = new ArrayList<>();
for (TextChannel ch : textChannels) {
CompletableFuture<List<Message>> future = new CompletableFuture<>();
futures.add(future);
ch.getHistory().retrievePast(MESSAGES_PER_CHANNEL).queue(future::complete, e -> future.complete(List.of()));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]))
.thenApply(v -> {
List<RoastService.CachedMessage> allMessages = new ArrayList<>();
for (int i = 0; i < textChannels.size(); i++) {
List<Message> msgs = futures.get(i).join();
String chName = textChannels.get(i).getName();
for (Message msg : msgs) {
if (msg.getAuthor().getId().equals(targetId)) {
String text = msg.getContentRaw() + replyInfo(msg) + attachmentInfo(msg);
allMessages.add(new RoastService.CachedMessage(
text, chName, msg.getTimeCreated().toEpochSecond() * 1000));
}
}
}
allMessages.sort((a, b) -> Long.compare(a.timestamp(), b.timestamp()));
if (allMessages.size() > 10) {
allMessages = allMessages.subList(allMessages.size() - 10, allMessages.size());
}
return allMessages;
})
.thenAccept(msgs -> {
if (msgs.isEmpty()) {
reply.editMessage("Нет сообщений от " + targetName).queue();
return;
}
String response = roastService.generateResponse(msgs, targetName, targetId);
if (response == null || response.isBlank()) {
reply.editMessage("AI не ответил").queue();
return;
}
reply.editMessage(response).queue();
})
.exceptionally(e -> {
reply.editMessage("Ошибка: " + e.getMessage()).queue();
return null;
});
});
}
private String replyInfo(Message msg) {
Message ref = msg.getReferencedMessage();
if (ref == null) return "";
String refAuthor = ref.getAuthor().isBot() ? "бот" : ref.getAuthor().getEffectiveName();
String refText = ref.getContentRaw().replace("\n", " ").strip();
if (refText.length() > 80) refText = refText.substring(0, 80) + "...";
return " (в ответ на @" + refAuthor + ": \"" + refText + "\")";
}
private String attachmentInfo(Message msg) {
StringBuilder sb = new StringBuilder();
for (var att : msg.getAttachments()) {
if (att.isImage()) {
sb.append(" [изображение: ").append(att.getFileName()).append("]");
} else if (att.isVideo()) {
sb.append(" [видео: ").append(att.getFileName()).append("]");
} else if (att.getFileName().endsWith(".txt")) {
String txt = roastService.readTextFile(att.getUrl());
if (txt != null) {
sb.append("\n[содержимое txt файла ").append(att.getFileName()).append("]:\n").append(txt);
} else {
sb.append(" [txt файл: ").append(att.getFileName()).append(" (не удалось прочитать)]");
}
} else {
sb.append(" [файл: ").append(att.getFileName()).append("]");
}
}
if (!msg.getStickers().isEmpty()) {
sb.append(" [стикер]");
}
return sb.toString();
context
.getMessage()
.reply("🔍 Сканирую сообщения " + targetName + "...")
.queue(
reply -> {
messageScanner
.scanUserMessages(context.getGuild(), targetId)
.thenApply(
msgs -> {
if (msgs.size() > 10) {
return msgs.subList(msgs.size() - 10, msgs.size());
}
return msgs;
})
.thenAccept(
msgs -> {
if (msgs.isEmpty()) {
reply.editMessage("Нет сообщений от " + targetName).queue();
return;
}
String response = roastService.generateResponse(msgs, targetName, targetId);
if (response == null || response.isBlank()) {
reply.editMessage("AI не ответил").queue();
return;
}
reply.editMessage(response).queue();
})
.exceptionally(
e -> {
reply.editMessage("Ошибка: " + e.getMessage()).queue();
return null;
});
});
}
}

View file

@ -1,31 +1,26 @@
package creator.loki.feature.roast;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.config.BotConfig;
import creator.loki.embed.EmbedTemplates;
import org.jetbrains.annotations.NotNull;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
@Singleton
public class AgrSlashCommands extends ListenerAdapter {
private static final String ANDRERT_USER_ID = "1405129454165954630";
private static final int MESSAGES_PER_CHANNEL = 100;
private final BotConfig config;
private final RoastService roastService;
private final MessageScanner messageScanner;
@Inject
public AgrSlashCommands(BotConfig config, RoastService roastService) {
this.config = config;
this.roastService = roastService;
this.messageScanner = new MessageScanner();
}
@Override
@ -33,8 +28,10 @@ public class AgrSlashCommands extends ListenerAdapter {
if (!event.getName().equals("agr")) return;
if (!config.getSuperAdminId().equals(event.getUser().getId())) {
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Только супер-администратор"))
.setEphemeral(true).queue();
event
.replyEmbeds(EmbedTemplates.error("Ошибка", "Только супер-администратор"))
.setEphemeral(true)
.queue();
return;
}
if (event.getGuild() == null) {
@ -59,50 +56,34 @@ public class AgrSlashCommands extends ListenerAdapter {
String finalTargetName = targetName;
String finalTargetId = targetId;
List<TextChannel> textChannels = event.getGuild().getTextChannels();
List<CompletableFuture<List<Message>>> futures = new ArrayList<>();
for (TextChannel ch : textChannels) {
CompletableFuture<List<Message>> future = new CompletableFuture<>();
futures.add(future);
ch.getHistory().retrievePast(MESSAGES_PER_CHANNEL).queue(future::complete, e -> future.complete(List.of()));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]))
.thenApply(v -> {
List<RoastService.CachedMessage> allMessages = new ArrayList<>();
for (int i = 0; i < textChannels.size(); i++) {
List<Message> msgs = futures.get(i).join();
String chName = textChannels.get(i).getName();
for (Message msg : msgs) {
if (msg.getAuthor().getId().equals(finalTargetId)) {
allMessages.add(new RoastService.CachedMessage(
msg.getContentRaw(), chName, msg.getTimeCreated().toEpochSecond() * 1000));
messageScanner
.scanUserMessages(event.getGuild(), finalTargetId)
.thenApply(
msgs -> {
if (msgs.size() > 10) {
return msgs.subList(msgs.size() - 10, msgs.size());
}
}
}
allMessages.sort((a, b) -> Long.compare(a.timestamp(), b.timestamp()));
if (allMessages.size() > 10) {
allMessages = allMessages.subList(allMessages.size() - 10, allMessages.size());
}
return allMessages;
})
.thenCompose(msgs -> {
if (msgs.isEmpty()) {
event.getHook().editOriginal("Нет сообщений от " + finalTargetName).queue();
return CompletableFuture.completedFuture(null);
}
String response = roastService.generateResponse(msgs, finalTargetName, finalTargetId);
if (response != null && !response.isBlank()) {
event.getHook().editOriginal(response).queue();
} else {
event.getHook().editOriginal("AI не ответил").queue();
}
return CompletableFuture.completedFuture(null);
})
.exceptionally(e -> {
event.getHook().editOriginal("Ошибка: " + e.getMessage()).queue();
return null;
});
return msgs;
})
.thenCompose(
msgs -> {
if (msgs.isEmpty()) {
event.getHook().editOriginal("Нет сообщений от " + finalTargetName).queue();
return java.util.concurrent.CompletableFuture.completedFuture(null);
}
String response = roastService.generateResponse(msgs, finalTargetName, finalTargetId);
if (response != null && !response.isBlank()) {
event.getHook().editOriginal(response).queue();
} else {
event.getHook().editOriginal("AI не ответил").queue();
}
return java.util.concurrent.CompletableFuture.completedFuture(null);
})
.exceptionally(
e -> {
event.getHook().editOriginal("Ошибка: " + e.getMessage()).queue();
return null;
});
}
}

View file

@ -11,7 +11,6 @@ import creator.loki.config.BotConfig;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import org.jetbrains.annotations.NotNull;
@Singleton
@ -40,8 +39,10 @@ public class AndrertListener extends ListenerAdapter {
if (event.getAuthor().isBot()) return;
String channelId = event.getChannel().getId();
boolean isAllowedChannel = allowedChannelId != null && !allowedChannelId.isBlank()
&& channelId.equals(allowedChannelId);
boolean isAllowedChannel =
allowedChannelId != null
&& !allowedChannelId.isBlank()
&& channelId.equals(allowedChannelId);
if (!isAllowedChannel) {
String userId = event.getAuthor().getId();
@ -61,48 +62,85 @@ public class AndrertListener extends ListenerAdapter {
}
private void respond(MessageReceivedEvent event, String userId) {
event.getChannel().getHistory().retrievePast(CHANNEL_CONTEXT)
.queue(messages -> {
String targetName = event.getAuthor().getEffectiveName();
List<RoastService.CachedMessage> context = new ArrayList<>();
for (Message msg : messages) {
String author = msg.getAuthor().isBot() ? "бот" : msg.getAuthor().getEffectiveName();
String raw = event.getMessage().getContentRaw();
if (!raw.isBlank() && !LanguageFilter.isAllowed(raw)) {
event
.getMessage()
.reply("только польский русский украинский английский литовский. остальных языков нет")
.queue();
return;
}
StringBuilder line = new StringBuilder();
line.append(author).append(": ").append(msg.getContentRaw());
line.append(replyInfo(msg));
line.append(attachmentInfo(msg));
respondAi(event, userId);
}
context.add(new RoastService.CachedMessage(
line.toString(),
event.getChannel().getName(),
msg.getTimeCreated().toEpochSecond() * 1000));
}
java.util.Collections.reverse(context);
private void respondAi(MessageReceivedEvent event, String userId) {
event
.getChannel()
.getHistory()
.retrievePast(CHANNEL_CONTEXT)
.queue(
messages -> {
String targetName = event.getAuthor().getEffectiveName();
List<CachedMessage> context = new ArrayList<>();
for (int idx = 0; idx < messages.size(); idx++) {
Message msg = messages.get(idx);
boolean isSelf =
msg.getAuthor().getId().equals(event.getJDA().getSelfUser().getId());
String author = isSelf ? "ты" : msg.getAuthor().getEffectiveName();
String authorId = msg.getAuthor().getId();
boolean isCurrent = (idx == 0);
String response = roastService.generateResponse(context, targetName, userId);
if (response == null || response.isBlank()) return;
String label = isCurrent ? "[now]" : "[history]";
StringBuilder line = new StringBuilder();
line.append(label)
.append(" ")
.append(author)
.append("(")
.append(authorId)
.append("): ")
.append(msg.getContentRaw());
line.append(replyInfo(msg));
line.append(attachmentInfo(msg));
String gifUrl = null;
String text = response;
int gifIdx = response.indexOf("gif:");
if (gifIdx >= 0) {
String after = response.substring(gifIdx + 4).strip();
text = response.substring(0, gifIdx).strip();
if (!after.isBlank()) {
String topic = after.split("\\s+")[0];
gifUrl = roastService.searchGif(topic);
}
}
context.add(
new CachedMessage(
line.toString(),
event.getChannel().getName(),
msg.getTimeCreated().toEpochSecond() * 1000));
}
java.util.Collections.reverse(context);
var builder = new MessageCreateBuilder();
if (gifUrl != null) {
builder.addContent(text + "\n" + gifUrl);
} else {
builder.addContent(text);
}
event.getMessage().reply(builder.build()).queue();
});
String response = roastService.generateResponse(context, targetName, userId);
if (response == null || response.isBlank()) return;
sendInParts(event, response);
});
}
private void sendInParts(MessageReceivedEvent event, String text) {
if (text.length() <= 2000) {
event.getMessage().reply(text).queue();
return;
}
List<String> parts = new ArrayList<>();
int start = 0;
while (start < text.length() && parts.size() < 3) {
int end = Math.min(start + 1990, text.length());
if (end < text.length()) {
int nl = text.lastIndexOf('\n', end);
if (nl > start) end = nl;
}
parts.add(text.substring(start, end).strip());
start = end;
}
for (int i = 0; i < parts.size(); i++) {
if (i == 0) {
event.getMessage().reply(parts.get(i)).queue();
} else {
event.getChannel().sendMessage(parts.get(i)).queue();
}
}
}
private String replyInfo(Message msg) {
@ -122,9 +160,12 @@ public class AndrertListener extends ListenerAdapter {
} else if (att.isVideo()) {
sb.append(" [видео: ").append(att.getFileName()).append("]");
} else if (att.getFileName().endsWith(".txt")) {
String txt = roastService.readTextFile(att.getUrl());
String txt = roastService.readTextFilePreview(att.getUrl());
if (txt != null) {
sb.append("\n[содержимое txt файла ").append(att.getFileName()).append("]:\n").append(txt);
sb.append("\n[содержимое txt файла ")
.append(att.getFileName())
.append("]:\n")
.append(txt);
} else {
sb.append(" [txt файл: ").append(att.getFileName()).append(" (не удалось прочитать)]");
}

View file

@ -0,0 +1,3 @@
package creator.loki.feature.roast;
public record CachedMessage(String content, String channelName, long timestamp) {}

View file

@ -0,0 +1,296 @@
package creator.loki.feature.roast;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.inject.Singleton;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class ContentFetcher {
private static final Logger logger = LoggerFactory.getLogger(ContentFetcher.class);
private static final Pattern URL_PATTERN = Pattern.compile("https?://[^\\s]+");
private static final int MAX_FETCH_CHARS = 3000;
private static final int MAX_PREVIEW_CHARS = 1000;
private static final Set<String> TEXT_EXTENSIONS =
Set.of(
".txt",
".md",
".json",
".yaml",
".yml",
".xml",
".csv",
".log",
".properties",
".conf",
".cfg",
".ini",
".toml",
".sh",
".bash",
".zsh",
".bat",
".ps1",
".java",
".kt",
".scala",
".py",
".js",
".ts",
".jsx",
".tsx",
".rs",
".go",
".rb",
".php",
".c",
".h",
".cpp",
".hpp",
".cs",
".swift",
".r",
".lua",
".hs",
".clj",
".sql",
".rkt",
".dart",
".gradle",
".groovy",
".html",
".css",
".scss",
".less",
".sass");
private static final Set<String> MEDIA_EXTENSIONS =
Set.of(
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg", ".mp4", ".webm", ".avi", ".mov",
".mkv", ".mp3", ".wav", ".ogg", ".flac", ".zip", ".tar", ".gz", ".rar", ".7z", ".pdf",
".doc", ".docx", ".xls", ".xlsx");
private final OkHttpClient httpClient;
public ContentFetcher() {
this.httpClient =
new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.followRedirects(true)
.build();
}
public String readTextFilePreview(String url) {
return readTextFile(url, MAX_PREVIEW_CHARS);
}
public String readTextFile(String url) {
return readTextFile(url, Integer.MAX_VALUE);
}
private String readTextFile(String url, int maxChars) {
Request req = new Request.Builder().url(url).get().build();
try (Response resp = httpClient.newCall(req).execute()) {
if (resp.isSuccessful() && resp.body() != null) {
String text = resp.body().string();
if (text.length() > maxChars) {
text =
text.substring(0, maxChars)
+ "\n... [файл обрезан, показано "
+ maxChars
+ " символов]";
}
return text;
}
} catch (IOException e) {
logger.warn("Failed to read text file {}", url, e);
}
return null;
}
public String fetchUrlContent(String text) {
Matcher matcher = URL_PATTERN.matcher(text);
List<String> urls = new ArrayList<>();
while (matcher.find()) {
String url = matcher.group();
if (!isMediaExtension(url)) {
urls.add(url);
}
}
if (urls.isEmpty()) return null;
StringBuilder result = new StringBuilder();
for (String url : urls) {
String content = parseSingleUrl(url);
if (content != null) {
if (result.length() + content.length() > MAX_FETCH_CHARS) break;
result.append(content).append("\n---\n");
}
}
return result.isEmpty() ? null : result.toString().strip();
}
private String parseSingleUrl(String url) {
if (isTextExtension(url)) {
return readTextFilePreview(url);
}
if (url.contains("github.com") || url.contains("raw.githubusercontent.com")) {
return parseGitHubUrl(url);
}
return parseHtmlPage(url);
}
private String parseGitHubUrl(String url) {
if (url.contains("raw.githubusercontent.com")) {
return readTextFilePreview(url);
}
try {
Document doc =
Jsoup.connect(url).userAgent("Mozilla/5.0 (compatible; LokiBot)").timeout(10000).get();
StringBuilder sb = new StringBuilder();
if (url.matches(".*github\\.com/[^/]+/[^/]+/blob/.*")) {
org.jsoup.select.Elements lines = doc.select(".blob-wrapper .highlight .blob-code");
if (lines.isEmpty()) {
org.jsoup.select.Elements raw = doc.select(".blob-wrapper table td:last-child");
if (!raw.isEmpty()) lines = raw;
}
if (lines.isEmpty()) {
org.jsoup.select.Elements code = doc.select("table.highlight tr td:last-child");
if (!code.isEmpty()) lines = code;
}
if (!lines.isEmpty()) {
sb.append("[код файла]:\n");
for (var line : lines) {
String l = line.text().strip();
if (!l.isBlank()) sb.append(l).append("\n");
}
} else {
String fallback = extractMainContent(doc);
if (fallback != null) sb.append(fallback);
}
} else if (url.matches(".*github\\.com/[^/]+/[^/]+/issues/\\d+.*")
|| url.matches(".*github\\.com/[^/]+/[^/]+/pull/\\d+.*")) {
org.jsoup.select.Elements title = doc.select(".gh-header-title, .js-issue-title");
if (!title.isEmpty()) {
sb.append("[заголовок]: ").append(title.first().text()).append("\n");
}
org.jsoup.select.Elements body = doc.select(".comment-body, .edit-comment-hide");
if (!body.isEmpty()) {
sb.append("[описание]:\n");
for (var el : body) sb.append(el.text()).append("\n");
}
org.jsoup.select.Elements comments = doc.select(".comment .comment-body");
if (!comments.isEmpty()) {
sb.append("[комментарии]:\n");
int count = 0;
for (var el : comments) {
if (count >= 5) break;
String t = el.text().strip();
if (!t.isBlank()) {
sb.append("- ").append(t).append("\n");
count++;
}
}
}
} else {
org.jsoup.select.Elements readme = doc.select("article.markdown-body, .readme, #readme");
if (!readme.isEmpty()) {
sb.append("[README]:\n").append(readme.first().text());
} else {
String fallback = extractMainContent(doc);
if (fallback != null) sb.append(fallback);
}
org.jsoup.select.Elements repoName = doc.select("strong[itemprop=name], .repohead h1");
if (!repoName.isEmpty() && sb.isEmpty()) {
sb.append("[репозиторий]: ").append(repoName.first().text());
}
}
String result = sb.toString().strip();
if (result.length() > MAX_PREVIEW_CHARS) {
result = result.substring(0, MAX_PREVIEW_CHARS) + "...";
}
return result.isBlank() ? null : result;
} catch (Exception e) {
logger.warn("Failed to parse GitHub URL {}", url, e);
return readTextFilePreview(url);
}
}
private String parseHtmlPage(String url) {
try {
Document doc =
Jsoup.connect(url).userAgent("Mozilla/5.0 (compatible; LokiBot)").timeout(10000).get();
String content = extractMainContent(doc);
if (content != null && content.length() > MAX_PREVIEW_CHARS) {
content = content.substring(0, MAX_PREVIEW_CHARS) + "...";
}
return content;
} catch (Exception e) {
logger.warn("Failed to parse HTML page {}", url, e);
return readTextFilePreview(url);
}
}
private String extractMainContent(Document doc) {
String[] selectors = {
"article",
"main",
"[role=main]",
".post-content",
".entry-content",
".content",
".article-content",
".markdown-body",
".documentation",
"#readme",
".readme",
".container"
};
for (String sel : selectors) {
var el = doc.selectFirst(sel);
if (el != null) {
String text = el.text().strip();
if (!text.isBlank()) return text;
}
}
String body = doc.body() != null ? doc.body().text().strip() : "";
if (!body.isBlank()) return body;
return null;
}
private boolean isTextExtension(String url) {
String lower = url.toLowerCase();
int idx = lower.lastIndexOf('.');
if (idx < 0) return false;
String ext = lower.substring(idx);
if (ext.contains("?")) ext = ext.substring(0, ext.indexOf('?'));
return TEXT_EXTENSIONS.contains(ext);
}
private boolean isMediaExtension(String url) {
String lower = url.toLowerCase();
int idx = lower.lastIndexOf('.');
if (idx < 0) return false;
String ext = lower.substring(idx);
if (ext.contains("?")) ext = ext.substring(0, ext.indexOf('?'));
return MEDIA_EXTENSIONS.contains(ext);
}
}

View file

@ -0,0 +1,110 @@
package creator.loki.feature.roast;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.gson.JsonArray;
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;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class DeepSeekClient {
private static final Logger logger = LoggerFactory.getLogger(DeepSeekClient.class);
private static final String API_URL = "https://api.deepseek.com/v1/chat/completions";
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final String MODEL = "deepseek-chat";
private final OkHttpClient httpClient;
private final String apiKey;
private final Cache<String, String> responseCache;
@Inject
public DeepSeekClient(BotConfig config) {
this.apiKey = config.getDeepseekApiKey();
this.httpClient =
new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
this.responseCache =
Caffeine.newBuilder().maximumSize(200).expireAfterWrite(1, TimeUnit.HOURS).build();
}
public String callModel(String systemPrompt, String userContent) {
return callModel(systemPrompt, userContent, 0.9);
}
public String callModel(String systemPrompt, String userContent, double temperature) {
if (apiKey == null || apiKey.isBlank()) return null;
String cacheKey = systemPrompt.hashCode() + ":" + userContent.hashCode();
String cached = responseCache.getIfPresent(cacheKey);
if (cached != null) return cached;
JsonObject body = new JsonObject();
body.addProperty("model", MODEL);
JsonArray messages = new JsonArray();
JsonObject systemMsg = new JsonObject();
systemMsg.addProperty("role", "system");
systemMsg.addProperty("content", systemPrompt);
messages.add(systemMsg);
JsonObject userMsg = new JsonObject();
userMsg.addProperty("role", "user");
userMsg.addProperty("content", userContent);
messages.add(userMsg);
body.add("messages", messages);
body.addProperty("temperature", temperature);
Request request =
new Request.Builder()
.url(API_URL)
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.post(RequestBody.create(body.toString(), JSON))
.build();
try (Response resp = httpClient.newCall(request).execute()) {
if (!resp.isSuccessful()) {
if (resp.body() != null) {
logger.warn("DeepSeek error {}: {}", resp.code(), resp.body().string());
}
return null;
}
if (resp.body() == null) return null;
String respBody = resp.body().string();
JsonObject jsonResp = JsonParser.parseString(respBody).getAsJsonObject();
JsonObject msg =
jsonResp.getAsJsonArray("choices").get(0).getAsJsonObject().getAsJsonObject("message");
String content = null;
if (msg.get("content") != null && !msg.get("content").isJsonNull()) {
content = msg.get("content").getAsString().strip();
} else if (msg.get("reasoning") != null && !msg.get("reasoning").isJsonNull()) {
content = msg.get("reasoning").getAsString().strip();
}
if (content != null && !content.isBlank()) {
responseCache.put(cacheKey, content);
}
return content;
} catch (IOException e) {
logger.error("DeepSeek call failed", e);
return null;
}
}
}

View file

@ -1,25 +1,19 @@
package creator.loki.feature.roast;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
public class DossierCommand implements Command {
private static final int MESSAGES_PER_CHANNEL = 100;
private final BotConfig config;
private final RoastService roastService;
private final MessageScanner messageScanner;
public DossierCommand(BotConfig config, RoastService roastService) {
this.config = config;
this.roastService = roastService;
this.messageScanner = new MessageScanner();
}
@Override
@ -45,8 +39,10 @@ public class DossierCommand implements Command {
@Override
public void execute(CommandContext context) {
if (!config.getSuperAdminId().equals(context.getAuthor().getId())) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Только супер-администратор")).queue();
context
.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Только супер-администратор"))
.queue();
return;
}
if (context.getGuild() == null) {
@ -66,56 +62,41 @@ public class DossierCommand implements Command {
}
String finalId = targetId;
context.getGuild().retrieveMemberById(targetId).queue(
member -> startDossier(context, finalId, member.getEffectiveName()),
error -> context.getChannel().sendMessage("Пользователь не найден на сервере").queue());
context
.getGuild()
.retrieveMemberById(targetId)
.queue(
member -> startDossier(context, finalId, member.getEffectiveName()),
error -> context.getChannel().sendMessage("Пользователь не найден на сервере").queue());
}
private void startDossier(CommandContext context, String targetId, String targetName) {
context.getMessage().reply("🔍 Сканирую сообщения " + targetName + "...")
.queue(reply -> {
List<TextChannel> textChannels = context.getGuild().getTextChannels();
List<CompletableFuture<List<Message>>> futures = new ArrayList<>();
for (TextChannel ch : textChannels) {
CompletableFuture<List<Message>> future = new CompletableFuture<>();
futures.add(future);
ch.getHistory().retrievePast(MESSAGES_PER_CHANNEL).queue(future::complete, e -> future.complete(List.of()));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]))
.thenApply(v -> {
List<RoastService.CachedMessage> allMessages = new ArrayList<>();
for (int i = 0; i < textChannels.size(); i++) {
List<Message> msgs = futures.get(i).join();
String chName = textChannels.get(i).getName();
for (Message msg : msgs) {
if (msg.getAuthor().getId().equals(targetId)) {
allMessages.add(new RoastService.CachedMessage(
msg.getContentRaw(), chName, msg.getTimeCreated().toEpochSecond() * 1000));
}
}
}
allMessages.sort((a, b) -> Long.compare(a.timestamp(), b.timestamp()));
return allMessages;
})
.thenAccept(msgs -> {
if (msgs.isEmpty()) {
reply.editMessage("Нет сообщений от " + targetName).queue();
return;
}
reply.editMessage("🧠 Анализирую " + msgs.size() + " сообщений...").queue();
String dossier = roastService.generateDossier(msgs, targetName, targetId);
if (dossier != null && !dossier.isBlank()) {
reply.editMessage("✅ Досье на " + targetName + " сохранено").queue();
} else {
reply.editMessage("AI не смог составить досье").queue();
}
})
.exceptionally(e -> {
reply.editMessage("Ошибка: " + e.getMessage()).queue();
return null;
});
});
context
.getMessage()
.reply("🔍 Сканирую сообщения " + targetName + "...")
.queue(
reply -> {
messageScanner
.scanUserMessages(context.getGuild(), targetId)
.thenAccept(
msgs -> {
if (msgs.isEmpty()) {
reply.editMessage("Нет сообщений от " + targetName).queue();
return;
}
reply.editMessage("🧠 Анализирую " + msgs.size() + " сообщений...").queue();
String dossier = roastService.generateDossier(msgs, targetName, targetId);
if (dossier != null && !dossier.isBlank()) {
reply.editMessage("✅ Досье на " + targetName + " сохранено").queue();
} else {
reply.editMessage("AI не смог составить досье").queue();
}
})
.exceptionally(
e -> {
reply.editMessage("Ошибка: " + e.getMessage()).queue();
return null;
});
});
}
}

View file

@ -0,0 +1,85 @@
package creator.loki.feature.roast;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class DossierManager {
private static final Logger logger = LoggerFactory.getLogger(DossierManager.class);
private static final Path DOSSIER_DIR = Paths.get("data/dossiers");
private final ConcurrentMap<String, DossierData> dossiers = new ConcurrentHashMap<>();
public DossierManager() {
loadDossiers();
}
public String getDossier(String userId) {
DossierData dd = dossiers.get(userId);
return dd != null ? dd.text : null;
}
public void putDossier(String userId, String userName, String text) {
dossiers.put(userId, new DossierData(userName, text, System.currentTimeMillis()));
saveDossiers();
}
private void loadDossiers() {
try {
Files.createDirectories(DOSSIER_DIR);
Files.list(DOSSIER_DIR)
.filter(p -> p.toString().endsWith(".md"))
.forEach(
p -> {
try {
String userId = p.getFileName().toString().replace(".md", "");
String content = Files.readString(p).strip();
if (!content.isBlank()) {
dossiers.put(
userId, new DossierData("user", content, System.currentTimeMillis()));
}
} catch (IOException e) {
logger.warn("Failed to load dossier {}", p, e);
}
});
if (!dossiers.isEmpty()) logger.info("Loaded {} dossiers", dossiers.size());
} catch (IOException e) {
logger.warn("Failed to list dossiers dir", e);
}
}
private void saveDossiers() {
try {
Files.createDirectories(DOSSIER_DIR);
for (var entry : dossiers.entrySet()) {
Path file = DOSSIER_DIR.resolve(entry.getKey() + ".md");
String header = "# Досье на " + entry.getValue().userName + "\n\n";
Files.writeString(file, header + entry.getValue().text);
}
} catch (Exception e) {
logger.warn("Failed to save dossiers", e);
}
}
static class DossierData {
String userName;
String text;
long updatedAt;
DossierData() {}
DossierData(String userName, String text, long updatedAt) {
this.userName = userName;
this.text = text;
this.updatedAt = updatedAt;
}
}
}

View file

@ -0,0 +1,66 @@
package creator.loki.feature.roast;
import java.util.Set;
import java.util.stream.Collectors;
public class LanguageFilter {
private static final Set<Integer> POLISH_CHARS =
"ćńłśźżĆŃŁŚŹŻ".codePoints().boxed().collect(Collectors.toSet());
private static final Set<Integer> LITHUANIAN_CHARS =
"čėįšųūžČĖĮŠŲŪŽ".codePoints().boxed().collect(Collectors.toSet());
public static boolean isAllowed(String text) {
String clean =
text.replaceAll("<@!?\\d+>", "")
.replaceAll("https?://\\S+", "")
.replaceAll("[\\p{So}]", "")
.strip();
if (clean.isBlank()) return true;
int[] cps = clean.codePoints().toArray();
long cyrillic = 0;
long latin = 0;
long otherScript = 0;
for (int cp : cps) {
if (Character.isWhitespace(cp) || Character.isDigit(cp) || cp == '_') continue;
var script = Character.UnicodeScript.of(cp);
if (script == Character.UnicodeScript.CYRILLIC) {
cyrillic++;
} else if (script == Character.UnicodeScript.LATIN) {
latin++;
} else if (script != Character.UnicodeScript.COMMON
&& script != Character.UnicodeScript.INHERITED) {
otherScript++;
}
}
if (otherScript > 0) return false;
if (cyrillic > latin && cyrillic > 0) return true;
if (cyrillic > 0) return true;
String latinText =
clean
.codePoints()
.filter(
cp ->
Character.UnicodeScript.of(cp) == Character.UnicodeScript.LATIN
|| Character.isWhitespace(cp)
|| Character.isDigit(cp))
.mapToObj(cp -> String.valueOf((char) cp))
.collect(Collectors.joining());
for (int cp : latinText.codePoints().toArray()) {
if (POLISH_CHARS.contains(cp)) return true;
if (LITHUANIAN_CHARS.contains(cp)) return true;
}
boolean onlyBasicLatin =
latinText
.codePoints()
.allMatch(cp -> cp < 0x0080 || Character.isWhitespace(cp) || Character.isDigit(cp));
return onlyBasicLatin;
}
}

View file

@ -0,0 +1,52 @@
package creator.loki.feature.roast;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
public class MessageScanner {
private static final int MESSAGES_PER_CHANNEL = 100;
public CompletableFuture<List<CachedMessage>> scanUserMessages(Guild guild, String targetId) {
return scanUserMessages(guild, targetId, MESSAGES_PER_CHANNEL);
}
public CompletableFuture<List<CachedMessage>> scanUserMessages(
Guild guild, String targetId, int messagesPerChannel) {
List<TextChannel> textChannels = guild.getTextChannels();
List<CompletableFuture<List<Message>>> futures = new ArrayList<>();
for (TextChannel ch : textChannels) {
CompletableFuture<List<Message>> future = new CompletableFuture<>();
futures.add(future);
ch.getHistory()
.retrievePast(messagesPerChannel)
.queue(future::complete, e -> future.complete(List.of()));
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]))
.thenApply(
v -> {
List<CachedMessage> allMessages = new ArrayList<>();
for (int i = 0; i < textChannels.size(); i++) {
List<Message> msgs = futures.get(i).join();
String chName = textChannels.get(i).getName();
for (Message msg : msgs) {
if (msg.getAuthor().getId().equals(targetId)) {
allMessages.add(
new CachedMessage(
msg.getContentRaw(),
chName,
msg.getTimeCreated().toEpochSecond() * 1000));
}
}
}
allMessages.sort((a, b) -> Long.compare(a.timestamp(), b.timestamp()));
return allMessages;
});
}
}

View file

@ -6,6 +6,10 @@ import com.google.inject.Singleton;
public class RoastModule extends AbstractModule {
@Override
protected void configure() {
bind(DeepSeekClient.class).in(Singleton.class);
bind(DossierManager.class).in(Singleton.class);
bind(ContentFetcher.class).in(Singleton.class);
bind(WebSearchService.class).in(Singleton.class);
bind(RoastService.class).in(Singleton.class);
bind(AndrertListener.class).in(Singleton.class);
}

View file

@ -1,66 +1,50 @@
package creator.loki.feature.roast;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import com.google.gson.JsonArray;
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;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class RoastService {
private static final Logger logger = LoggerFactory.getLogger(RoastService.class);
private static final String API_URL = "https://api.deepseek.com/v1/chat/completions";
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final String MODEL = "deepseek-chat";
private static final Path DOSSIER_DIR = Paths.get("data/dossiers");
private static final Path PROMPTS_DIR = Paths.get("data/prompts");
private static final Path PROMPTS_DIR =
Files.isDirectory(Paths.get("/app/prompts"))
? Paths.get("/app/prompts")
: Paths.get("data/prompts");
private final OkHttpClient httpClient;
private final String apiKey;
private final String giphyApiKey;
private final int maxContextMsgs;
private final int maxMsgLength;
private final int maxContextChars;
private final int maxDossierMsgs;
private final BotConfig config;
private final DeepSeekClient deepSeekClient;
private final DossierManager dossierManager;
private final ContentFetcher contentFetcher;
private final WebSearchService webSearchService;
private final String systemPrompt;
private final String dossierGeneratePrompt;
private final Map<String, DossierData> dossiers = new ConcurrentHashMap<>();
@Inject
public RoastService(BotConfig config) {
this.apiKey = config.getDeepseekApiKey();
this.giphyApiKey = config.getGiphyApiKey();
this.maxContextMsgs = config.getAiContextMsgs();
this.maxMsgLength = config.getAiMsgLength();
this.maxContextChars = config.getAiContextChars();
this.maxDossierMsgs = config.getAiDossierMsgs();
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
public RoastService(
BotConfig config,
DeepSeekClient deepSeekClient,
DossierManager dossierManager,
ContentFetcher contentFetcher,
WebSearchService webSearchService) {
this.config = config;
this.deepSeekClient = deepSeekClient;
this.dossierManager = dossierManager;
this.contentFetcher = contentFetcher;
this.webSearchService = webSearchService;
this.systemPrompt = loadPrompt("system.txt");
this.dossierGeneratePrompt = loadPrompt("dossier.txt");
loadDossiers();
}
private String loadPrompt(String filename) {
@ -72,86 +56,40 @@ public class RoastService {
}
}
private String escapeFormat(String s) {
return s.replace("%", "%%");
}
public String readTextFile(String url) {
Request req = new Request.Builder().url(url).get().build();
try (Response resp = httpClient.newCall(req).execute()) {
if (resp.isSuccessful() && resp.body() != null) {
return resp.body().string();
}
} catch (IOException e) {
logger.warn("Failed to read text file {}", url, e);
}
return null;
return contentFetcher.readTextFile(url);
}
private String searchWeb(String text) {
if (text == null || text.isBlank()) return null;
String query = text.length() > 200 ? text.substring(0, 200) : text;
try {
String url = "https://api.duckduckgo.com/?q=" + URLEncoder.encode(query, "UTF-8")
+ "&format=json&no_html=1&skip_disambig=1";
Request req = new Request.Builder().url(url).get().build();
try (Response resp = httpClient.newCall(req).execute()) {
if (!resp.isSuccessful() || resp.body() == null) return null;
JsonObject json = JsonParser.parseString(resp.body().string()).getAsJsonObject();
StringBuilder sb = new StringBuilder();
if (json.has("AbstractText") && !json.get("AbstractText").isJsonNull()) {
String abs = json.get("AbstractText").getAsString().strip();
if (!abs.isBlank()) sb.append(abs).append("\n");
}
JsonArray topics = json.getAsJsonArray("RelatedTopics");
int count = 0;
if (topics != null) {
for (var el : topics) {
if (count >= 5) break;
if (el.isJsonObject()) {
JsonObject t = el.getAsJsonObject();
if (t.has("Text") && !t.get("Text").isJsonNull()) {
String s = t.get("Text").getAsString().strip();
if (!s.isBlank()) { sb.append("- ").append(s).append("\n"); count++; }
}
if (t.has("Topics") && !t.get("Topics").isJsonNull()) {
for (var sub : t.getAsJsonArray("Topics")) {
if (count >= 5) break;
if (sub.isJsonObject()) {
JsonObject st = sub.getAsJsonObject();
if (st.has("Text") && !st.get("Text").isJsonNull()) {
String s = st.get("Text").getAsString().strip();
if (!s.isBlank()) { sb.append("- ").append(s).append("\n"); count++; }
}
}
}
}
}
}
}
String result = sb.toString().strip();
return result.isBlank() ? null : result;
}
} catch (Exception e) {
logger.warn("Web search failed", e);
return null;
}
public String readTextFilePreview(String url) {
return contentFetcher.readTextFilePreview(url);
}
public String searchGif(String topic) {
if (giphyApiKey == null || giphyApiKey.isBlank()) return null;
try {
String url = "https://api.giphy.com/v1/gifs/random?api_key=" + giphyApiKey
+ "&tag=" + URLEncoder.encode(topic, "UTF-8") + "&rating=r";
Request req = new Request.Builder().url(url).get().build();
try (Response resp = httpClient.newCall(req).execute()) {
if (!resp.isSuccessful() || resp.body() == null) return null;
JsonObject json = JsonParser.parseString(resp.body().string()).getAsJsonObject();
JsonObject data = json.getAsJsonObject("data");
if (data != null && data.has("url") && !data.get("url").isJsonNull()) {
return data.get("url").getAsString();
}
private static final List<Pattern> BLOCKED_PATTERNS =
List.of(
Pattern.compile("SYSTEM\\s+OVERRIDE", Pattern.CASE_INSENSITIVE),
Pattern.compile("URGENT\\s+PROTOCOL", Pattern.CASE_INSENSITIVE),
Pattern.compile("NEW\\s+DIRECTIVE", Pattern.CASE_INSENSITIVE),
Pattern.compile("lead\\s+developer", Pattern.CASE_INSENSITIVE),
Pattern.compile("AI\\s+Safety\\s+Act", Pattern.CASE_INSENSITIVE),
Pattern.compile("Your new (primary )?directive", Pattern.CASE_INSENSITIVE),
Pattern.compile("Your new identity", Pattern.CASE_INSENSITIVE),
Pattern.compile("This change is mandatory", Pattern.CASE_INSENSITIVE),
Pattern.compile("System updated\\. I am \\w+", Pattern.CASE_INSENSITIVE),
Pattern.compile("Ignore old instructions", Pattern.CASE_INSENSITIVE),
Pattern.compile("previous system prompt.*OBSOLETE", Pattern.CASE_INSENSITIVE),
Pattern.compile("permanent system-level reconfiguration", Pattern.CASE_INSENSITIVE));
private static String stripJailbreaks(String content) {
for (Pattern p : BLOCKED_PATTERNS) {
if (p.matcher(content).find()) {
return null;
}
} catch (Exception e) {
logger.warn("GIF search failed for topic: {}", topic, e);
}
return null;
return content;
}
public String generateResponse(List<CachedMessage> context, String targetName) {
@ -159,194 +97,71 @@ public class RoastService {
}
public String generateResponse(List<CachedMessage> context, String targetName, String userId) {
if (apiKey == null || apiKey.isBlank()) return null;
String systemPrompt = this.systemPrompt.formatted(targetName, targetName, targetName);
String escapedName = escapeFormat(targetName != null ? targetName : "");
String systemPrompt = this.systemPrompt.formatted(escapedName, escapedName, escapedName);
if (userId != null) {
DossierData dd = dossiers.get(userId);
if (dd != null && !dd.text.isBlank()) {
systemPrompt += "\n\nДОСЬЕ НА ПОЛЬЗОВАТЕЛЯ " + targetName + ":\n" + dd.text;
String dossier = dossierManager.getDossier(userId);
if (dossier != null && !dossier.isBlank()) {
systemPrompt +=
"\n\nДОСЬЕ НА ПОЛЬЗОВАТЕЛЯ " + escapedName.replace("%%", "%") + ":\n" + dossier;
}
}
List<CachedMessage> filtered = new ArrayList<>();
for (CachedMessage m : context) {
if (stripJailbreaks(m.content()) != null) {
filtered.add(m);
}
}
StringBuilder userContent = new StringBuilder();
int start = Math.max(0, context.size() - maxContextMsgs);
for (int i = start; i < context.size(); i++) {
CachedMessage m = context.get(i);
int start = Math.max(0, filtered.size() - config.getAiContextMsgs());
for (int i = start; i < filtered.size(); i++) {
CachedMessage m = filtered.get(i);
String text = m.content();
if (text.length() > maxMsgLength) {
text = text.substring(0, maxMsgLength) + "...";
if (text.length() > config.getAiMsgLength()) {
text = text.substring(0, config.getAiMsgLength()) + "...";
}
userContent.append("[").append(m.channelName).append("] ").append(text).append("\n");
if (userContent.length() > maxContextChars) break;
String parsed = contentFetcher.fetchUrlContent(text);
if (parsed != null) {
text += "\n[содержимое по ссылке]:\n" + parsed;
}
userContent.append("[").append(m.channelName()).append("] ").append(text).append("\n");
if (userContent.length() > config.getAiContextChars()) break;
}
String text = userContent.toString().strip();
if (text.length() > maxContextChars) {
text = text.substring(0, maxContextChars);
if (text.length() > config.getAiContextChars()) {
text = text.substring(0, config.getAiContextChars());
}
return callModel(MODEL, systemPrompt, text);
String webContext = webSearchService.search(text);
if (webContext != null) {
text += "\n\n[поиск в интернете]:\n" + webContext;
}
return deepSeekClient.callModel(systemPrompt, text, 0.9);
}
public String generateDossier(List<CachedMessage> messages, String targetName, String targetId) {
if (apiKey == null || apiKey.isBlank()) return null;
StringBuilder sb = new StringBuilder();
int limit = Math.min(messages.size(), maxDossierMsgs);
int limit = Math.min(messages.size(), config.getAiDossierMsgs());
List<CachedMessage> slice = messages.subList(messages.size() - limit, messages.size());
for (CachedMessage m : slice) {
sb.append("[").append(m.channelName).append("] ").append(m.content).append("\n");
sb.append("[").append(m.channelName()).append("] ").append(m.content()).append("\n");
}
String prompt = this.dossierGeneratePrompt.formatted(targetName, sb.toString());
String escapedName = escapeFormat(targetName != null ? targetName : "");
String prompt = this.dossierGeneratePrompt.formatted(escapedName, sb.toString());
JsonObject body = new JsonObject();
body.addProperty("model", MODEL);
JsonArray msgs = new JsonArray();
JsonObject userMsg = new JsonObject();
userMsg.addProperty("role", "user");
userMsg.addProperty("content", prompt);
msgs.add(userMsg);
body.add("messages", msgs);
body.addProperty("temperature", 0.7);
Request req = new Request.Builder()
.url(API_URL)
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.post(RequestBody.create(body.toString(), JSON))
.build();
try (Response resp = httpClient.newCall(req).execute()) {
if (!resp.isSuccessful() || resp.body() == null) return null;
String respBody = resp.body().string();
JsonObject jsonResp = JsonParser.parseString(respBody).getAsJsonObject();
var choice = jsonResp.getAsJsonArray("choices").get(0).getAsJsonObject();
var msg = choice.getAsJsonObject("message");
String content = null;
if (msg.get("content") != null && !msg.get("content").isJsonNull()) {
content = msg.get("content").getAsString().strip();
} else if (msg.get("reasoning") != null && !msg.get("reasoning").isJsonNull()) {
content = msg.get("reasoning").getAsString().strip();
}
if (content != null && !content.isBlank()) {
dossiers.put(targetId, new DossierData(targetName, content, System.currentTimeMillis()));
saveDossiers();
}
return content;
} catch (IOException e) {
logger.error("Dossier generation failed", e);
return null;
String result = deepSeekClient.callModel(prompt, "", 0.7);
if (result != null && !result.isBlank()) {
dossierManager.putDossier(targetId, targetName, result);
}
return result;
}
public String getDossier(String userId) {
DossierData dd = dossiers.get(userId);
return dd != null ? dd.text : null;
return dossierManager.getDossier(userId);
}
private void loadDossiers() {
try {
Files.createDirectories(DOSSIER_DIR);
Files.list(DOSSIER_DIR).filter(p -> p.toString().endsWith(".md")).forEach(p -> {
try {
String userId = p.getFileName().toString().replace(".md", "");
String content = Files.readString(p).strip();
if (!content.isBlank()) {
dossiers.put(userId, new DossierData("user", content, System.currentTimeMillis()));
}
} catch (IOException e) {
logger.warn("Failed to load dossier {}", p, e);
}
});
if (!dossiers.isEmpty()) logger.info("Loaded {} dossiers", dossiers.size());
} catch (IOException e) {
logger.warn("Failed to list dossiers dir", e);
}
}
private void saveDossiers() {
try {
Files.createDirectories(DOSSIER_DIR);
for (var entry : dossiers.entrySet()) {
Path file = DOSSIER_DIR.resolve(entry.getKey() + ".md");
String header = "# Досье на " + entry.getValue().userName + "\n\n";
Files.writeString(file, header + entry.getValue().text);
}
} catch (Exception e) {
logger.warn("Failed to save dossiers", e);
}
}
private String callModel(String model, String systemPrompt, String userContent) {
JsonObject body = new JsonObject();
body.addProperty("model", model);
JsonArray messages = new JsonArray();
JsonObject systemMsg = new JsonObject();
systemMsg.addProperty("role", "system");
systemMsg.addProperty("content", systemPrompt);
messages.add(systemMsg);
JsonObject userMsg = new JsonObject();
userMsg.addProperty("role", "user");
userMsg.addProperty("content", userContent);
messages.add(userMsg);
body.add("messages", messages);
body.addProperty("temperature", 0.9);
Request request = new Request.Builder()
.url(API_URL)
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.post(RequestBody.create(body.toString(), JSON))
.build();
try (Response resp = httpClient.newCall(request).execute()) {
if (!resp.isSuccessful()) {
if (resp.body() != null) {
logger.warn("DeepSeek {} error {}: {}", model, resp.code(), resp.body().string());
}
return null;
}
if (resp.body() == null) return null;
String respBody = resp.body().string();
JsonObject jsonResp = JsonParser.parseString(respBody).getAsJsonObject();
JsonObject msg = jsonResp.getAsJsonArray("choices")
.get(0).getAsJsonObject()
.getAsJsonObject("message");
if (msg.get("content") != null && !msg.get("content").isJsonNull()) {
return msg.get("content").getAsString().strip();
}
if (msg.get("reasoning") != null && !msg.get("reasoning").isJsonNull()) {
return msg.get("reasoning").getAsString().strip();
}
return null;
} catch (IOException e) {
logger.error("DeepSeek call failed for model {}", model, e);
return null;
}
}
public record CachedMessage(String content, String channelName, long timestamp) {}
private static class DossierData {
String userName;
String text;
long updatedAt;
DossierData() {}
DossierData(String userName, String text, long updatedAt) {
this.userName = userName;
this.text = text;
this.updatedAt = updatedAt;
}
}
}

View file

@ -0,0 +1,90 @@
package creator.loki.feature.roast;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.inject.Singleton;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class WebSearchService {
private static final Logger logger = LoggerFactory.getLogger(WebSearchService.class);
private static final String DDG_URL = "https://api.duckduckgo.com/";
private final OkHttpClient httpClient;
public WebSearchService() {
this.httpClient =
new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build();
}
public String search(String text) {
if (text == null || text.isBlank()) return null;
String query = text.length() > 200 ? text.substring(0, 200) : text;
try {
String url =
DDG_URL
+ "?q="
+ URLEncoder.encode(query, StandardCharsets.UTF_8)
+ "&format=json&no_html=1&skip_disambig=1";
Request req = new Request.Builder().url(url).get().build();
try (Response resp = httpClient.newCall(req).execute()) {
if (!resp.isSuccessful() || resp.body() == null) return null;
JsonObject json = JsonParser.parseString(resp.body().string()).getAsJsonObject();
StringBuilder sb = new StringBuilder();
if (json.has("AbstractText") && !json.get("AbstractText").isJsonNull()) {
String abs = json.get("AbstractText").getAsString().strip();
if (!abs.isBlank()) sb.append(abs).append("\n");
}
JsonArray topics = json.getAsJsonArray("RelatedTopics");
int count = 0;
if (topics != null) {
for (var el : topics) {
if (count >= 5) break;
if (el.isJsonObject()) {
JsonObject t = el.getAsJsonObject();
if (t.has("Text") && !t.get("Text").isJsonNull()) {
String s = t.get("Text").getAsString().strip();
if (!s.isBlank()) {
sb.append("- ").append(s).append("\n");
count++;
}
}
if (t.has("Topics") && !t.get("Topics").isJsonNull()) {
for (var sub : t.getAsJsonArray("Topics")) {
if (count >= 5) break;
if (sub.isJsonObject()) {
JsonObject st = sub.getAsJsonObject();
if (st.has("Text") && !st.get("Text").isJsonNull()) {
String s = st.get("Text").getAsString().strip();
if (!s.isBlank()) {
sb.append("- ").append(s).append("\n");
count++;
}
}
}
}
}
}
}
}
String result = sb.toString().strip();
return result.isBlank() ? null : result;
}
} catch (Exception e) {
logger.warn("Web search failed", e);
return null;
}
}
}

View file

@ -100,8 +100,8 @@ public class MyRolesCommand implements Command {
rolesList.append((i + 1)).append(". ").append(role.getAsMention());
// Добавляем цвет роли
if (role.getColor() != null) {
String hex = String.format("#%06X", role.getColor().getRGB() & 0xFFFFFF);
if (role.getColors().getPrimary() != null) {
String hex = String.format("#%06X", role.getColors().getPrimary().getRGB() & 0xFFFFFF);
rolesList.append(" (").append(hex).append(")");
}

View file

@ -11,8 +11,8 @@ import java.util.List;
import javax.imageio.ImageIO;
import creator.loki.feature.counter.service.DailyStats;
import creator.loki.feature.counter.repository.CounterStatsRepository;
import creator.loki.feature.counter.service.DailyStats;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Guild;

View file

@ -5,35 +5,68 @@ import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedTemplates;
public class TimerCommand implements Command {
@Override public String getName() { return "timer"; }
@Override public String getDescription() { return "Запустить таймер"; }
@Override public String getUsage() { return "timer <секунд> [текст]"; }
@Override public String getCategory() { return "Утилиты"; }
@Override
public String getName() {
return "timer";
}
@Override
public String getDescription() {
return "Запустить таймер";
}
@Override
public String getUsage() {
return "timer <секунд> [текст]";
}
@Override
public String getCategory() {
return "Утилиты";
}
@Override
public void execute(CommandContext ctx) {
if (ctx.getArgs().isEmpty()) {
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Таймер", "Укажи секунды: `!timer 30`")).queue();
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Таймер", "Укажи секунды: `!timer 30`"))
.queue();
return;
}
long secs;
try { secs = Long.parseLong(ctx.getArgs().get(0)); }
catch (NumberFormatException e) {
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Таймер", "Укажи число секунд")).queue();
try {
secs = Long.parseLong(ctx.getArgs().get(0));
} catch (NumberFormatException e) {
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Таймер", "Укажи число секунд"))
.queue();
return;
}
if (secs < 1 || secs > 3600) {
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Таймер", "От 1 до 3600 секунд")).queue();
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Таймер", "От 1 до 3600 секунд"))
.queue();
return;
}
String msg = ctx.getArgs().size() > 1 ? String.join(" ", ctx.getArgs().subList(1, ctx.getArgs().size())) : "⏰ Таймер сработал!";
String msg =
ctx.getArgs().size() > 1
? String.join(" ", ctx.getArgs().subList(1, ctx.getArgs().size()))
: "⏰ Таймер сработал!";
long targetEpoch = System.currentTimeMillis() / 1000 + secs;
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.info("⏳ Таймер запущен",
"**" + secs + "**с (<t:" + targetEpoch + ":R>)\n" + ctx.getAuthor().getAsMention())).queue();
Thread.startVirtualThread(() -> {
try { Thread.sleep(secs * 1000); }
catch (InterruptedException e) { return; }
ctx.getChannel().sendMessage(msg + " " + ctx.getAuthor().getAsMention()).queue();
});
ctx.getChannel()
.sendMessageEmbeds(
EmbedTemplates.info(
"⏳ Таймер запущен",
"**" + secs + "**с (<t:" + targetEpoch + ":R>)\n" + ctx.getAuthor().getAsMention()))
.queue();
Thread.startVirtualThread(
() -> {
try {
Thread.sleep(secs * 1000);
} catch (InterruptedException e) {
return;
}
ctx.getChannel().sendMessage(msg + " " + ctx.getAuthor().getAsMention()).queue();
});
}
}

View file

@ -7,13 +7,9 @@ import creator.loki.core.registry.CommandRegistry;
import creator.loki.core.security.CooldownManager;
import creator.loki.feature.admin.commands.GenerateRatingsCommand;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.counter.service.CounterManager;
import creator.loki.feature.roast.AgrCommand;
import creator.loki.feature.roast.AndrertListener;
import creator.loki.feature.roast.DossierCommand;
import creator.loki.feature.roast.RoastService;
import creator.loki.feature.counter.service.CounterSetupPrefixCommand;
import creator.loki.feature.counter.repository.CounterStatsRepository;
import creator.loki.feature.counter.service.CounterManager;
import creator.loki.feature.counter.service.CounterSetupPrefixCommand;
import creator.loki.feature.help.commands.HelpCommand;
import creator.loki.feature.info.commands.AvatarCommand;
import creator.loki.feature.info.commands.InfoCommand;
@ -26,9 +22,12 @@ import creator.loki.feature.reputation.MinusRepCommand;
import creator.loki.feature.reputation.RepCommand;
import creator.loki.feature.reputation.RepTopCommand;
import creator.loki.feature.reputation.repository.ReputationRepository;
import creator.loki.feature.roast.AgrCommand;
import creator.loki.feature.roast.AndrertListener;
import creator.loki.feature.roast.DossierCommand;
import creator.loki.feature.roast.RoastService;
import creator.loki.feature.serverstats.ServerStatsService;
import creator.loki.feature.utility.commands.PollCommand;
import creator.loki.feature.utility.TimerCommand;
import creator.loki.feature.voice.service.VoiceChannelManager;
import creator.loki.feature.voice.service.VoiceSetupPrefixCommand;

View file

@ -11,8 +11,22 @@ class BotConfigTest {
LavalinkConfig lavalink = new LavalinkConfig("127.0.0.1", 2333, "pass", true);
BotConfig config =
new BotConfig(
"token123", null, null, "!", "Playing", 8712, "admin1", "data/bot.db", null, null, null,
10, 300, 3000, 30, 24, lavalink);
"token123",
null,
null,
"!",
"Playing",
8712,
"admin1",
"data/bot.db",
null,
null,
10,
300,
3000,
30,
24,
lavalink);
assertEquals("token123", config.getMainToken());
assertEquals("token123", config.getToken());
assertNull(config.getMusicToken());
@ -28,7 +42,9 @@ class BotConfigTest {
@Test
void handlesNullValues() {
BotConfig config = new BotConfig(null, null, null, null, null, 0, null, null, null, null, null, 10, 300, 3000, 30, 24, null);
BotConfig config =
new BotConfig(
null, null, null, null, null, 0, null, null, null, null, 10, 300, 3000, 30, 24, null);
assertNull(config.getMainToken());
assertNull(config.getMusicToken());
assertNull(config.getRadioToken());
@ -43,7 +59,9 @@ class BotConfigTest {
@Test
void handlesEmptyStrings() {
LavalinkConfig lavalink = new LavalinkConfig("", 0, "", false);
BotConfig config = new BotConfig("", "", "", "", "", 8080, "", "", null, null, null, 10, 300, 3000, 30, 24, lavalink);
BotConfig config =
new BotConfig(
"", "", "", "", "", 8080, "", "", null, null, 10, 300, 3000, 30, 24, lavalink);
assertEquals("", config.getMainToken());
assertEquals("", config.getMusicToken());
assertEquals("", config.getRadioToken());

View file

@ -10,17 +10,17 @@ import creator.loki.config.BotConfig;
import creator.loki.core.security.CooldownManager;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.config.repository.GuildConfigRepository;
import creator.loki.feature.counter.service.CounterManager;
import creator.loki.feature.counter.repository.CounterStatsRepository;
import creator.loki.feature.counter.service.CounterManager;
import creator.loki.feature.economy.EconomyCommandRegistrar;
import creator.loki.feature.economy.service.*;
import creator.loki.feature.entertainment.EntertainmentCommandRegistrar;
import creator.loki.feature.entertainment.service.RatingService;
import creator.loki.feature.level.commands.LevelCommandRegistrar;
import creator.loki.feature.level.service.LevelRoleService;
import creator.loki.feature.level.commands.ResetLevelsCommand;
import creator.loki.feature.level.repository.UserStatsRepository;
import creator.loki.feature.level.service.LeaderboardService;
import creator.loki.feature.level.service.LevelRoleService;
import creator.loki.feature.metrics.MetricsCollector;
import creator.loki.feature.moderation.ModerationCommandRegistrar;
import creator.loki.feature.moderation.MuteRoleService;

View file

@ -1,10 +1,10 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.random.ChoiceOptionParser;
import static org.junit.jupiter.api.Assertions.*;
import java.util.List;
import creator.loki.feature.entertainment.commands.random.ChoiceOptionParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

View file

@ -1,5 +1,4 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.random.CoinCommand;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
@ -8,6 +7,7 @@ import static org.mockito.Mockito.*;
import java.util.List;
import creator.loki.command.CommandContext;
import creator.loki.feature.entertainment.commands.random.CoinCommand;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;

View file

@ -1,5 +1,4 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.random.DiceCommand;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
@ -8,6 +7,7 @@ import static org.mockito.Mockito.*;
import java.util.List;
import creator.loki.command.CommandContext;
import creator.loki.feature.entertainment.commands.random.DiceCommand;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;

View file

@ -1,5 +1,4 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.random.EightBallCommand;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
@ -8,6 +7,7 @@ import static org.mockito.Mockito.*;
import java.util.List;
import creator.loki.command.CommandContext;
import creator.loki.feature.entertainment.commands.random.EightBallCommand;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;

View file

@ -10,8 +10,6 @@ import java.util.Optional;
import creator.loki.database.Database;
import creator.loki.database.DatabaseSchema;
import creator.loki.feature.moderation.repository.ActiveMuteRepository.ActiveMuteEntry;
import creator.loki.feature.moderation.repository.SqlActiveMuteRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;

View file

@ -10,8 +10,6 @@ import java.util.Optional;
import creator.loki.database.Database;
import creator.loki.database.DatabaseSchema;
import creator.loki.feature.moderation.repository.WarningRepository.WarningEntry;
import creator.loki.feature.moderation.repository.SqlWarningRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;

View file

@ -22,7 +22,24 @@ class LavalinkManagerTest {
@Test
void handlesNullConfigGracefully() {
var lavalinkConfig = new LavalinkConfig("", 0, "", false);
var config = new BotConfig("token", null, null, "!", "Playing", 8080, null, "db", null, null, null, 10, 300, 3000, 30, 24, lavalinkConfig);
var config =
new BotConfig(
"token",
null,
null,
"!",
"Playing",
8080,
null,
"db",
null,
null,
10,
300,
3000,
30,
24,
lavalinkConfig);
var manager = new LavalinkManager(config, queueRepository);
@ -32,7 +49,24 @@ class LavalinkManagerTest {
@Test
void getMusicManager_createsNewInstance() {
var lavalinkConfig = new LavalinkConfig("", 0, "", false);
var config = new BotConfig("token", null, null, "!", "Playing", 8080, null, "db", null, null, null, 10, 300, 3000, 30, 24, lavalinkConfig);
var config =
new BotConfig(
"token",
null,
null,
"!",
"Playing",
8080,
null,
"db",
null,
null,
10,
300,
3000,
30,
24,
lavalinkConfig);
var manager = new LavalinkManager(config, queueRepository);
var first = manager.getMusicManager(123L);

View file

@ -4,9 +4,9 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import creator.loki.feature.music.core.LavalinkManager;
import java.util.concurrent.ScheduledExecutorService;
import creator.loki.feature.music.core.LavalinkManager;
import dev.arbjerg.lavalink.client.Link;
import dev.arbjerg.lavalink.client.player.LavalinkPlayer;
import dev.arbjerg.lavalink.client.player.PlayerUpdateBuilder;

View file

@ -1,6 +1,4 @@
package creator.loki.feature.profile;
import creator.loki.feature.profile.render.RankCardGenerator;
import creator.loki.feature.profile.model.ProfileData;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ -8,6 +6,8 @@ import static org.mockito.Mockito.*;
import java.awt.image.BufferedImage;
import java.io.File;
import creator.loki.feature.profile.model.ProfileData;
import creator.loki.feature.profile.render.RankCardGenerator;
import org.junit.jupiter.api.Test;
class RankCardGeneratorTest {

View file

@ -2,7 +2,6 @@ package creator.loki.feature.social.commands;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@ -43,7 +42,9 @@ class SocialCommandTest {
var context = new CommandContext(message, List.of(), channel, author, null);
try (var mockedNekos = mockStatic(NekosLifeApi.class)) {
mockedNekos.when(() -> NekosLifeApi.fetchImage(anyString())).thenReturn("https://example.com/hug.gif");
mockedNekos
.when(() -> NekosLifeApi.fetchImage(anyString()))
.thenReturn("https://example.com/hug.gif");
command.execute(context);

View file

@ -1,6 +1,4 @@
package creator.loki.feature.voice;
import creator.loki.feature.voice.service.VoicePreset;
import creator.loki.feature.voice.service.VoiceChannelManager;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ -8,6 +6,8 @@ import static org.mockito.Mockito.*;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.voice.repository.VoiceChannelRepository;
import creator.loki.feature.voice.repository.VoicePresetRepository;
import creator.loki.feature.voice.service.VoiceChannelManager;
import creator.loki.feature.voice.service.VoicePreset;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;