diff --git a/.dockerignore b/.dockerignore index c905585..a0dcb22 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,4 +10,5 @@ out .vscode *.md logs -data +data/dossiers +data/*.db diff --git a/.env.example b/.env.example index 69abb8a..58c3562 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,7 @@ DB_PATH=data/bot.db # DeepSeek AI API (для roast) DEEPSEEK_API_KEY=your_deepseek_key + # Lavalink LAVALINK_HOST=127.0.0.1 LAVALINK_PORT=2333 diff --git a/Dockerfile b/Dockerfile index 6ebeec9..e79e8c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,12 +3,13 @@ WORKDIR /build COPY gradle gradle COPY gradlew settings.gradle build.gradle ./ COPY src src -RUN ./gradlew jar --no-daemon --stacktrace +RUN ./gradlew jar --stacktrace FROM eclipse-temurin:21-jre-alpine RUN addgroup -S bot && adduser -S bot -G bot WORKDIR /app COPY --from=builder /build/build/libs/discord-bot-1.0.0.jar app.jar +COPY data/prompts /app/prompts RUN mkdir -p /app/data /app/logs && chown -R bot:bot /app USER bot VOLUME /app/data diff --git a/build.gradle b/build.gradle index c600a02..b2f350f 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation libs.hikaricp implementation libs.okhttp implementation libs.logging.interceptor + implementation libs.jsoup testImplementation libs.junit.jupiter testImplementation libs.mockito.core diff --git a/docker-compose.yml b/docker-compose.yml index 3ac217e..a6d3d89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,6 @@ services: SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET} DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} AI_CHANNEL_ID: ${AI_CHANNEL_ID} - GIPHY_API_KEY: ${GIPHY_API_KEY} AI_CONTEXT_MSGS: ${AI_CONTEXT_MSGS:-10} AI_MSG_LENGTH: ${AI_MSG_LENGTH:-300} AI_CONTEXT_CHARS: ${AI_CONTEXT_CHARS:-3000} diff --git a/gradle.properties b/gradle.properties index c3dc2e1..a4a79ca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,16 +2,12 @@ jdaVersion=5.0.0-beta.20 dotenvVersion=3.0.0 slf4jVersion=2.0.12 -logbackVersion=1.5.3 +logbackVersion=5.0.3 -# Gradle -org.gradle.jvmargs=-Xmx2048m -org.gradle.parallel=true -org.gradle.caching=true - -# Speed optimizations +# Gradle JVM +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8 org.gradle.parallel=true org.gradle.caching=true org.gradle.configureondemand=true org.gradle.daemon=true -org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 +org.gradle.workers.max=8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 505dc61..2091ebe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ junitPlatformLauncher = "1.12.1" checkstyle = "10.23.0" spotbugs = "4.9.3" spotbugsPlugin = "6.1.7" +jsoup = "1.19.1" [libraries] jda = { module = "net.dv8tion:JDA", version.ref = "jda" } @@ -39,6 +40,7 @@ junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" } spotbugs = { module = "com.github.spotbugs:spotbugs", version.ref = "spotbugs" } spotbugs-annotations = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "spotbugs" } +jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } [plugins] checkstyle = { id = "checkstyle", version = "" } diff --git a/src/main/java/creator/loki/config/BotConfig.java b/src/main/java/creator/loki/config/BotConfig.java index 5d33ecc..84a9594 100644 --- a/src/main/java/creator/loki/config/BotConfig.java +++ b/src/main/java/creator/loki/config/BotConfig.java @@ -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; } diff --git a/src/main/java/creator/loki/config/ConfigLoader.java b/src/main/java/creator/loki/config/ConfigLoader.java index da3b849..6f7ee78 100644 --- a/src/main/java/creator/loki/config/ConfigLoader.java +++ b/src/main/java/creator/loki/config/ConfigLoader.java @@ -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, diff --git a/src/main/java/creator/loki/core/BotInitializer.java b/src/main/java/creator/loki/core/BotInitializer.java index b26ff0d..6913918 100644 --- a/src/main/java/creator/loki/core/BotInitializer.java +++ b/src/main/java/creator/loki/core/BotInitializer.java @@ -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(); diff --git a/src/main/java/creator/loki/core/ListenerAggregator.java b/src/main/java/creator/loki/core/ListenerAggregator.java index 74665a3..695c551 100644 --- a/src/main/java/creator/loki/core/ListenerAggregator.java +++ b/src/main/java/creator/loki/core/ListenerAggregator.java @@ -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 }; } diff --git a/src/main/java/creator/loki/database/DatabaseMigration.java b/src/main/java/creator/loki/database/DatabaseMigration.java index 0b02dfc..d3da6fd 100644 --- a/src/main/java/creator/loki/database/DatabaseMigration.java +++ b/src/main/java/creator/loki/database/DatabaseMigration.java @@ -48,6 +48,7 @@ public class DatabaseMigration { LoggerDecorator.success(logger, "Миграция завершена успешно"); } + @SuppressWarnings("unchecked") private Map parseJson(String path) { try (var reader = new InputStreamReader(new FileInputStream(path), UTF_8)) { var type = new TypeToken>() {}.getType(); @@ -58,6 +59,7 @@ public class DatabaseMigration { } } + @SuppressWarnings("unchecked") private void migrateUserStats(Map data) { var userStats = (Map) data.get("userStats"); if (userStats == null) return; @@ -82,6 +84,7 @@ public class DatabaseMigration { } } + @SuppressWarnings("unchecked") private void migrateReputation(Map data) { var reputation = (Map) data.get("reputation"); if (reputation == null) return; @@ -99,6 +102,7 @@ public class DatabaseMigration { } } + @SuppressWarnings("unchecked") private void migrateShipPairs(Map data) { var shipPairs = (Map) data.get("shipPairs"); if (shipPairs == null) return; @@ -121,6 +125,7 @@ public class DatabaseMigration { } } + @SuppressWarnings("unchecked") private void migrateCustomRoles(Map data) { var customRoles = (Map) data.get("custom_roles"); if (customRoles == null) return; diff --git a/src/main/java/creator/loki/database/DatabaseModule.java b/src/main/java/creator/loki/database/DatabaseModule.java index d3845c4..ab1f939 100644 --- a/src/main/java/creator/loki/database/DatabaseModule.java +++ b/src/main/java/creator/loki/database/DatabaseModule.java @@ -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); } diff --git a/src/main/java/creator/loki/di/BotModule.java b/src/main/java/creator/loki/di/BotModule.java index 45b2e9a..c89fe96 100644 --- a/src/main/java/creator/loki/di/BotModule.java +++ b/src/main/java/creator/loki/di/BotModule.java @@ -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); } } diff --git a/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java b/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java index b593c90..2756296 100644 --- a/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java +++ b/src/main/java/creator/loki/feature/entertainment/EntertainmentCommandRegistrar.java @@ -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; diff --git a/src/main/java/creator/loki/feature/entertainment/commands/GlazbobaCommand.java b/src/main/java/creator/loki/feature/entertainment/commands/GlazbobaCommand.java index 599dc3c..8a32270 100644 --- a/src/main/java/creator/loki/feature/entertainment/commands/GlazbobaCommand.java +++ b/src/main/java/creator/loki/feature/entertainment/commands/GlazbobaCommand.java @@ -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(); diff --git a/src/main/java/creator/loki/feature/entertainment/commands/random/ChoiceOptionParser.java b/src/main/java/creator/loki/feature/entertainment/commands/random/ChoiceOptionParser.java index 81c5f03..703e021 100644 --- a/src/main/java/creator/loki/feature/entertainment/commands/random/ChoiceOptionParser.java +++ b/src/main/java/creator/loki/feature/entertainment/commands/random/ChoiceOptionParser.java @@ -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 parseOptions(String tail) { + public static List parseOptions(String tail) { String t = tail.strip(); for (SplitRule rule : RULES) { if (rule.when.test(t)) { diff --git a/src/main/java/creator/loki/feature/level/LevelModule.java b/src/main/java/creator/loki/feature/level/LevelModule.java index 3f8de86..a93ee70 100644 --- a/src/main/java/creator/loki/feature/level/LevelModule.java +++ b/src/main/java/creator/loki/feature/level/LevelModule.java @@ -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; diff --git a/src/main/java/creator/loki/feature/level/commands/LevelCommandRegistrar.java b/src/main/java/creator/loki/feature/level/commands/LevelCommandRegistrar.java index dea93ff..cd0aa55 100644 --- a/src/main/java/creator/loki/feature/level/commands/LevelCommandRegistrar.java +++ b/src/main/java/creator/loki/feature/level/commands/LevelCommandRegistrar.java @@ -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; diff --git a/src/main/java/creator/loki/feature/level/listener/ResetLevelsButtonHandler.java b/src/main/java/creator/loki/feature/level/listener/ResetLevelsButtonHandler.java index ed41f57..eefed91 100644 --- a/src/main/java/creator/loki/feature/level/listener/ResetLevelsButtonHandler.java +++ b/src/main/java/creator/loki/feature/level/listener/ResetLevelsButtonHandler.java @@ -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; diff --git a/src/main/java/creator/loki/feature/level/service/LevelUpNotifier.java b/src/main/java/creator/loki/feature/level/service/LevelUpNotifier.java index 63c9c54..d7a4700 100644 --- a/src/main/java/creator/loki/feature/level/service/LevelUpNotifier.java +++ b/src/main/java/creator/loki/feature/level/service/LevelUpNotifier.java @@ -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; diff --git a/src/main/java/creator/loki/feature/level/service/VoiceTracker.java b/src/main/java/creator/loki/feature/level/service/VoiceTracker.java index 097aa02..4b175a6 100644 --- a/src/main/java/creator/loki/feature/level/service/VoiceTracker.java +++ b/src/main/java/creator/loki/feature/level/service/VoiceTracker.java @@ -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; diff --git a/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java b/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java index b3b7bcb..36afa96 100644 --- a/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java +++ b/src/main/java/creator/loki/feature/moderation/ModerationCommandRegistrar.java @@ -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; diff --git a/src/main/java/creator/loki/feature/moderation/commands/utils/ClearCommand.java b/src/main/java/creator/loki/feature/moderation/commands/utils/ClearCommand.java index dfd8324..fe8beef 100644 --- a/src/main/java/creator/loki/feature/moderation/commands/utils/ClearCommand.java +++ b/src/main/java/creator/loki/feature/moderation/commands/utils/ClearCommand.java @@ -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 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())); }); diff --git a/src/main/java/creator/loki/feature/moderation/commands/utils/ClearSlashCommands.java b/src/main/java/creator/loki/feature/moderation/commands/utils/ClearSlashCommands.java new file mode 100644 index 0000000..fb7447e --- /dev/null +++ b/src/main/java/creator/loki/feature/moderation/commands/utils/ClearSlashCommands.java @@ -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 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()); + } +} diff --git a/src/main/java/creator/loki/feature/music/manage/GuildMusicManager.java b/src/main/java/creator/loki/feature/music/manage/GuildMusicManager.java index c043f38..d1d918f 100644 --- a/src/main/java/creator/loki/feature/music/manage/GuildMusicManager.java +++ b/src/main/java/creator/loki/feature/music/manage/GuildMusicManager.java @@ -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; diff --git a/src/main/java/creator/loki/feature/profile/render/RankCardGenerator.java b/src/main/java/creator/loki/feature/profile/render/RankCardGenerator.java index 9e3a001..3cba936 100644 --- a/src/main/java/creator/loki/feature/profile/render/RankCardGenerator.java +++ b/src/main/java/creator/loki/feature/profile/render/RankCardGenerator.java @@ -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(); diff --git a/src/main/java/creator/loki/feature/rating/commands/AuraCommand.java b/src/main/java/creator/loki/feature/rating/commands/AuraCommand.java index bf79afd..acf4b69 100644 --- a/src/main/java/creator/loki/feature/rating/commands/AuraCommand.java +++ b/src/main/java/creator/loki/feature/rating/commands/AuraCommand.java @@ -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(); } } diff --git a/src/main/java/creator/loki/feature/rating/commands/GlazeCommand.java b/src/main/java/creator/loki/feature/rating/commands/GlazeCommand.java index 03ff933..3912a5b 100644 --- a/src/main/java/creator/loki/feature/rating/commands/GlazeCommand.java +++ b/src/main/java/creator/loki/feature/rating/commands/GlazeCommand.java @@ -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(); } } diff --git a/src/main/java/creator/loki/feature/rating/commands/GoonCommand.java b/src/main/java/creator/loki/feature/rating/commands/GoonCommand.java index 7f87d4e..c9654ca 100644 --- a/src/main/java/creator/loki/feature/rating/commands/GoonCommand.java +++ b/src/main/java/creator/loki/feature/rating/commands/GoonCommand.java @@ -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(); } } diff --git a/src/main/java/creator/loki/feature/rating/commands/LuckCommand.java b/src/main/java/creator/loki/feature/rating/commands/LuckCommand.java index 2fcae1f..7556aeb 100644 --- a/src/main/java/creator/loki/feature/rating/commands/LuckCommand.java +++ b/src/main/java/creator/loki/feature/rating/commands/LuckCommand.java @@ -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(); } } diff --git a/src/main/java/creator/loki/feature/rating/commands/RizzCommand.java b/src/main/java/creator/loki/feature/rating/commands/RizzCommand.java index 9dbcaa9..691ed52 100644 --- a/src/main/java/creator/loki/feature/rating/commands/RizzCommand.java +++ b/src/main/java/creator/loki/feature/rating/commands/RizzCommand.java @@ -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(); } } diff --git a/src/main/java/creator/loki/feature/rating/commands/SimpCommand.java b/src/main/java/creator/loki/feature/rating/commands/SimpCommand.java index cd23923..a12bfaa 100644 --- a/src/main/java/creator/loki/feature/rating/commands/SimpCommand.java +++ b/src/main/java/creator/loki/feature/rating/commands/SimpCommand.java @@ -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(); } } diff --git a/src/main/java/creator/loki/feature/rating/commands/VibeCommand.java b/src/main/java/creator/loki/feature/rating/commands/VibeCommand.java index a64e264..b958ee8 100644 --- a/src/main/java/creator/loki/feature/rating/commands/VibeCommand.java +++ b/src/main/java/creator/loki/feature/rating/commands/VibeCommand.java @@ -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(); } } diff --git a/src/main/java/creator/loki/feature/rating/config/RatingConfig.java b/src/main/java/creator/loki/feature/rating/config/RatingConfig.java index 645a045..2cc65c0 100644 --- a/src/main/java/creator/loki/feature/rating/config/RatingConfig.java +++ b/src/main/java/creator/loki/feature/rating/config/RatingConfig.java @@ -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 getLuckLevels() { return LEVELS.get("luck"); } - public static List getSimpLevels() { return LEVELS.get("simp"); } - public static List getVibeLevels() { return LEVELS.get("vibe"); } - public static List getGlazeLevels() { return LEVELS.get("glaze"); } + public static List getLuckLevels() { + return LEVELS.get("luck"); + } + + public static List getSimpLevels() { + return LEVELS.get("simp"); + } + + public static List getVibeLevels() { + return LEVELS.get("vibe"); + } + + public static List getGlazeLevels() { + return LEVELS.get("glaze"); + } } diff --git a/src/main/java/creator/loki/feature/roast/AgrCommand.java b/src/main/java/creator/loki/feature/roast/AgrCommand.java index 9e14e50..ad2c948 100644 --- a/src/main/java/creator/loki/feature/roast/AgrCommand.java +++ b/src/main/java/creator/loki/feature/roast/AgrCommand.java @@ -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 textChannels = context.getGuild().getTextChannels(); - List>> futures = new ArrayList<>(); - - for (TextChannel ch : textChannels) { - CompletableFuture> 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 allMessages = new ArrayList<>(); - for (int i = 0; i < textChannels.size(); i++) { - List 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; + }); + }); } } diff --git a/src/main/java/creator/loki/feature/roast/AgrSlashCommands.java b/src/main/java/creator/loki/feature/roast/AgrSlashCommands.java index 3c08863..f751606 100644 --- a/src/main/java/creator/loki/feature/roast/AgrSlashCommands.java +++ b/src/main/java/creator/loki/feature/roast/AgrSlashCommands.java @@ -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 textChannels = event.getGuild().getTextChannels(); - List>> futures = new ArrayList<>(); - for (TextChannel ch : textChannels) { - CompletableFuture> 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 allMessages = new ArrayList<>(); - for (int i = 0; i < textChannels.size(); i++) { - List 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; + }); } } diff --git a/src/main/java/creator/loki/feature/roast/AndrertListener.java b/src/main/java/creator/loki/feature/roast/AndrertListener.java index d42a7cb..0d07477 100644 --- a/src/main/java/creator/loki/feature/roast/AndrertListener.java +++ b/src/main/java/creator/loki/feature/roast/AndrertListener.java @@ -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 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 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 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(" (не удалось прочитать)]"); } diff --git a/src/main/java/creator/loki/feature/roast/CachedMessage.java b/src/main/java/creator/loki/feature/roast/CachedMessage.java new file mode 100644 index 0000000..20abcf8 --- /dev/null +++ b/src/main/java/creator/loki/feature/roast/CachedMessage.java @@ -0,0 +1,3 @@ +package creator.loki.feature.roast; + +public record CachedMessage(String content, String channelName, long timestamp) {} diff --git a/src/main/java/creator/loki/feature/roast/ContentFetcher.java b/src/main/java/creator/loki/feature/roast/ContentFetcher.java new file mode 100644 index 0000000..efaed25 --- /dev/null +++ b/src/main/java/creator/loki/feature/roast/ContentFetcher.java @@ -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 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 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 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); + } +} diff --git a/src/main/java/creator/loki/feature/roast/DeepSeekClient.java b/src/main/java/creator/loki/feature/roast/DeepSeekClient.java new file mode 100644 index 0000000..07c49ff --- /dev/null +++ b/src/main/java/creator/loki/feature/roast/DeepSeekClient.java @@ -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 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; + } + } +} diff --git a/src/main/java/creator/loki/feature/roast/DossierCommand.java b/src/main/java/creator/loki/feature/roast/DossierCommand.java index 9bbafc4..642f2fb 100644 --- a/src/main/java/creator/loki/feature/roast/DossierCommand.java +++ b/src/main/java/creator/loki/feature/roast/DossierCommand.java @@ -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 textChannels = context.getGuild().getTextChannels(); - List>> futures = new ArrayList<>(); - - for (TextChannel ch : textChannels) { - CompletableFuture> 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 allMessages = new ArrayList<>(); - for (int i = 0; i < textChannels.size(); i++) { - List 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; + }); + }); } } diff --git a/src/main/java/creator/loki/feature/roast/DossierManager.java b/src/main/java/creator/loki/feature/roast/DossierManager.java new file mode 100644 index 0000000..e577a0d --- /dev/null +++ b/src/main/java/creator/loki/feature/roast/DossierManager.java @@ -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 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; + } + } +} diff --git a/src/main/java/creator/loki/feature/roast/LanguageFilter.java b/src/main/java/creator/loki/feature/roast/LanguageFilter.java new file mode 100644 index 0000000..192d262 --- /dev/null +++ b/src/main/java/creator/loki/feature/roast/LanguageFilter.java @@ -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 POLISH_CHARS = + "ćńłśźżĆŃŁŚŹŻ".codePoints().boxed().collect(Collectors.toSet()); + private static final Set 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; + } +} diff --git a/src/main/java/creator/loki/feature/roast/MessageScanner.java b/src/main/java/creator/loki/feature/roast/MessageScanner.java new file mode 100644 index 0000000..ed83789 --- /dev/null +++ b/src/main/java/creator/loki/feature/roast/MessageScanner.java @@ -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> scanUserMessages(Guild guild, String targetId) { + return scanUserMessages(guild, targetId, MESSAGES_PER_CHANNEL); + } + + public CompletableFuture> scanUserMessages( + Guild guild, String targetId, int messagesPerChannel) { + List textChannels = guild.getTextChannels(); + List>> futures = new ArrayList<>(); + + for (TextChannel ch : textChannels) { + CompletableFuture> 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 allMessages = new ArrayList<>(); + for (int i = 0; i < textChannels.size(); i++) { + List 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; + }); + } +} diff --git a/src/main/java/creator/loki/feature/roast/RoastModule.java b/src/main/java/creator/loki/feature/roast/RoastModule.java index 0b31fe0..044938f 100644 --- a/src/main/java/creator/loki/feature/roast/RoastModule.java +++ b/src/main/java/creator/loki/feature/roast/RoastModule.java @@ -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); } diff --git a/src/main/java/creator/loki/feature/roast/RoastService.java b/src/main/java/creator/loki/feature/roast/RoastService.java index 79c62a9..1149e33 100644 --- a/src/main/java/creator/loki/feature/roast/RoastService.java +++ b/src/main/java/creator/loki/feature/roast/RoastService.java @@ -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 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 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 context, String targetName) { @@ -159,194 +97,71 @@ public class RoastService { } public String generateResponse(List 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 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 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 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; - } - } - } diff --git a/src/main/java/creator/loki/feature/roast/WebSearchService.java b/src/main/java/creator/loki/feature/roast/WebSearchService.java new file mode 100644 index 0000000..15b6658 --- /dev/null +++ b/src/main/java/creator/loki/feature/roast/WebSearchService.java @@ -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; + } + } +} diff --git a/src/main/java/creator/loki/feature/role/command/MyRolesCommand.java b/src/main/java/creator/loki/feature/role/command/MyRolesCommand.java index 4ad1e60..97c2271 100644 --- a/src/main/java/creator/loki/feature/role/command/MyRolesCommand.java +++ b/src/main/java/creator/loki/feature/role/command/MyRolesCommand.java @@ -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(")"); } diff --git a/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java b/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java index 39d5082..dca21a4 100644 --- a/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java +++ b/src/main/java/creator/loki/feature/serverstats/ServerStatsService.java @@ -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; diff --git a/src/main/java/creator/loki/feature/utility/TimerCommand.java b/src/main/java/creator/loki/feature/utility/TimerCommand.java index 9e6b195..de88599 100644 --- a/src/main/java/creator/loki/feature/utility/TimerCommand.java +++ b/src/main/java/creator/loki/feature/utility/TimerCommand.java @@ -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 + "**с ()\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 + "**с ()\n" + ctx.getAuthor().getAsMention())) + .queue(); + Thread.startVirtualThread( + () -> { + try { + Thread.sleep(secs * 1000); + } catch (InterruptedException e) { + return; + } + ctx.getChannel().sendMessage(msg + " " + ctx.getAuthor().getAsMention()).queue(); + }); } } diff --git a/src/main/java/creator/loki/feature/utility/UtilityCommandRegistrar.java b/src/main/java/creator/loki/feature/utility/UtilityCommandRegistrar.java index 3611c10..d649c51 100644 --- a/src/main/java/creator/loki/feature/utility/UtilityCommandRegistrar.java +++ b/src/main/java/creator/loki/feature/utility/UtilityCommandRegistrar.java @@ -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; diff --git a/src/test/java/creator/loki/config/BotConfigTest.java b/src/test/java/creator/loki/config/BotConfigTest.java index 4a298c0..2da80b8 100644 --- a/src/test/java/creator/loki/config/BotConfigTest.java +++ b/src/test/java/creator/loki/config/BotConfigTest.java @@ -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()); diff --git a/src/test/java/creator/loki/core/registry/SmokeCommandRegistrationTest.java b/src/test/java/creator/loki/core/registry/SmokeCommandRegistrationTest.java index 1552c89..8b6ea0b 100644 --- a/src/test/java/creator/loki/core/registry/SmokeCommandRegistrationTest.java +++ b/src/test/java/creator/loki/core/registry/SmokeCommandRegistrationTest.java @@ -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; diff --git a/src/test/java/creator/loki/feature/entertainment/commands/ChoiceOptionParserTest.java b/src/test/java/creator/loki/feature/entertainment/commands/ChoiceOptionParserTest.java index 369a1f7..2028b53 100644 --- a/src/test/java/creator/loki/feature/entertainment/commands/ChoiceOptionParserTest.java +++ b/src/test/java/creator/loki/feature/entertainment/commands/ChoiceOptionParserTest.java @@ -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; diff --git a/src/test/java/creator/loki/feature/entertainment/commands/CoinCommandTest.java b/src/test/java/creator/loki/feature/entertainment/commands/CoinCommandTest.java index 7706fd6..059cd9e 100644 --- a/src/test/java/creator/loki/feature/entertainment/commands/CoinCommandTest.java +++ b/src/test/java/creator/loki/feature/entertainment/commands/CoinCommandTest.java @@ -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; diff --git a/src/test/java/creator/loki/feature/entertainment/commands/DiceCommandTest.java b/src/test/java/creator/loki/feature/entertainment/commands/DiceCommandTest.java index 313e46e..24b1940 100644 --- a/src/test/java/creator/loki/feature/entertainment/commands/DiceCommandTest.java +++ b/src/test/java/creator/loki/feature/entertainment/commands/DiceCommandTest.java @@ -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; diff --git a/src/test/java/creator/loki/feature/entertainment/commands/EightBallCommandTest.java b/src/test/java/creator/loki/feature/entertainment/commands/EightBallCommandTest.java index 818d2a0..9ebcdf3 100644 --- a/src/test/java/creator/loki/feature/entertainment/commands/EightBallCommandTest.java +++ b/src/test/java/creator/loki/feature/entertainment/commands/EightBallCommandTest.java @@ -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; diff --git a/src/test/java/creator/loki/feature/moderation/repository/ActiveMuteRepositoryTest.java b/src/test/java/creator/loki/feature/moderation/repository/ActiveMuteRepositoryTest.java index 09b6ca9..3f7db65 100644 --- a/src/test/java/creator/loki/feature/moderation/repository/ActiveMuteRepositoryTest.java +++ b/src/test/java/creator/loki/feature/moderation/repository/ActiveMuteRepositoryTest.java @@ -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; diff --git a/src/test/java/creator/loki/feature/moderation/repository/WarningRepositoryTest.java b/src/test/java/creator/loki/feature/moderation/repository/WarningRepositoryTest.java index 680033d..cc53511 100644 --- a/src/test/java/creator/loki/feature/moderation/repository/WarningRepositoryTest.java +++ b/src/test/java/creator/loki/feature/moderation/repository/WarningRepositoryTest.java @@ -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; diff --git a/src/test/java/creator/loki/feature/music/core/LavalinkManagerTest.java b/src/test/java/creator/loki/feature/music/core/LavalinkManagerTest.java index b98f594..7e5c0c6 100644 --- a/src/test/java/creator/loki/feature/music/core/LavalinkManagerTest.java +++ b/src/test/java/creator/loki/feature/music/core/LavalinkManagerTest.java @@ -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); diff --git a/src/test/java/creator/loki/feature/music/manage/TrackSchedulerTest.java b/src/test/java/creator/loki/feature/music/manage/TrackSchedulerTest.java index 26d327b..9379b03 100644 --- a/src/test/java/creator/loki/feature/music/manage/TrackSchedulerTest.java +++ b/src/test/java/creator/loki/feature/music/manage/TrackSchedulerTest.java @@ -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; diff --git a/src/test/java/creator/loki/feature/profile/RankCardGeneratorTest.java b/src/test/java/creator/loki/feature/profile/RankCardGeneratorTest.java index b562275..4cb4364 100644 --- a/src/test/java/creator/loki/feature/profile/RankCardGeneratorTest.java +++ b/src/test/java/creator/loki/feature/profile/RankCardGeneratorTest.java @@ -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 { diff --git a/src/test/java/creator/loki/feature/social/commands/SocialCommandTest.java b/src/test/java/creator/loki/feature/social/commands/SocialCommandTest.java index 81ad7c1..b12098d 100644 --- a/src/test/java/creator/loki/feature/social/commands/SocialCommandTest.java +++ b/src/test/java/creator/loki/feature/social/commands/SocialCommandTest.java @@ -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); diff --git a/src/test/java/creator/loki/feature/voice/VoiceChannelManagerTest.java b/src/test/java/creator/loki/feature/voice/VoiceChannelManagerTest.java index ed6be6d..c25078c 100644 --- a/src/test/java/creator/loki/feature/voice/VoiceChannelManagerTest.java +++ b/src/test/java/creator/loki/feature/voice/VoiceChannelManagerTest.java @@ -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;