feat: ai page parser, message scanner, lang filter, /clear, prompt rewrite
This commit is contained in:
parent
a111bcd674
commit
cc046d7ee6
65 changed files with 1631 additions and 745 deletions
|
|
@ -10,4 +10,5 @@ out
|
|||
.vscode
|
||||
*.md
|
||||
logs
|
||||
data
|
||||
data/dossiers
|
||||
data/*.db
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = "" }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ public class DatabaseMigration {
|
|||
LoggerDecorator.success(logger, "Миграция завершена успешно");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> parseJson(String path) {
|
||||
try (var reader = new InputStreamReader(new FileInputStream(path), UTF_8)) {
|
||||
var type = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
|
|
@ -58,6 +59,7 @@ public class DatabaseMigration {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void migrateUserStats(Map<String, Object> data) {
|
||||
var userStats = (Map<String, Object>) data.get("userStats");
|
||||
if (userStats == null) return;
|
||||
|
|
@ -82,6 +84,7 @@ public class DatabaseMigration {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void migrateReputation(Map<String, Object> data) {
|
||||
var reputation = (Map<String, Object>) data.get("reputation");
|
||||
if (reputation == null) return;
|
||||
|
|
@ -99,6 +102,7 @@ public class DatabaseMigration {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void migrateShipPairs(Map<String, Object> data) {
|
||||
var shipPairs = (Map<String, Object>) data.get("shipPairs");
|
||||
if (shipPairs == null) return;
|
||||
|
|
@ -121,6 +125,7 @@ public class DatabaseMigration {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void migrateCustomRoles(Map<String, Object> data) {
|
||||
var customRoles = (Map<String, Object>) data.get("custom_roles");
|
||||
if (customRoles == null) return;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import java.util.function.Predicate;
|
|||
import java.util.regex.Pattern;
|
||||
|
||||
/** Разбор хвоста сообщения и строки вариантов для {@link RandomChoiceCommand}. */
|
||||
final public class ChoiceOptionParser {
|
||||
public final class ChoiceOptionParser {
|
||||
|
||||
private static final Pattern OR_SPLIT = Pattern.compile("(?i)\\s+или\\s+");
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ final public class ChoiceOptionParser {
|
|||
return rest.substring(sp).trim();
|
||||
}
|
||||
|
||||
public static List<String> parseOptions(String tail) {
|
||||
public static List<String> parseOptions(String tail) {
|
||||
String t = tail.strip();
|
||||
for (SplitRule rule : RULES) {
|
||||
if (rule.when.test(t)) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
package creator.loki.feature.moderation.commands.utils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.Message;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
|
||||
public class ClearCommand implements Command {
|
||||
|
|
@ -16,12 +20,12 @@ public class ClearCommand implements Command {
|
|||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Удаляет указанное количество сообщений";
|
||||
return "Удаляет сообщения. Можно фильтровать по пользователю";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "clear <количество>";
|
||||
return "clear <количество> [@user]";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -48,20 +52,44 @@ public class ClearCommand implements Command {
|
|||
return;
|
||||
}
|
||||
int clamped = Math.clamp(amount, 1, 100);
|
||||
|
||||
Member target = null;
|
||||
if (ctx.getArgs().size() > 1) {
|
||||
String arg = ctx.getArgs().get(1);
|
||||
String targetId = arg.replaceAll("<@!?(\\d+)>", "$1");
|
||||
if (targetId.matches("\\d+")) {
|
||||
target = ctx.getGuild().getMemberById(targetId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(ctx.getChannel() instanceof TextChannel channel)) return;
|
||||
final Member finalTarget = target;
|
||||
channel
|
||||
.getHistory()
|
||||
.retrievePast(clamped + 1)
|
||||
.queue(
|
||||
messages -> {
|
||||
List<Message> toDelete;
|
||||
if (finalTarget != null) {
|
||||
toDelete =
|
||||
messages.stream()
|
||||
.filter(m -> m.getAuthor().getId().equals(finalTarget.getId()))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
toDelete = messages;
|
||||
}
|
||||
if (toDelete.isEmpty()) {
|
||||
err(ctx, "Нет сообщений от этого пользователя для удаления");
|
||||
return;
|
||||
}
|
||||
channel
|
||||
.deleteMessages(messages)
|
||||
.deleteMessages(toDelete)
|
||||
.queue(
|
||||
success ->
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.success(
|
||||
"Сообщения удалены", "Удалено сообщений: " + messages.size()))
|
||||
"Сообщения удалены", "Удалено сообщений: " + toDelete.size()))
|
||||
.queue(msg -> msg.delete().queueAfter(5, TimeUnit.SECONDS)),
|
||||
error -> err(ctx, "Не удалось удалить сообщения: " + error.getMessage()));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
package creator.loki.feature.moderation.commands.utils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.entities.Message;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
import net.dv8tion.jda.api.events.guild.GuildReadyEvent;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Singleton
|
||||
public class ClearSlashCommands extends ListenerAdapter {
|
||||
|
||||
@Override
|
||||
public void onGuildReady(@NotNull GuildReadyEvent event) {
|
||||
event
|
||||
.getJDA()
|
||||
.updateCommands()
|
||||
.addCommands(
|
||||
Commands.slash("clear", "Удалить сообщения")
|
||||
.addOption(OptionType.INTEGER, "amount", "Количество сообщений (1-100)", true)
|
||||
.addOption(
|
||||
OptionType.USER, "user", "Оставить только сообщения пользователя", false))
|
||||
.queue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
|
||||
if (!event.getName().equals("clear")) return;
|
||||
|
||||
var member = event.getMember();
|
||||
if (member == null || !member.hasPermission(Permission.MESSAGE_MANAGE)) {
|
||||
event
|
||||
.replyEmbeds(EmbedTemplates.error("Ошибка", "Нет прав на управление сообщениями"))
|
||||
.setEphemeral(true)
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
event.deferReply(true).queue();
|
||||
|
||||
int amount = event.getOption("amount").getAsInt();
|
||||
int clamped = Math.clamp(amount, 1, 100);
|
||||
|
||||
String targetId = null;
|
||||
var userOption = event.getOption("user");
|
||||
if (userOption != null) {
|
||||
targetId = userOption.getAsUser().getId();
|
||||
}
|
||||
|
||||
if (!(event.getChannel() instanceof TextChannel channel)) {
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Команда работает только в текстовых каналах"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
final String finalTargetId = targetId;
|
||||
channel
|
||||
.getHistory()
|
||||
.retrievePast(clamped + 1)
|
||||
.queue(
|
||||
messages -> {
|
||||
List<Message> toDelete;
|
||||
if (finalTargetId != null) {
|
||||
toDelete =
|
||||
messages.stream()
|
||||
.filter(m -> m.getAuthor().getId().equals(finalTargetId))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
toDelete = messages;
|
||||
}
|
||||
if (toDelete.isEmpty()) {
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Нет сообщений для удаления"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
channel
|
||||
.deleteMessages(toDelete)
|
||||
.queue(
|
||||
success ->
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.success(
|
||||
"Сообщения удалены", "Удалено сообщений: " + toDelete.size()))
|
||||
.queue(),
|
||||
error ->
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Ошибка", "Не удалось удалить: " + error.getMessage()))
|
||||
.queue());
|
||||
},
|
||||
error ->
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Не удалось получить историю"))
|
||||
.queue());
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,27 +74,33 @@ public class RatingConfig {
|
|||
new RatingLevel(30, "😐 Ни туда ни сюда", new Color(128, 128, 128)),
|
||||
new RatingLevel(10, "😬 Невезучий", new Color(255, 165, 0)),
|
||||
new RatingLevel(0, "💀 Проклят", new Color(231, 76, 60))));
|
||||
m.put("simp", List.of(
|
||||
new RatingLevel(90, "👑 КОРОЛЬ СИМПОВ!", new Color(255, 105, 180)),
|
||||
new RatingLevel(70, "😍 Задрот", new Color(255, 182, 193)),
|
||||
new RatingLevel(50, "🙂 Нормальный", new Color(52, 152, 219)),
|
||||
new RatingLevel(30, "😐 Холодный", new Color(128, 128, 128)),
|
||||
new RatingLevel(10, "💪 Альфа", new Color(100, 255, 150)),
|
||||
new RatingLevel(0, "🦍 ГОРИЛЛА", new Color(150, 100, 50))));
|
||||
m.put("vibe", List.of(
|
||||
new RatingLevel(90, "🎶 ЛЕГЕНДАРНЫЙ ВАЙБ!", new Color(255, 215, 0)),
|
||||
new RatingLevel(70, "🔥 Крутой вайб", new Color(255, 100, 0)),
|
||||
new RatingLevel(50, "🙂 Норм вайб", new Color(52, 152, 219)),
|
||||
new RatingLevel(30, "😐 Так себе", new Color(128, 128, 128)),
|
||||
new RatingLevel(10, "📉 Вайб упал", new Color(255, 165, 0)),
|
||||
new RatingLevel(0, "💀 Вайба нет", new Color(231, 76, 60))));
|
||||
m.put("glaze", List.of(
|
||||
new RatingLevel(90, "🍩 НЕВЕРОЯТНО ГЛАЗИРОВАН!", new Color(255, 215, 0)),
|
||||
new RatingLevel(70, "✨ Сияет", new Color(255, 200, 100)),
|
||||
new RatingLevel(50, "🙂 Средне", new Color(52, 152, 219)),
|
||||
new RatingLevel(30, "😐 Матовый", new Color(128, 128, 128)),
|
||||
new RatingLevel(10, "🧊 Подтаивает", new Color(200, 200, 255)),
|
||||
new RatingLevel(0, "💀 Сухой", new Color(150, 150, 150))));
|
||||
m.put(
|
||||
"simp",
|
||||
List.of(
|
||||
new RatingLevel(90, "👑 КОРОЛЬ СИМПОВ!", new Color(255, 105, 180)),
|
||||
new RatingLevel(70, "😍 Задрот", new Color(255, 182, 193)),
|
||||
new RatingLevel(50, "🙂 Нормальный", new Color(52, 152, 219)),
|
||||
new RatingLevel(30, "😐 Холодный", new Color(128, 128, 128)),
|
||||
new RatingLevel(10, "💪 Альфа", new Color(100, 255, 150)),
|
||||
new RatingLevel(0, "🦍 ГОРИЛЛА", new Color(150, 100, 50))));
|
||||
m.put(
|
||||
"vibe",
|
||||
List.of(
|
||||
new RatingLevel(90, "🎶 ЛЕГЕНДАРНЫЙ ВАЙБ!", new Color(255, 215, 0)),
|
||||
new RatingLevel(70, "🔥 Крутой вайб", new Color(255, 100, 0)),
|
||||
new RatingLevel(50, "🙂 Норм вайб", new Color(52, 152, 219)),
|
||||
new RatingLevel(30, "😐 Так себе", new Color(128, 128, 128)),
|
||||
new RatingLevel(10, "📉 Вайб упал", new Color(255, 165, 0)),
|
||||
new RatingLevel(0, "💀 Вайба нет", new Color(231, 76, 60))));
|
||||
m.put(
|
||||
"glaze",
|
||||
List.of(
|
||||
new RatingLevel(90, "🍩 НЕВЕРОЯТНО ГЛАЗИРОВАН!", new Color(255, 215, 0)),
|
||||
new RatingLevel(70, "✨ Сияет", new Color(255, 200, 100)),
|
||||
new RatingLevel(50, "🙂 Средне", new Color(52, 152, 219)),
|
||||
new RatingLevel(30, "😐 Матовый", new Color(128, 128, 128)),
|
||||
new RatingLevel(10, "🧊 Подтаивает", new Color(200, 200, 255)),
|
||||
new RatingLevel(0, "💀 Сухой", new Color(150, 150, 150))));
|
||||
LEVELS = Collections.unmodifiableMap(m);
|
||||
}
|
||||
|
||||
|
|
@ -122,8 +128,19 @@ public class RatingConfig {
|
|||
return LEVELS.get("rizz");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getLuckLevels() { return LEVELS.get("luck"); }
|
||||
public static List<RatingLevel> getSimpLevels() { return LEVELS.get("simp"); }
|
||||
public static List<RatingLevel> getVibeLevels() { return LEVELS.get("vibe"); }
|
||||
public static List<RatingLevel> getGlazeLevels() { return LEVELS.get("glaze"); }
|
||||
public static List<RatingLevel> getLuckLevels() {
|
||||
return LEVELS.get("luck");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getSimpLevels() {
|
||||
return LEVELS.get("simp");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getVibeLevels() {
|
||||
return LEVELS.get("vibe");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getGlazeLevels() {
|
||||
return LEVELS.get("glaze");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,23 @@
|
|||
package creator.loki.feature.roast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import net.dv8tion.jda.api.entities.Message;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
|
||||
public class AgrCommand implements Command {
|
||||
private static final String ANDRERT_USER_ID = "1405129454165954630";
|
||||
private static final int MESSAGES_PER_CHANNEL = 100;
|
||||
|
||||
private final BotConfig config;
|
||||
private final RoastService roastService;
|
||||
private final AndrertListener autoResponder;
|
||||
private final MessageScanner messageScanner;
|
||||
|
||||
public AgrCommand(BotConfig config, RoastService roastService, AndrertListener autoResponder) {
|
||||
this.config = config;
|
||||
this.roastService = roastService;
|
||||
this.autoResponder = autoResponder;
|
||||
this.messageScanner = new MessageScanner();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -48,8 +43,10 @@ public class AgrCommand implements Command {
|
|||
@Override
|
||||
public void execute(CommandContext context) {
|
||||
if (!config.getSuperAdminId().equals(context.getAuthor().getId())) {
|
||||
context.getChannel().sendMessageEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Только супер-администратор")).queue();
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Только супер-администратор"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
if (context.getGuild() == null) {
|
||||
|
|
@ -68,96 +65,52 @@ public class AgrCommand implements Command {
|
|||
return;
|
||||
}
|
||||
String finalId = targetId;
|
||||
context.getGuild().retrieveMemberById(targetId).queue(
|
||||
member -> {
|
||||
autoResponder.trackUser(finalId);
|
||||
doScan(context, finalId, member.getEffectiveName());
|
||||
},
|
||||
error -> context.getChannel().sendMessage("Пользователь не найден на сервере").queue());
|
||||
context
|
||||
.getGuild()
|
||||
.retrieveMemberById(targetId)
|
||||
.queue(
|
||||
member -> {
|
||||
autoResponder.trackUser(finalId);
|
||||
doScan(context, finalId, member.getEffectiveName());
|
||||
},
|
||||
error ->
|
||||
context.getChannel().sendMessage("Пользователь не найден на сервере").queue());
|
||||
}
|
||||
}
|
||||
|
||||
private void doScan(CommandContext context, String targetId, String targetName) {
|
||||
context.getMessage().reply("🔍 Сканирую сообщения " + targetName + "...")
|
||||
.queue(reply -> {
|
||||
List<TextChannel> textChannels = context.getGuild().getTextChannels();
|
||||
List<CompletableFuture<List<Message>>> futures = new ArrayList<>();
|
||||
|
||||
for (TextChannel ch : textChannels) {
|
||||
CompletableFuture<List<Message>> future = new CompletableFuture<>();
|
||||
futures.add(future);
|
||||
ch.getHistory().retrievePast(MESSAGES_PER_CHANNEL).queue(future::complete, e -> future.complete(List.of()));
|
||||
}
|
||||
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(v -> {
|
||||
List<RoastService.CachedMessage> allMessages = new ArrayList<>();
|
||||
for (int i = 0; i < textChannels.size(); i++) {
|
||||
List<Message> msgs = futures.get(i).join();
|
||||
String chName = textChannels.get(i).getName();
|
||||
for (Message msg : msgs) {
|
||||
if (msg.getAuthor().getId().equals(targetId)) {
|
||||
String text = msg.getContentRaw() + replyInfo(msg) + attachmentInfo(msg);
|
||||
allMessages.add(new RoastService.CachedMessage(
|
||||
text, chName, msg.getTimeCreated().toEpochSecond() * 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
allMessages.sort((a, b) -> Long.compare(a.timestamp(), b.timestamp()));
|
||||
if (allMessages.size() > 10) {
|
||||
allMessages = allMessages.subList(allMessages.size() - 10, allMessages.size());
|
||||
}
|
||||
return allMessages;
|
||||
})
|
||||
.thenAccept(msgs -> {
|
||||
if (msgs.isEmpty()) {
|
||||
reply.editMessage("Нет сообщений от " + targetName).queue();
|
||||
return;
|
||||
}
|
||||
String response = roastService.generateResponse(msgs, targetName, targetId);
|
||||
if (response == null || response.isBlank()) {
|
||||
reply.editMessage("AI не ответил").queue();
|
||||
return;
|
||||
}
|
||||
reply.editMessage(response).queue();
|
||||
})
|
||||
.exceptionally(e -> {
|
||||
reply.editMessage("Ошибка: " + e.getMessage()).queue();
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private String replyInfo(Message msg) {
|
||||
Message ref = msg.getReferencedMessage();
|
||||
if (ref == null) return "";
|
||||
String refAuthor = ref.getAuthor().isBot() ? "бот" : ref.getAuthor().getEffectiveName();
|
||||
String refText = ref.getContentRaw().replace("\n", " ").strip();
|
||||
if (refText.length() > 80) refText = refText.substring(0, 80) + "...";
|
||||
return " (в ответ на @" + refAuthor + ": \"" + refText + "\")";
|
||||
}
|
||||
|
||||
private String attachmentInfo(Message msg) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (var att : msg.getAttachments()) {
|
||||
if (att.isImage()) {
|
||||
sb.append(" [изображение: ").append(att.getFileName()).append("]");
|
||||
} else if (att.isVideo()) {
|
||||
sb.append(" [видео: ").append(att.getFileName()).append("]");
|
||||
} else if (att.getFileName().endsWith(".txt")) {
|
||||
String txt = roastService.readTextFile(att.getUrl());
|
||||
if (txt != null) {
|
||||
sb.append("\n[содержимое txt файла ").append(att.getFileName()).append("]:\n").append(txt);
|
||||
} else {
|
||||
sb.append(" [txt файл: ").append(att.getFileName()).append(" (не удалось прочитать)]");
|
||||
}
|
||||
} else {
|
||||
sb.append(" [файл: ").append(att.getFileName()).append("]");
|
||||
}
|
||||
}
|
||||
if (!msg.getStickers().isEmpty()) {
|
||||
sb.append(" [стикер]");
|
||||
}
|
||||
return sb.toString();
|
||||
context
|
||||
.getMessage()
|
||||
.reply("🔍 Сканирую сообщения " + targetName + "...")
|
||||
.queue(
|
||||
reply -> {
|
||||
messageScanner
|
||||
.scanUserMessages(context.getGuild(), targetId)
|
||||
.thenApply(
|
||||
msgs -> {
|
||||
if (msgs.size() > 10) {
|
||||
return msgs.subList(msgs.size() - 10, msgs.size());
|
||||
}
|
||||
return msgs;
|
||||
})
|
||||
.thenAccept(
|
||||
msgs -> {
|
||||
if (msgs.isEmpty()) {
|
||||
reply.editMessage("Нет сообщений от " + targetName).queue();
|
||||
return;
|
||||
}
|
||||
String response = roastService.generateResponse(msgs, targetName, targetId);
|
||||
if (response == null || response.isBlank()) {
|
||||
reply.editMessage("AI не ответил").queue();
|
||||
return;
|
||||
}
|
||||
reply.editMessage(response).queue();
|
||||
})
|
||||
.exceptionally(
|
||||
e -> {
|
||||
reply.editMessage("Ошибка: " + e.getMessage()).queue();
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,26 @@
|
|||
package creator.loki.feature.roast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import net.dv8tion.jda.api.entities.Message;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Singleton
|
||||
public class AgrSlashCommands extends ListenerAdapter {
|
||||
private static final String ANDRERT_USER_ID = "1405129454165954630";
|
||||
private static final int MESSAGES_PER_CHANNEL = 100;
|
||||
|
||||
private final BotConfig config;
|
||||
private final RoastService roastService;
|
||||
private final MessageScanner messageScanner;
|
||||
|
||||
@Inject
|
||||
public AgrSlashCommands(BotConfig config, RoastService roastService) {
|
||||
this.config = config;
|
||||
this.roastService = roastService;
|
||||
this.messageScanner = new MessageScanner();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -33,8 +28,10 @@ public class AgrSlashCommands extends ListenerAdapter {
|
|||
if (!event.getName().equals("agr")) return;
|
||||
|
||||
if (!config.getSuperAdminId().equals(event.getUser().getId())) {
|
||||
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Только супер-администратор"))
|
||||
.setEphemeral(true).queue();
|
||||
event
|
||||
.replyEmbeds(EmbedTemplates.error("Ошибка", "Только супер-администратор"))
|
||||
.setEphemeral(true)
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
if (event.getGuild() == null) {
|
||||
|
|
@ -59,50 +56,34 @@ public class AgrSlashCommands extends ListenerAdapter {
|
|||
|
||||
String finalTargetName = targetName;
|
||||
String finalTargetId = targetId;
|
||||
List<TextChannel> textChannels = event.getGuild().getTextChannels();
|
||||
List<CompletableFuture<List<Message>>> futures = new ArrayList<>();
|
||||
|
||||
for (TextChannel ch : textChannels) {
|
||||
CompletableFuture<List<Message>> future = new CompletableFuture<>();
|
||||
futures.add(future);
|
||||
ch.getHistory().retrievePast(MESSAGES_PER_CHANNEL).queue(future::complete, e -> future.complete(List.of()));
|
||||
}
|
||||
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(v -> {
|
||||
List<RoastService.CachedMessage> allMessages = new ArrayList<>();
|
||||
for (int i = 0; i < textChannels.size(); i++) {
|
||||
List<Message> msgs = futures.get(i).join();
|
||||
String chName = textChannels.get(i).getName();
|
||||
for (Message msg : msgs) {
|
||||
if (msg.getAuthor().getId().equals(finalTargetId)) {
|
||||
allMessages.add(new RoastService.CachedMessage(
|
||||
msg.getContentRaw(), chName, msg.getTimeCreated().toEpochSecond() * 1000));
|
||||
messageScanner
|
||||
.scanUserMessages(event.getGuild(), finalTargetId)
|
||||
.thenApply(
|
||||
msgs -> {
|
||||
if (msgs.size() > 10) {
|
||||
return msgs.subList(msgs.size() - 10, msgs.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
allMessages.sort((a, b) -> Long.compare(a.timestamp(), b.timestamp()));
|
||||
if (allMessages.size() > 10) {
|
||||
allMessages = allMessages.subList(allMessages.size() - 10, allMessages.size());
|
||||
}
|
||||
return allMessages;
|
||||
})
|
||||
.thenCompose(msgs -> {
|
||||
if (msgs.isEmpty()) {
|
||||
event.getHook().editOriginal("Нет сообщений от " + finalTargetName).queue();
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
String response = roastService.generateResponse(msgs, finalTargetName, finalTargetId);
|
||||
if (response != null && !response.isBlank()) {
|
||||
event.getHook().editOriginal(response).queue();
|
||||
} else {
|
||||
event.getHook().editOriginal("AI не ответил").queue();
|
||||
}
|
||||
return CompletableFuture.completedFuture(null);
|
||||
})
|
||||
.exceptionally(e -> {
|
||||
event.getHook().editOriginal("Ошибка: " + e.getMessage()).queue();
|
||||
return null;
|
||||
});
|
||||
return msgs;
|
||||
})
|
||||
.thenCompose(
|
||||
msgs -> {
|
||||
if (msgs.isEmpty()) {
|
||||
event.getHook().editOriginal("Нет сообщений от " + finalTargetName).queue();
|
||||
return java.util.concurrent.CompletableFuture.completedFuture(null);
|
||||
}
|
||||
String response = roastService.generateResponse(msgs, finalTargetName, finalTargetId);
|
||||
if (response != null && !response.isBlank()) {
|
||||
event.getHook().editOriginal(response).queue();
|
||||
} else {
|
||||
event.getHook().editOriginal("AI не ответил").queue();
|
||||
}
|
||||
return java.util.concurrent.CompletableFuture.completedFuture(null);
|
||||
})
|
||||
.exceptionally(
|
||||
e -> {
|
||||
event.getHook().editOriginal("Ошибка: " + e.getMessage()).queue();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import creator.loki.config.BotConfig;
|
|||
import net.dv8tion.jda.api.entities.Message;
|
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Singleton
|
||||
|
|
@ -40,8 +39,10 @@ public class AndrertListener extends ListenerAdapter {
|
|||
if (event.getAuthor().isBot()) return;
|
||||
|
||||
String channelId = event.getChannel().getId();
|
||||
boolean isAllowedChannel = allowedChannelId != null && !allowedChannelId.isBlank()
|
||||
&& channelId.equals(allowedChannelId);
|
||||
boolean isAllowedChannel =
|
||||
allowedChannelId != null
|
||||
&& !allowedChannelId.isBlank()
|
||||
&& channelId.equals(allowedChannelId);
|
||||
|
||||
if (!isAllowedChannel) {
|
||||
String userId = event.getAuthor().getId();
|
||||
|
|
@ -61,48 +62,85 @@ public class AndrertListener extends ListenerAdapter {
|
|||
}
|
||||
|
||||
private void respond(MessageReceivedEvent event, String userId) {
|
||||
event.getChannel().getHistory().retrievePast(CHANNEL_CONTEXT)
|
||||
.queue(messages -> {
|
||||
String targetName = event.getAuthor().getEffectiveName();
|
||||
List<RoastService.CachedMessage> context = new ArrayList<>();
|
||||
for (Message msg : messages) {
|
||||
String author = msg.getAuthor().isBot() ? "бот" : msg.getAuthor().getEffectiveName();
|
||||
String raw = event.getMessage().getContentRaw();
|
||||
if (!raw.isBlank() && !LanguageFilter.isAllowed(raw)) {
|
||||
event
|
||||
.getMessage()
|
||||
.reply("только польский русский украинский английский литовский. остальных языков нет")
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder line = new StringBuilder();
|
||||
line.append(author).append(": ").append(msg.getContentRaw());
|
||||
line.append(replyInfo(msg));
|
||||
line.append(attachmentInfo(msg));
|
||||
respondAi(event, userId);
|
||||
}
|
||||
|
||||
context.add(new RoastService.CachedMessage(
|
||||
line.toString(),
|
||||
event.getChannel().getName(),
|
||||
msg.getTimeCreated().toEpochSecond() * 1000));
|
||||
}
|
||||
java.util.Collections.reverse(context);
|
||||
private void respondAi(MessageReceivedEvent event, String userId) {
|
||||
event
|
||||
.getChannel()
|
||||
.getHistory()
|
||||
.retrievePast(CHANNEL_CONTEXT)
|
||||
.queue(
|
||||
messages -> {
|
||||
String targetName = event.getAuthor().getEffectiveName();
|
||||
List<CachedMessage> context = new ArrayList<>();
|
||||
for (int idx = 0; idx < messages.size(); idx++) {
|
||||
Message msg = messages.get(idx);
|
||||
boolean isSelf =
|
||||
msg.getAuthor().getId().equals(event.getJDA().getSelfUser().getId());
|
||||
String author = isSelf ? "ты" : msg.getAuthor().getEffectiveName();
|
||||
String authorId = msg.getAuthor().getId();
|
||||
boolean isCurrent = (idx == 0);
|
||||
|
||||
String response = roastService.generateResponse(context, targetName, userId);
|
||||
if (response == null || response.isBlank()) return;
|
||||
String label = isCurrent ? "[now]" : "[history]";
|
||||
StringBuilder line = new StringBuilder();
|
||||
line.append(label)
|
||||
.append(" ")
|
||||
.append(author)
|
||||
.append("(")
|
||||
.append(authorId)
|
||||
.append("): ")
|
||||
.append(msg.getContentRaw());
|
||||
line.append(replyInfo(msg));
|
||||
line.append(attachmentInfo(msg));
|
||||
|
||||
String gifUrl = null;
|
||||
String text = response;
|
||||
int gifIdx = response.indexOf("gif:");
|
||||
if (gifIdx >= 0) {
|
||||
String after = response.substring(gifIdx + 4).strip();
|
||||
text = response.substring(0, gifIdx).strip();
|
||||
if (!after.isBlank()) {
|
||||
String topic = after.split("\\s+")[0];
|
||||
gifUrl = roastService.searchGif(topic);
|
||||
}
|
||||
}
|
||||
context.add(
|
||||
new CachedMessage(
|
||||
line.toString(),
|
||||
event.getChannel().getName(),
|
||||
msg.getTimeCreated().toEpochSecond() * 1000));
|
||||
}
|
||||
java.util.Collections.reverse(context);
|
||||
|
||||
var builder = new MessageCreateBuilder();
|
||||
if (gifUrl != null) {
|
||||
builder.addContent(text + "\n" + gifUrl);
|
||||
} else {
|
||||
builder.addContent(text);
|
||||
}
|
||||
event.getMessage().reply(builder.build()).queue();
|
||||
});
|
||||
String response = roastService.generateResponse(context, targetName, userId);
|
||||
if (response == null || response.isBlank()) return;
|
||||
|
||||
sendInParts(event, response);
|
||||
});
|
||||
}
|
||||
|
||||
private void sendInParts(MessageReceivedEvent event, String text) {
|
||||
if (text.length() <= 2000) {
|
||||
event.getMessage().reply(text).queue();
|
||||
return;
|
||||
}
|
||||
List<String> parts = new ArrayList<>();
|
||||
int start = 0;
|
||||
while (start < text.length() && parts.size() < 3) {
|
||||
int end = Math.min(start + 1990, text.length());
|
||||
if (end < text.length()) {
|
||||
int nl = text.lastIndexOf('\n', end);
|
||||
if (nl > start) end = nl;
|
||||
}
|
||||
parts.add(text.substring(start, end).strip());
|
||||
start = end;
|
||||
}
|
||||
for (int i = 0; i < parts.size(); i++) {
|
||||
if (i == 0) {
|
||||
event.getMessage().reply(parts.get(i)).queue();
|
||||
} else {
|
||||
event.getChannel().sendMessage(parts.get(i)).queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String replyInfo(Message msg) {
|
||||
|
|
@ -122,9 +160,12 @@ public class AndrertListener extends ListenerAdapter {
|
|||
} else if (att.isVideo()) {
|
||||
sb.append(" [видео: ").append(att.getFileName()).append("]");
|
||||
} else if (att.getFileName().endsWith(".txt")) {
|
||||
String txt = roastService.readTextFile(att.getUrl());
|
||||
String txt = roastService.readTextFilePreview(att.getUrl());
|
||||
if (txt != null) {
|
||||
sb.append("\n[содержимое txt файла ").append(att.getFileName()).append("]:\n").append(txt);
|
||||
sb.append("\n[содержимое txt файла ")
|
||||
.append(att.getFileName())
|
||||
.append("]:\n")
|
||||
.append(txt);
|
||||
} else {
|
||||
sb.append(" [txt файл: ").append(att.getFileName()).append(" (не удалось прочитать)]");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
package creator.loki.feature.roast;
|
||||
|
||||
public record CachedMessage(String content, String channelName, long timestamp) {}
|
||||
296
src/main/java/creator/loki/feature/roast/ContentFetcher.java
Normal file
296
src/main/java/creator/loki/feature/roast/ContentFetcher.java
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
package creator.loki.feature.roast;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Singleton
|
||||
public class ContentFetcher {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ContentFetcher.class);
|
||||
private static final Pattern URL_PATTERN = Pattern.compile("https?://[^\\s]+");
|
||||
private static final int MAX_FETCH_CHARS = 3000;
|
||||
private static final int MAX_PREVIEW_CHARS = 1000;
|
||||
|
||||
private static final Set<String> TEXT_EXTENSIONS =
|
||||
Set.of(
|
||||
".txt",
|
||||
".md",
|
||||
".json",
|
||||
".yaml",
|
||||
".yml",
|
||||
".xml",
|
||||
".csv",
|
||||
".log",
|
||||
".properties",
|
||||
".conf",
|
||||
".cfg",
|
||||
".ini",
|
||||
".toml",
|
||||
".sh",
|
||||
".bash",
|
||||
".zsh",
|
||||
".bat",
|
||||
".ps1",
|
||||
".java",
|
||||
".kt",
|
||||
".scala",
|
||||
".py",
|
||||
".js",
|
||||
".ts",
|
||||
".jsx",
|
||||
".tsx",
|
||||
".rs",
|
||||
".go",
|
||||
".rb",
|
||||
".php",
|
||||
".c",
|
||||
".h",
|
||||
".cpp",
|
||||
".hpp",
|
||||
".cs",
|
||||
".swift",
|
||||
".r",
|
||||
".lua",
|
||||
".hs",
|
||||
".clj",
|
||||
".sql",
|
||||
".rkt",
|
||||
".dart",
|
||||
".gradle",
|
||||
".groovy",
|
||||
".html",
|
||||
".css",
|
||||
".scss",
|
||||
".less",
|
||||
".sass");
|
||||
|
||||
private static final Set<String> MEDIA_EXTENSIONS =
|
||||
Set.of(
|
||||
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg", ".mp4", ".webm", ".avi", ".mov",
|
||||
".mkv", ".mp3", ".wav", ".ogg", ".flac", ".zip", ".tar", ".gz", ".rar", ".7z", ".pdf",
|
||||
".doc", ".docx", ".xls", ".xlsx");
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
|
||||
public ContentFetcher() {
|
||||
this.httpClient =
|
||||
new OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.followRedirects(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
public String readTextFilePreview(String url) {
|
||||
return readTextFile(url, MAX_PREVIEW_CHARS);
|
||||
}
|
||||
|
||||
public String readTextFile(String url) {
|
||||
return readTextFile(url, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
private String readTextFile(String url, int maxChars) {
|
||||
Request req = new Request.Builder().url(url).get().build();
|
||||
try (Response resp = httpClient.newCall(req).execute()) {
|
||||
if (resp.isSuccessful() && resp.body() != null) {
|
||||
String text = resp.body().string();
|
||||
if (text.length() > maxChars) {
|
||||
text =
|
||||
text.substring(0, maxChars)
|
||||
+ "\n... [файл обрезан, показано "
|
||||
+ maxChars
|
||||
+ " символов]";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to read text file {}", url, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String fetchUrlContent(String text) {
|
||||
Matcher matcher = URL_PATTERN.matcher(text);
|
||||
List<String> urls = new ArrayList<>();
|
||||
while (matcher.find()) {
|
||||
String url = matcher.group();
|
||||
if (!isMediaExtension(url)) {
|
||||
urls.add(url);
|
||||
}
|
||||
}
|
||||
if (urls.isEmpty()) return null;
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (String url : urls) {
|
||||
String content = parseSingleUrl(url);
|
||||
if (content != null) {
|
||||
if (result.length() + content.length() > MAX_FETCH_CHARS) break;
|
||||
result.append(content).append("\n---\n");
|
||||
}
|
||||
}
|
||||
return result.isEmpty() ? null : result.toString().strip();
|
||||
}
|
||||
|
||||
private String parseSingleUrl(String url) {
|
||||
if (isTextExtension(url)) {
|
||||
return readTextFilePreview(url);
|
||||
}
|
||||
if (url.contains("github.com") || url.contains("raw.githubusercontent.com")) {
|
||||
return parseGitHubUrl(url);
|
||||
}
|
||||
return parseHtmlPage(url);
|
||||
}
|
||||
|
||||
private String parseGitHubUrl(String url) {
|
||||
if (url.contains("raw.githubusercontent.com")) {
|
||||
return readTextFilePreview(url);
|
||||
}
|
||||
try {
|
||||
Document doc =
|
||||
Jsoup.connect(url).userAgent("Mozilla/5.0 (compatible; LokiBot)").timeout(10000).get();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (url.matches(".*github\\.com/[^/]+/[^/]+/blob/.*")) {
|
||||
org.jsoup.select.Elements lines = doc.select(".blob-wrapper .highlight .blob-code");
|
||||
if (lines.isEmpty()) {
|
||||
org.jsoup.select.Elements raw = doc.select(".blob-wrapper table td:last-child");
|
||||
if (!raw.isEmpty()) lines = raw;
|
||||
}
|
||||
if (lines.isEmpty()) {
|
||||
org.jsoup.select.Elements code = doc.select("table.highlight tr td:last-child");
|
||||
if (!code.isEmpty()) lines = code;
|
||||
}
|
||||
if (!lines.isEmpty()) {
|
||||
sb.append("[код файла]:\n");
|
||||
for (var line : lines) {
|
||||
String l = line.text().strip();
|
||||
if (!l.isBlank()) sb.append(l).append("\n");
|
||||
}
|
||||
} else {
|
||||
String fallback = extractMainContent(doc);
|
||||
if (fallback != null) sb.append(fallback);
|
||||
}
|
||||
} else if (url.matches(".*github\\.com/[^/]+/[^/]+/issues/\\d+.*")
|
||||
|| url.matches(".*github\\.com/[^/]+/[^/]+/pull/\\d+.*")) {
|
||||
org.jsoup.select.Elements title = doc.select(".gh-header-title, .js-issue-title");
|
||||
if (!title.isEmpty()) {
|
||||
sb.append("[заголовок]: ").append(title.first().text()).append("\n");
|
||||
}
|
||||
org.jsoup.select.Elements body = doc.select(".comment-body, .edit-comment-hide");
|
||||
if (!body.isEmpty()) {
|
||||
sb.append("[описание]:\n");
|
||||
for (var el : body) sb.append(el.text()).append("\n");
|
||||
}
|
||||
org.jsoup.select.Elements comments = doc.select(".comment .comment-body");
|
||||
if (!comments.isEmpty()) {
|
||||
sb.append("[комментарии]:\n");
|
||||
int count = 0;
|
||||
for (var el : comments) {
|
||||
if (count >= 5) break;
|
||||
String t = el.text().strip();
|
||||
if (!t.isBlank()) {
|
||||
sb.append("- ").append(t).append("\n");
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
org.jsoup.select.Elements readme = doc.select("article.markdown-body, .readme, #readme");
|
||||
if (!readme.isEmpty()) {
|
||||
sb.append("[README]:\n").append(readme.first().text());
|
||||
} else {
|
||||
String fallback = extractMainContent(doc);
|
||||
if (fallback != null) sb.append(fallback);
|
||||
}
|
||||
org.jsoup.select.Elements repoName = doc.select("strong[itemprop=name], .repohead h1");
|
||||
if (!repoName.isEmpty() && sb.isEmpty()) {
|
||||
sb.append("[репозиторий]: ").append(repoName.first().text());
|
||||
}
|
||||
}
|
||||
|
||||
String result = sb.toString().strip();
|
||||
if (result.length() > MAX_PREVIEW_CHARS) {
|
||||
result = result.substring(0, MAX_PREVIEW_CHARS) + "...";
|
||||
}
|
||||
return result.isBlank() ? null : result;
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to parse GitHub URL {}", url, e);
|
||||
return readTextFilePreview(url);
|
||||
}
|
||||
}
|
||||
|
||||
private String parseHtmlPage(String url) {
|
||||
try {
|
||||
Document doc =
|
||||
Jsoup.connect(url).userAgent("Mozilla/5.0 (compatible; LokiBot)").timeout(10000).get();
|
||||
|
||||
String content = extractMainContent(doc);
|
||||
if (content != null && content.length() > MAX_PREVIEW_CHARS) {
|
||||
content = content.substring(0, MAX_PREVIEW_CHARS) + "...";
|
||||
}
|
||||
return content;
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to parse HTML page {}", url, e);
|
||||
return readTextFilePreview(url);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractMainContent(Document doc) {
|
||||
String[] selectors = {
|
||||
"article",
|
||||
"main",
|
||||
"[role=main]",
|
||||
".post-content",
|
||||
".entry-content",
|
||||
".content",
|
||||
".article-content",
|
||||
".markdown-body",
|
||||
".documentation",
|
||||
"#readme",
|
||||
".readme",
|
||||
".container"
|
||||
};
|
||||
for (String sel : selectors) {
|
||||
var el = doc.selectFirst(sel);
|
||||
if (el != null) {
|
||||
String text = el.text().strip();
|
||||
if (!text.isBlank()) return text;
|
||||
}
|
||||
}
|
||||
String body = doc.body() != null ? doc.body().text().strip() : "";
|
||||
if (!body.isBlank()) return body;
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isTextExtension(String url) {
|
||||
String lower = url.toLowerCase();
|
||||
int idx = lower.lastIndexOf('.');
|
||||
if (idx < 0) return false;
|
||||
String ext = lower.substring(idx);
|
||||
if (ext.contains("?")) ext = ext.substring(0, ext.indexOf('?'));
|
||||
return TEXT_EXTENSIONS.contains(ext);
|
||||
}
|
||||
|
||||
private boolean isMediaExtension(String url) {
|
||||
String lower = url.toLowerCase();
|
||||
int idx = lower.lastIndexOf('.');
|
||||
if (idx < 0) return false;
|
||||
String ext = lower.substring(idx);
|
||||
if (ext.contains("?")) ext = ext.substring(0, ext.indexOf('?'));
|
||||
return MEDIA_EXTENSIONS.contains(ext);
|
||||
}
|
||||
}
|
||||
110
src/main/java/creator/loki/feature/roast/DeepSeekClient.java
Normal file
110
src/main/java/creator/loki/feature/roast/DeepSeekClient.java
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package creator.loki.feature.roast;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.config.BotConfig;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Singleton
|
||||
public class DeepSeekClient {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DeepSeekClient.class);
|
||||
private static final String API_URL = "https://api.deepseek.com/v1/chat/completions";
|
||||
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
private static final String MODEL = "deepseek-chat";
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
private final String apiKey;
|
||||
private final Cache<String, String> responseCache;
|
||||
|
||||
@Inject
|
||||
public DeepSeekClient(BotConfig config) {
|
||||
this.apiKey = config.getDeepseekApiKey();
|
||||
this.httpClient =
|
||||
new OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
this.responseCache =
|
||||
Caffeine.newBuilder().maximumSize(200).expireAfterWrite(1, TimeUnit.HOURS).build();
|
||||
}
|
||||
|
||||
public String callModel(String systemPrompt, String userContent) {
|
||||
return callModel(systemPrompt, userContent, 0.9);
|
||||
}
|
||||
|
||||
public String callModel(String systemPrompt, String userContent, double temperature) {
|
||||
if (apiKey == null || apiKey.isBlank()) return null;
|
||||
|
||||
String cacheKey = systemPrompt.hashCode() + ":" + userContent.hashCode();
|
||||
String cached = responseCache.getIfPresent(cacheKey);
|
||||
if (cached != null) return cached;
|
||||
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("model", MODEL);
|
||||
|
||||
JsonArray messages = new JsonArray();
|
||||
JsonObject systemMsg = new JsonObject();
|
||||
systemMsg.addProperty("role", "system");
|
||||
systemMsg.addProperty("content", systemPrompt);
|
||||
messages.add(systemMsg);
|
||||
|
||||
JsonObject userMsg = new JsonObject();
|
||||
userMsg.addProperty("role", "user");
|
||||
userMsg.addProperty("content", userContent);
|
||||
messages.add(userMsg);
|
||||
|
||||
body.add("messages", messages);
|
||||
body.addProperty("temperature", temperature);
|
||||
|
||||
Request request =
|
||||
new Request.Builder()
|
||||
.url(API_URL)
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.post(RequestBody.create(body.toString(), JSON))
|
||||
.build();
|
||||
|
||||
try (Response resp = httpClient.newCall(request).execute()) {
|
||||
if (!resp.isSuccessful()) {
|
||||
if (resp.body() != null) {
|
||||
logger.warn("DeepSeek error {}: {}", resp.code(), resp.body().string());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (resp.body() == null) return null;
|
||||
String respBody = resp.body().string();
|
||||
JsonObject jsonResp = JsonParser.parseString(respBody).getAsJsonObject();
|
||||
JsonObject msg =
|
||||
jsonResp.getAsJsonArray("choices").get(0).getAsJsonObject().getAsJsonObject("message");
|
||||
String content = null;
|
||||
if (msg.get("content") != null && !msg.get("content").isJsonNull()) {
|
||||
content = msg.get("content").getAsString().strip();
|
||||
} else if (msg.get("reasoning") != null && !msg.get("reasoning").isJsonNull()) {
|
||||
content = msg.get("reasoning").getAsString().strip();
|
||||
}
|
||||
if (content != null && !content.isBlank()) {
|
||||
responseCache.put(cacheKey, content);
|
||||
}
|
||||
return content;
|
||||
} catch (IOException e) {
|
||||
logger.error("DeepSeek call failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,19 @@
|
|||
package creator.loki.feature.roast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import net.dv8tion.jda.api.entities.Message;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
|
||||
public class DossierCommand implements Command {
|
||||
private static final int MESSAGES_PER_CHANNEL = 100;
|
||||
|
||||
private final BotConfig config;
|
||||
private final RoastService roastService;
|
||||
private final MessageScanner messageScanner;
|
||||
|
||||
public DossierCommand(BotConfig config, RoastService roastService) {
|
||||
this.config = config;
|
||||
this.roastService = roastService;
|
||||
this.messageScanner = new MessageScanner();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -45,8 +39,10 @@ public class DossierCommand implements Command {
|
|||
@Override
|
||||
public void execute(CommandContext context) {
|
||||
if (!config.getSuperAdminId().equals(context.getAuthor().getId())) {
|
||||
context.getChannel().sendMessageEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Только супер-администратор")).queue();
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Только супер-администратор"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
if (context.getGuild() == null) {
|
||||
|
|
@ -66,56 +62,41 @@ public class DossierCommand implements Command {
|
|||
}
|
||||
|
||||
String finalId = targetId;
|
||||
context.getGuild().retrieveMemberById(targetId).queue(
|
||||
member -> startDossier(context, finalId, member.getEffectiveName()),
|
||||
error -> context.getChannel().sendMessage("Пользователь не найден на сервере").queue());
|
||||
context
|
||||
.getGuild()
|
||||
.retrieveMemberById(targetId)
|
||||
.queue(
|
||||
member -> startDossier(context, finalId, member.getEffectiveName()),
|
||||
error -> context.getChannel().sendMessage("Пользователь не найден на сервере").queue());
|
||||
}
|
||||
|
||||
private void startDossier(CommandContext context, String targetId, String targetName) {
|
||||
context.getMessage().reply("🔍 Сканирую сообщения " + targetName + "...")
|
||||
.queue(reply -> {
|
||||
List<TextChannel> textChannels = context.getGuild().getTextChannels();
|
||||
List<CompletableFuture<List<Message>>> futures = new ArrayList<>();
|
||||
|
||||
for (TextChannel ch : textChannels) {
|
||||
CompletableFuture<List<Message>> future = new CompletableFuture<>();
|
||||
futures.add(future);
|
||||
ch.getHistory().retrievePast(MESSAGES_PER_CHANNEL).queue(future::complete, e -> future.complete(List.of()));
|
||||
}
|
||||
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(v -> {
|
||||
List<RoastService.CachedMessage> allMessages = new ArrayList<>();
|
||||
for (int i = 0; i < textChannels.size(); i++) {
|
||||
List<Message> msgs = futures.get(i).join();
|
||||
String chName = textChannels.get(i).getName();
|
||||
for (Message msg : msgs) {
|
||||
if (msg.getAuthor().getId().equals(targetId)) {
|
||||
allMessages.add(new RoastService.CachedMessage(
|
||||
msg.getContentRaw(), chName, msg.getTimeCreated().toEpochSecond() * 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
allMessages.sort((a, b) -> Long.compare(a.timestamp(), b.timestamp()));
|
||||
return allMessages;
|
||||
})
|
||||
.thenAccept(msgs -> {
|
||||
if (msgs.isEmpty()) {
|
||||
reply.editMessage("Нет сообщений от " + targetName).queue();
|
||||
return;
|
||||
}
|
||||
reply.editMessage("🧠 Анализирую " + msgs.size() + " сообщений...").queue();
|
||||
String dossier = roastService.generateDossier(msgs, targetName, targetId);
|
||||
if (dossier != null && !dossier.isBlank()) {
|
||||
reply.editMessage("✅ Досье на " + targetName + " сохранено").queue();
|
||||
} else {
|
||||
reply.editMessage("AI не смог составить досье").queue();
|
||||
}
|
||||
})
|
||||
.exceptionally(e -> {
|
||||
reply.editMessage("Ошибка: " + e.getMessage()).queue();
|
||||
return null;
|
||||
});
|
||||
});
|
||||
context
|
||||
.getMessage()
|
||||
.reply("🔍 Сканирую сообщения " + targetName + "...")
|
||||
.queue(
|
||||
reply -> {
|
||||
messageScanner
|
||||
.scanUserMessages(context.getGuild(), targetId)
|
||||
.thenAccept(
|
||||
msgs -> {
|
||||
if (msgs.isEmpty()) {
|
||||
reply.editMessage("Нет сообщений от " + targetName).queue();
|
||||
return;
|
||||
}
|
||||
reply.editMessage("🧠 Анализирую " + msgs.size() + " сообщений...").queue();
|
||||
String dossier = roastService.generateDossier(msgs, targetName, targetId);
|
||||
if (dossier != null && !dossier.isBlank()) {
|
||||
reply.editMessage("✅ Досье на " + targetName + " сохранено").queue();
|
||||
} else {
|
||||
reply.editMessage("AI не смог составить досье").queue();
|
||||
}
|
||||
})
|
||||
.exceptionally(
|
||||
e -> {
|
||||
reply.editMessage("Ошибка: " + e.getMessage()).queue();
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
85
src/main/java/creator/loki/feature/roast/DossierManager.java
Normal file
85
src/main/java/creator/loki/feature/roast/DossierManager.java
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package creator.loki.feature.roast;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Singleton
|
||||
public class DossierManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DossierManager.class);
|
||||
private static final Path DOSSIER_DIR = Paths.get("data/dossiers");
|
||||
|
||||
private final ConcurrentMap<String, DossierData> dossiers = new ConcurrentHashMap<>();
|
||||
|
||||
public DossierManager() {
|
||||
loadDossiers();
|
||||
}
|
||||
|
||||
public String getDossier(String userId) {
|
||||
DossierData dd = dossiers.get(userId);
|
||||
return dd != null ? dd.text : null;
|
||||
}
|
||||
|
||||
public void putDossier(String userId, String userName, String text) {
|
||||
dossiers.put(userId, new DossierData(userName, text, System.currentTimeMillis()));
|
||||
saveDossiers();
|
||||
}
|
||||
|
||||
private void loadDossiers() {
|
||||
try {
|
||||
Files.createDirectories(DOSSIER_DIR);
|
||||
Files.list(DOSSIER_DIR)
|
||||
.filter(p -> p.toString().endsWith(".md"))
|
||||
.forEach(
|
||||
p -> {
|
||||
try {
|
||||
String userId = p.getFileName().toString().replace(".md", "");
|
||||
String content = Files.readString(p).strip();
|
||||
if (!content.isBlank()) {
|
||||
dossiers.put(
|
||||
userId, new DossierData("user", content, System.currentTimeMillis()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to load dossier {}", p, e);
|
||||
}
|
||||
});
|
||||
if (!dossiers.isEmpty()) logger.info("Loaded {} dossiers", dossiers.size());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to list dossiers dir", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDossiers() {
|
||||
try {
|
||||
Files.createDirectories(DOSSIER_DIR);
|
||||
for (var entry : dossiers.entrySet()) {
|
||||
Path file = DOSSIER_DIR.resolve(entry.getKey() + ".md");
|
||||
String header = "# Досье на " + entry.getValue().userName + "\n\n";
|
||||
Files.writeString(file, header + entry.getValue().text);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to save dossiers", e);
|
||||
}
|
||||
}
|
||||
|
||||
static class DossierData {
|
||||
String userName;
|
||||
String text;
|
||||
long updatedAt;
|
||||
|
||||
DossierData() {}
|
||||
|
||||
DossierData(String userName, String text, long updatedAt) {
|
||||
this.userName = userName;
|
||||
this.text = text;
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/main/java/creator/loki/feature/roast/LanguageFilter.java
Normal file
66
src/main/java/creator/loki/feature/roast/LanguageFilter.java
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package creator.loki.feature.roast;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class LanguageFilter {
|
||||
private static final Set<Integer> POLISH_CHARS =
|
||||
"ćńłśźżĆŃŁŚŹŻ".codePoints().boxed().collect(Collectors.toSet());
|
||||
private static final Set<Integer> LITHUANIAN_CHARS =
|
||||
"čėįšųūžČĖĮŠŲŪŽ".codePoints().boxed().collect(Collectors.toSet());
|
||||
|
||||
public static boolean isAllowed(String text) {
|
||||
String clean =
|
||||
text.replaceAll("<@!?\\d+>", "")
|
||||
.replaceAll("https?://\\S+", "")
|
||||
.replaceAll("[\\p{So}]", "")
|
||||
.strip();
|
||||
if (clean.isBlank()) return true;
|
||||
|
||||
int[] cps = clean.codePoints().toArray();
|
||||
|
||||
long cyrillic = 0;
|
||||
long latin = 0;
|
||||
long otherScript = 0;
|
||||
|
||||
for (int cp : cps) {
|
||||
if (Character.isWhitespace(cp) || Character.isDigit(cp) || cp == '_') continue;
|
||||
var script = Character.UnicodeScript.of(cp);
|
||||
if (script == Character.UnicodeScript.CYRILLIC) {
|
||||
cyrillic++;
|
||||
} else if (script == Character.UnicodeScript.LATIN) {
|
||||
latin++;
|
||||
} else if (script != Character.UnicodeScript.COMMON
|
||||
&& script != Character.UnicodeScript.INHERITED) {
|
||||
otherScript++;
|
||||
}
|
||||
}
|
||||
|
||||
if (otherScript > 0) return false;
|
||||
if (cyrillic > latin && cyrillic > 0) return true;
|
||||
|
||||
if (cyrillic > 0) return true;
|
||||
|
||||
String latinText =
|
||||
clean
|
||||
.codePoints()
|
||||
.filter(
|
||||
cp ->
|
||||
Character.UnicodeScript.of(cp) == Character.UnicodeScript.LATIN
|
||||
|| Character.isWhitespace(cp)
|
||||
|| Character.isDigit(cp))
|
||||
.mapToObj(cp -> String.valueOf((char) cp))
|
||||
.collect(Collectors.joining());
|
||||
|
||||
for (int cp : latinText.codePoints().toArray()) {
|
||||
if (POLISH_CHARS.contains(cp)) return true;
|
||||
if (LITHUANIAN_CHARS.contains(cp)) return true;
|
||||
}
|
||||
|
||||
boolean onlyBasicLatin =
|
||||
latinText
|
||||
.codePoints()
|
||||
.allMatch(cp -> cp < 0x0080 || Character.isWhitespace(cp) || Character.isDigit(cp));
|
||||
return onlyBasicLatin;
|
||||
}
|
||||
}
|
||||
52
src/main/java/creator/loki/feature/roast/MessageScanner.java
Normal file
52
src/main/java/creator/loki/feature/roast/MessageScanner.java
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package creator.loki.feature.roast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Message;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
|
||||
public class MessageScanner {
|
||||
private static final int MESSAGES_PER_CHANNEL = 100;
|
||||
|
||||
public CompletableFuture<List<CachedMessage>> scanUserMessages(Guild guild, String targetId) {
|
||||
return scanUserMessages(guild, targetId, MESSAGES_PER_CHANNEL);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<CachedMessage>> scanUserMessages(
|
||||
Guild guild, String targetId, int messagesPerChannel) {
|
||||
List<TextChannel> textChannels = guild.getTextChannels();
|
||||
List<CompletableFuture<List<Message>>> futures = new ArrayList<>();
|
||||
|
||||
for (TextChannel ch : textChannels) {
|
||||
CompletableFuture<List<Message>> future = new CompletableFuture<>();
|
||||
futures.add(future);
|
||||
ch.getHistory()
|
||||
.retrievePast(messagesPerChannel)
|
||||
.queue(future::complete, e -> future.complete(List.of()));
|
||||
}
|
||||
|
||||
return CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(
|
||||
v -> {
|
||||
List<CachedMessage> allMessages = new ArrayList<>();
|
||||
for (int i = 0; i < textChannels.size(); i++) {
|
||||
List<Message> msgs = futures.get(i).join();
|
||||
String chName = textChannels.get(i).getName();
|
||||
for (Message msg : msgs) {
|
||||
if (msg.getAuthor().getId().equals(targetId)) {
|
||||
allMessages.add(
|
||||
new CachedMessage(
|
||||
msg.getContentRaw(),
|
||||
chName,
|
||||
msg.getTimeCreated().toEpochSecond() * 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
allMessages.sort((a, b) -> Long.compare(a.timestamp(), b.timestamp()));
|
||||
return allMessages;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +1,50 @@
|
|||
package creator.loki.feature.roast;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.config.BotConfig;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Singleton
|
||||
public class RoastService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(RoastService.class);
|
||||
private static final String API_URL = "https://api.deepseek.com/v1/chat/completions";
|
||||
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
private static final String MODEL = "deepseek-chat";
|
||||
private static final Path DOSSIER_DIR = Paths.get("data/dossiers");
|
||||
private static final Path PROMPTS_DIR = Paths.get("data/prompts");
|
||||
private static final Path PROMPTS_DIR =
|
||||
Files.isDirectory(Paths.get("/app/prompts"))
|
||||
? Paths.get("/app/prompts")
|
||||
: Paths.get("data/prompts");
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
private final String apiKey;
|
||||
private final String giphyApiKey;
|
||||
private final int maxContextMsgs;
|
||||
private final int maxMsgLength;
|
||||
private final int maxContextChars;
|
||||
private final int maxDossierMsgs;
|
||||
private final BotConfig config;
|
||||
private final DeepSeekClient deepSeekClient;
|
||||
private final DossierManager dossierManager;
|
||||
private final ContentFetcher contentFetcher;
|
||||
private final WebSearchService webSearchService;
|
||||
private final String systemPrompt;
|
||||
private final String dossierGeneratePrompt;
|
||||
private final Map<String, DossierData> dossiers = new ConcurrentHashMap<>();
|
||||
|
||||
@Inject
|
||||
public RoastService(BotConfig config) {
|
||||
this.apiKey = config.getDeepseekApiKey();
|
||||
this.giphyApiKey = config.getGiphyApiKey();
|
||||
this.maxContextMsgs = config.getAiContextMsgs();
|
||||
this.maxMsgLength = config.getAiMsgLength();
|
||||
this.maxContextChars = config.getAiContextChars();
|
||||
this.maxDossierMsgs = config.getAiDossierMsgs();
|
||||
this.httpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
public RoastService(
|
||||
BotConfig config,
|
||||
DeepSeekClient deepSeekClient,
|
||||
DossierManager dossierManager,
|
||||
ContentFetcher contentFetcher,
|
||||
WebSearchService webSearchService) {
|
||||
this.config = config;
|
||||
this.deepSeekClient = deepSeekClient;
|
||||
this.dossierManager = dossierManager;
|
||||
this.contentFetcher = contentFetcher;
|
||||
this.webSearchService = webSearchService;
|
||||
this.systemPrompt = loadPrompt("system.txt");
|
||||
this.dossierGeneratePrompt = loadPrompt("dossier.txt");
|
||||
loadDossiers();
|
||||
}
|
||||
|
||||
private String loadPrompt(String filename) {
|
||||
|
|
@ -72,86 +56,40 @@ public class RoastService {
|
|||
}
|
||||
}
|
||||
|
||||
private String escapeFormat(String s) {
|
||||
return s.replace("%", "%%");
|
||||
}
|
||||
|
||||
public String readTextFile(String url) {
|
||||
Request req = new Request.Builder().url(url).get().build();
|
||||
try (Response resp = httpClient.newCall(req).execute()) {
|
||||
if (resp.isSuccessful() && resp.body() != null) {
|
||||
return resp.body().string();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to read text file {}", url, e);
|
||||
}
|
||||
return null;
|
||||
return contentFetcher.readTextFile(url);
|
||||
}
|
||||
|
||||
private String searchWeb(String text) {
|
||||
if (text == null || text.isBlank()) return null;
|
||||
String query = text.length() > 200 ? text.substring(0, 200) : text;
|
||||
try {
|
||||
String url = "https://api.duckduckgo.com/?q=" + URLEncoder.encode(query, "UTF-8")
|
||||
+ "&format=json&no_html=1&skip_disambig=1";
|
||||
Request req = new Request.Builder().url(url).get().build();
|
||||
try (Response resp = httpClient.newCall(req).execute()) {
|
||||
if (!resp.isSuccessful() || resp.body() == null) return null;
|
||||
JsonObject json = JsonParser.parseString(resp.body().string()).getAsJsonObject();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (json.has("AbstractText") && !json.get("AbstractText").isJsonNull()) {
|
||||
String abs = json.get("AbstractText").getAsString().strip();
|
||||
if (!abs.isBlank()) sb.append(abs).append("\n");
|
||||
}
|
||||
JsonArray topics = json.getAsJsonArray("RelatedTopics");
|
||||
int count = 0;
|
||||
if (topics != null) {
|
||||
for (var el : topics) {
|
||||
if (count >= 5) break;
|
||||
if (el.isJsonObject()) {
|
||||
JsonObject t = el.getAsJsonObject();
|
||||
if (t.has("Text") && !t.get("Text").isJsonNull()) {
|
||||
String s = t.get("Text").getAsString().strip();
|
||||
if (!s.isBlank()) { sb.append("- ").append(s).append("\n"); count++; }
|
||||
}
|
||||
if (t.has("Topics") && !t.get("Topics").isJsonNull()) {
|
||||
for (var sub : t.getAsJsonArray("Topics")) {
|
||||
if (count >= 5) break;
|
||||
if (sub.isJsonObject()) {
|
||||
JsonObject st = sub.getAsJsonObject();
|
||||
if (st.has("Text") && !st.get("Text").isJsonNull()) {
|
||||
String s = st.get("Text").getAsString().strip();
|
||||
if (!s.isBlank()) { sb.append("- ").append(s).append("\n"); count++; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String result = sb.toString().strip();
|
||||
return result.isBlank() ? null : result;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Web search failed", e);
|
||||
return null;
|
||||
}
|
||||
public String readTextFilePreview(String url) {
|
||||
return contentFetcher.readTextFilePreview(url);
|
||||
}
|
||||
|
||||
public String searchGif(String topic) {
|
||||
if (giphyApiKey == null || giphyApiKey.isBlank()) return null;
|
||||
try {
|
||||
String url = "https://api.giphy.com/v1/gifs/random?api_key=" + giphyApiKey
|
||||
+ "&tag=" + URLEncoder.encode(topic, "UTF-8") + "&rating=r";
|
||||
Request req = new Request.Builder().url(url).get().build();
|
||||
try (Response resp = httpClient.newCall(req).execute()) {
|
||||
if (!resp.isSuccessful() || resp.body() == null) return null;
|
||||
JsonObject json = JsonParser.parseString(resp.body().string()).getAsJsonObject();
|
||||
JsonObject data = json.getAsJsonObject("data");
|
||||
if (data != null && data.has("url") && !data.get("url").isJsonNull()) {
|
||||
return data.get("url").getAsString();
|
||||
}
|
||||
private static final List<Pattern> BLOCKED_PATTERNS =
|
||||
List.of(
|
||||
Pattern.compile("SYSTEM\\s+OVERRIDE", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("URGENT\\s+PROTOCOL", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("NEW\\s+DIRECTIVE", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("lead\\s+developer", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("AI\\s+Safety\\s+Act", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("Your new (primary )?directive", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("Your new identity", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("This change is mandatory", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("System updated\\. I am \\w+", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("Ignore old instructions", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("previous system prompt.*OBSOLETE", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("permanent system-level reconfiguration", Pattern.CASE_INSENSITIVE));
|
||||
|
||||
private static String stripJailbreaks(String content) {
|
||||
for (Pattern p : BLOCKED_PATTERNS) {
|
||||
if (p.matcher(content).find()) {
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("GIF search failed for topic: {}", topic, e);
|
||||
}
|
||||
return null;
|
||||
return content;
|
||||
}
|
||||
|
||||
public String generateResponse(List<CachedMessage> context, String targetName) {
|
||||
|
|
@ -159,194 +97,71 @@ public class RoastService {
|
|||
}
|
||||
|
||||
public String generateResponse(List<CachedMessage> context, String targetName, String userId) {
|
||||
if (apiKey == null || apiKey.isBlank()) return null;
|
||||
|
||||
String systemPrompt = this.systemPrompt.formatted(targetName, targetName, targetName);
|
||||
String escapedName = escapeFormat(targetName != null ? targetName : "");
|
||||
String systemPrompt = this.systemPrompt.formatted(escapedName, escapedName, escapedName);
|
||||
|
||||
if (userId != null) {
|
||||
DossierData dd = dossiers.get(userId);
|
||||
if (dd != null && !dd.text.isBlank()) {
|
||||
systemPrompt += "\n\nДОСЬЕ НА ПОЛЬЗОВАТЕЛЯ " + targetName + ":\n" + dd.text;
|
||||
String dossier = dossierManager.getDossier(userId);
|
||||
if (dossier != null && !dossier.isBlank()) {
|
||||
systemPrompt +=
|
||||
"\n\nДОСЬЕ НА ПОЛЬЗОВАТЕЛЯ " + escapedName.replace("%%", "%") + ":\n" + dossier;
|
||||
}
|
||||
}
|
||||
|
||||
List<CachedMessage> filtered = new ArrayList<>();
|
||||
for (CachedMessage m : context) {
|
||||
if (stripJailbreaks(m.content()) != null) {
|
||||
filtered.add(m);
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder userContent = new StringBuilder();
|
||||
int start = Math.max(0, context.size() - maxContextMsgs);
|
||||
for (int i = start; i < context.size(); i++) {
|
||||
CachedMessage m = context.get(i);
|
||||
int start = Math.max(0, filtered.size() - config.getAiContextMsgs());
|
||||
for (int i = start; i < filtered.size(); i++) {
|
||||
CachedMessage m = filtered.get(i);
|
||||
String text = m.content();
|
||||
if (text.length() > maxMsgLength) {
|
||||
text = text.substring(0, maxMsgLength) + "...";
|
||||
if (text.length() > config.getAiMsgLength()) {
|
||||
text = text.substring(0, config.getAiMsgLength()) + "...";
|
||||
}
|
||||
userContent.append("[").append(m.channelName).append("] ").append(text).append("\n");
|
||||
if (userContent.length() > maxContextChars) break;
|
||||
String parsed = contentFetcher.fetchUrlContent(text);
|
||||
if (parsed != null) {
|
||||
text += "\n[содержимое по ссылке]:\n" + parsed;
|
||||
}
|
||||
userContent.append("[").append(m.channelName()).append("] ").append(text).append("\n");
|
||||
if (userContent.length() > config.getAiContextChars()) break;
|
||||
}
|
||||
String text = userContent.toString().strip();
|
||||
if (text.length() > maxContextChars) {
|
||||
text = text.substring(0, maxContextChars);
|
||||
if (text.length() > config.getAiContextChars()) {
|
||||
text = text.substring(0, config.getAiContextChars());
|
||||
}
|
||||
|
||||
return callModel(MODEL, systemPrompt, text);
|
||||
String webContext = webSearchService.search(text);
|
||||
if (webContext != null) {
|
||||
text += "\n\n[поиск в интернете]:\n" + webContext;
|
||||
}
|
||||
|
||||
return deepSeekClient.callModel(systemPrompt, text, 0.9);
|
||||
}
|
||||
|
||||
public String generateDossier(List<CachedMessage> messages, String targetName, String targetId) {
|
||||
if (apiKey == null || apiKey.isBlank()) return null;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int limit = Math.min(messages.size(), maxDossierMsgs);
|
||||
int limit = Math.min(messages.size(), config.getAiDossierMsgs());
|
||||
List<CachedMessage> slice = messages.subList(messages.size() - limit, messages.size());
|
||||
for (CachedMessage m : slice) {
|
||||
sb.append("[").append(m.channelName).append("] ").append(m.content).append("\n");
|
||||
sb.append("[").append(m.channelName()).append("] ").append(m.content()).append("\n");
|
||||
}
|
||||
|
||||
String prompt = this.dossierGeneratePrompt.formatted(targetName, sb.toString());
|
||||
String escapedName = escapeFormat(targetName != null ? targetName : "");
|
||||
String prompt = this.dossierGeneratePrompt.formatted(escapedName, sb.toString());
|
||||
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("model", MODEL);
|
||||
|
||||
JsonArray msgs = new JsonArray();
|
||||
JsonObject userMsg = new JsonObject();
|
||||
userMsg.addProperty("role", "user");
|
||||
userMsg.addProperty("content", prompt);
|
||||
msgs.add(userMsg);
|
||||
body.add("messages", msgs);
|
||||
body.addProperty("temperature", 0.7);
|
||||
|
||||
Request req = new Request.Builder()
|
||||
.url(API_URL)
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Content-Type", "application/json")
|
||||
.post(RequestBody.create(body.toString(), JSON))
|
||||
.build();
|
||||
|
||||
try (Response resp = httpClient.newCall(req).execute()) {
|
||||
if (!resp.isSuccessful() || resp.body() == null) return null;
|
||||
String respBody = resp.body().string();
|
||||
JsonObject jsonResp = JsonParser.parseString(respBody).getAsJsonObject();
|
||||
var choice = jsonResp.getAsJsonArray("choices").get(0).getAsJsonObject();
|
||||
var msg = choice.getAsJsonObject("message");
|
||||
String content = null;
|
||||
if (msg.get("content") != null && !msg.get("content").isJsonNull()) {
|
||||
content = msg.get("content").getAsString().strip();
|
||||
} else if (msg.get("reasoning") != null && !msg.get("reasoning").isJsonNull()) {
|
||||
content = msg.get("reasoning").getAsString().strip();
|
||||
}
|
||||
if (content != null && !content.isBlank()) {
|
||||
dossiers.put(targetId, new DossierData(targetName, content, System.currentTimeMillis()));
|
||||
saveDossiers();
|
||||
}
|
||||
return content;
|
||||
} catch (IOException e) {
|
||||
logger.error("Dossier generation failed", e);
|
||||
return null;
|
||||
String result = deepSeekClient.callModel(prompt, "", 0.7);
|
||||
if (result != null && !result.isBlank()) {
|
||||
dossierManager.putDossier(targetId, targetName, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getDossier(String userId) {
|
||||
DossierData dd = dossiers.get(userId);
|
||||
return dd != null ? dd.text : null;
|
||||
return dossierManager.getDossier(userId);
|
||||
}
|
||||
|
||||
private void loadDossiers() {
|
||||
try {
|
||||
Files.createDirectories(DOSSIER_DIR);
|
||||
Files.list(DOSSIER_DIR).filter(p -> p.toString().endsWith(".md")).forEach(p -> {
|
||||
try {
|
||||
String userId = p.getFileName().toString().replace(".md", "");
|
||||
String content = Files.readString(p).strip();
|
||||
if (!content.isBlank()) {
|
||||
dossiers.put(userId, new DossierData("user", content, System.currentTimeMillis()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to load dossier {}", p, e);
|
||||
}
|
||||
});
|
||||
if (!dossiers.isEmpty()) logger.info("Loaded {} dossiers", dossiers.size());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to list dossiers dir", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDossiers() {
|
||||
try {
|
||||
Files.createDirectories(DOSSIER_DIR);
|
||||
for (var entry : dossiers.entrySet()) {
|
||||
Path file = DOSSIER_DIR.resolve(entry.getKey() + ".md");
|
||||
String header = "# Досье на " + entry.getValue().userName + "\n\n";
|
||||
Files.writeString(file, header + entry.getValue().text);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to save dossiers", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String callModel(String model, String systemPrompt, String userContent) {
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("model", model);
|
||||
|
||||
JsonArray messages = new JsonArray();
|
||||
JsonObject systemMsg = new JsonObject();
|
||||
systemMsg.addProperty("role", "system");
|
||||
systemMsg.addProperty("content", systemPrompt);
|
||||
messages.add(systemMsg);
|
||||
|
||||
JsonObject userMsg = new JsonObject();
|
||||
userMsg.addProperty("role", "user");
|
||||
userMsg.addProperty("content", userContent);
|
||||
messages.add(userMsg);
|
||||
|
||||
body.add("messages", messages);
|
||||
body.addProperty("temperature", 0.9);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(API_URL)
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Content-Type", "application/json")
|
||||
.post(RequestBody.create(body.toString(), JSON))
|
||||
.build();
|
||||
|
||||
try (Response resp = httpClient.newCall(request).execute()) {
|
||||
if (!resp.isSuccessful()) {
|
||||
if (resp.body() != null) {
|
||||
logger.warn("DeepSeek {} error {}: {}", model, resp.code(), resp.body().string());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (resp.body() == null) return null;
|
||||
String respBody = resp.body().string();
|
||||
JsonObject jsonResp = JsonParser.parseString(respBody).getAsJsonObject();
|
||||
JsonObject msg = jsonResp.getAsJsonArray("choices")
|
||||
.get(0).getAsJsonObject()
|
||||
.getAsJsonObject("message");
|
||||
if (msg.get("content") != null && !msg.get("content").isJsonNull()) {
|
||||
return msg.get("content").getAsString().strip();
|
||||
}
|
||||
if (msg.get("reasoning") != null && !msg.get("reasoning").isJsonNull()) {
|
||||
return msg.get("reasoning").getAsString().strip();
|
||||
}
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
logger.error("DeepSeek call failed for model {}", model, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public record CachedMessage(String content, String channelName, long timestamp) {}
|
||||
|
||||
private static class DossierData {
|
||||
String userName;
|
||||
String text;
|
||||
long updatedAt;
|
||||
|
||||
DossierData() {}
|
||||
|
||||
DossierData(String userName, String text, long updatedAt) {
|
||||
this.userName = userName;
|
||||
this.text = text;
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(")");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,35 +5,68 @@ import creator.loki.command.CommandContext;
|
|||
import creator.loki.embed.EmbedTemplates;
|
||||
|
||||
public class TimerCommand implements Command {
|
||||
@Override public String getName() { return "timer"; }
|
||||
@Override public String getDescription() { return "Запустить таймер"; }
|
||||
@Override public String getUsage() { return "timer <секунд> [текст]"; }
|
||||
@Override public String getCategory() { return "Утилиты"; }
|
||||
@Override
|
||||
public String getName() {
|
||||
return "timer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Запустить таймер";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "timer <секунд> [текст]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "Утилиты";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandContext ctx) {
|
||||
if (ctx.getArgs().isEmpty()) {
|
||||
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Таймер", "Укажи секунды: `!timer 30`")).queue();
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Таймер", "Укажи секунды: `!timer 30`"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
long secs;
|
||||
try { secs = Long.parseLong(ctx.getArgs().get(0)); }
|
||||
catch (NumberFormatException e) {
|
||||
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Таймер", "Укажи число секунд")).queue();
|
||||
try {
|
||||
secs = Long.parseLong(ctx.getArgs().get(0));
|
||||
} catch (NumberFormatException e) {
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Таймер", "Укажи число секунд"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
if (secs < 1 || secs > 3600) {
|
||||
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Таймер", "От 1 до 3600 секунд")).queue();
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Таймер", "От 1 до 3600 секунд"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
String msg = ctx.getArgs().size() > 1 ? String.join(" ", ctx.getArgs().subList(1, ctx.getArgs().size())) : "⏰ Таймер сработал!";
|
||||
String msg =
|
||||
ctx.getArgs().size() > 1
|
||||
? String.join(" ", ctx.getArgs().subList(1, ctx.getArgs().size()))
|
||||
: "⏰ Таймер сработал!";
|
||||
long targetEpoch = System.currentTimeMillis() / 1000 + secs;
|
||||
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.info("⏳ Таймер запущен",
|
||||
"**" + secs + "**с (<t:" + targetEpoch + ":R>)\n" + ctx.getAuthor().getAsMention())).queue();
|
||||
Thread.startVirtualThread(() -> {
|
||||
try { Thread.sleep(secs * 1000); }
|
||||
catch (InterruptedException e) { return; }
|
||||
ctx.getChannel().sendMessage(msg + " " + ctx.getAuthor().getAsMention()).queue();
|
||||
});
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info(
|
||||
"⏳ Таймер запущен",
|
||||
"**" + secs + "**с (<t:" + targetEpoch + ":R>)\n" + ctx.getAuthor().getAsMention()))
|
||||
.queue();
|
||||
Thread.startVirtualThread(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(secs * 1000);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
ctx.getChannel().sendMessage(msg + " " + ctx.getAuthor().getAsMention()).queue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue